fix(api): add global /api prefix to resolve frontend route mismatch
Some checks failed
ci/woodpecker/push/api Pipeline failed

Frontend API client prefixes all paths with /api/ (e.g. /api/projects,
/api/knowledge/entries) but backend controllers had no global prefix,
causing 404s on all data-fetching pages in production.

- Add setGlobalPrefix('api') in main.ts with exclusions for /health
  (Docker healthcheck) and /auth/* (BetterAuth OAuth flow)
- Strip redundant 'api/' from federation and CSRF controller paths
  that already included the prefix manually

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 19:01:40 -06:00
parent 9a7673bea2
commit 0c40630aa0
7 changed files with 17 additions and 7 deletions

View File

@@ -16,7 +16,7 @@ interface AuthenticatedRequest extends Request {
user?: AuthenticatedUser; user?: AuthenticatedUser;
} }
@Controller("api/v1/csrf") @Controller("v1/csrf")
export class CsrfController { export class CsrfController {
constructor(private readonly csrfService: CsrfService) {} constructor(private readonly csrfService: CsrfService) {}

View File

@@ -12,7 +12,7 @@ import type { AuthenticatedRequest } from "../common/types/user.types";
import type { CommandMessageDetails, CommandResponse } from "./types/message.types"; import type { CommandMessageDetails, CommandResponse } from "./types/message.types";
import type { FederationMessageStatus } from "@prisma/client"; import type { FederationMessageStatus } from "@prisma/client";
@Controller("api/v1/federation") @Controller("v1/federation")
export class CommandController { export class CommandController {
private readonly logger = new Logger(CommandController.name); private readonly logger = new Logger(CommandController.name);

View File

@@ -23,7 +23,7 @@ import {
IncomingEventAckDto, IncomingEventAckDto,
} from "./dto/event.dto"; } from "./dto/event.dto";
@Controller("api/v1/federation") @Controller("v1/federation")
export class EventController { export class EventController {
private readonly logger = new Logger(EventController.name); private readonly logger = new Logger(EventController.name);

View File

@@ -18,7 +18,7 @@ import {
ValidateFederatedTokenDto, ValidateFederatedTokenDto,
} from "./dto/federated-auth.dto"; } from "./dto/federated-auth.dto";
@Controller("api/v1/federation/auth") @Controller("v1/federation/auth")
export class FederationAuthController { export class FederationAuthController {
private readonly logger = new Logger(FederationAuthController.name); private readonly logger = new Logger(FederationAuthController.name);

View File

@@ -27,7 +27,7 @@ import {
} from "./dto/connection.dto"; } from "./dto/connection.dto";
import { FederationConnectionStatus } from "@prisma/client"; import { FederationConnectionStatus } from "@prisma/client";
@Controller("api/v1/federation") @Controller("v1/federation")
export class FederationController { export class FederationController {
private readonly logger = new Logger(FederationController.name); private readonly logger = new Logger(FederationController.name);

View File

@@ -12,7 +12,7 @@ import type { AuthenticatedRequest } from "../common/types/user.types";
import type { QueryMessageDetails, QueryResponse } from "./types/message.types"; import type { QueryMessageDetails, QueryResponse } from "./types/message.types";
import type { FederationMessageStatus } from "@prisma/client"; import type { FederationMessageStatus } from "@prisma/client";
@Controller("api/v1/federation") @Controller("v1/federation")
export class QueryController { export class QueryController {
private readonly logger = new Logger(QueryController.name); private readonly logger = new Logger(QueryController.name);

View File

@@ -1,5 +1,5 @@
import { NestFactory } from "@nestjs/core"; import { NestFactory } from "@nestjs/core";
import { ValidationPipe } from "@nestjs/common"; import { RequestMethod, ValidationPipe } from "@nestjs/common";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import { AppModule } from "./app.module"; import { AppModule } from "./app.module";
import { getTrustedOrigins } from "./auth/auth.config"; import { getTrustedOrigins } from "./auth/auth.config";
@@ -47,6 +47,16 @@ async function bootstrap() {
app.useGlobalFilters(new GlobalExceptionFilter()); app.useGlobalFilters(new GlobalExceptionFilter());
// Set global API prefix — all routes get /api/* except auth and health
// Auth routes are excluded because BetterAuth expects /auth/* paths
// Health is excluded because Docker healthchecks hit /health directly
app.setGlobalPrefix("api", {
exclude: [
{ path: "health", method: RequestMethod.GET },
{ path: "auth/(.*)", method: RequestMethod.ALL },
],
});
// Configure CORS for cookie-based authentication // Configure CORS for cookie-based authentication
// Origin list is shared with BetterAuth trustedOrigins via getTrustedOrigins() // Origin list is shared with BetterAuth trustedOrigins via getTrustedOrigins()
const trustedOrigins = getTrustedOrigins(); const trustedOrigins = getTrustedOrigins();