diff --git a/client/.env.example b/client/.env.example new file mode 100644 index 0000000..8360ce8 --- /dev/null +++ b/client/.env.example @@ -0,0 +1,3 @@ +# Frontend API base URL. +# Example for local Docker server: backend container is exposed on localhost:13000. +VITE_API_URL=http://localhost:13000/api/v1 diff --git a/client/src/api/client.ts b/client/src/api/client.ts index b0de9f1..9cfa713 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -13,9 +13,9 @@ import type { } from './types'; import { emitApiErrorToast } from '../lib/toast-events'; -// In dev, Vite proxies /environments /sessions /scenarios to localhost:3000. -// In production (or when VITE_API_URL is set) we hit the configured origin directly. -const BASE_URL: string = (import.meta.env.VITE_API_URL as string | undefined) ?? ''; +// API calls are versioned under /api/v1 by default. +// Set VITE_API_URL (for example, http://localhost:13000/api/v1) to use a different origin. +const BASE_URL = ((import.meta.env.VITE_API_URL as string | undefined) ?? '/api/v1').replace(/\/$/, ''); async function request(path: string, init?: RequestInit): Promise { let res: Response; diff --git a/client/src/main.tsx b/client/src/main.tsx index 36128a4..cef3f98 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,6 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import { HashRouter } from 'react-router-dom'; +import { BrowserRouter } from 'react-router-dom'; import './i18n'; import './ui/tokens.css'; import App from './App'; @@ -9,9 +9,9 @@ import { ToastProvider } from './ui'; createRoot(document.getElementById('root')!).render( - + - + , ); diff --git a/client/vite.config.ts b/client/vite.config.ts index 4da88c7..0bffac6 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -68,15 +68,6 @@ export default defineConfig({ }, }, }, - server: { - proxy: { - '/environments': 'http://localhost:13000', - '/credentials': 'http://localhost:13000', - '/snippets': 'http://localhost:13000', - '/sessions': 'http://localhost:13000', - '/scenarios': 'http://localhost:13000', - }, - }, test: { projects: [{ extends: true, diff --git a/server/src/config/app.config.ts b/server/src/config/app.config.ts index a09dee5..37ed288 100644 --- a/server/src/config/app.config.ts +++ b/server/src/config/app.config.ts @@ -23,7 +23,7 @@ export class AppConfig { KEYS_DIR: string = "keys"; @IsString() - DB_PATH: string = "data/sessions.db"; + DB_PATH: string = "data/liqa.db"; @IsInt() @Min(1) diff --git a/server/src/health/health.controller.ts b/server/src/health/health.controller.ts index e3eb9c6..f3f9700 100644 --- a/server/src/health/health.controller.ts +++ b/server/src/health/health.controller.ts @@ -4,6 +4,13 @@ import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; @ApiTags("health") @Controller() export class HealthController { + @Get("health") + @ApiOperation({ summary: "Health check" }) + @ApiResponse({ status: 200, description: "Service is healthy" }) + health(): { status: string } { + return { status: "ok" }; + } + @Get("healthz") @ApiOperation({ summary: "Health check" }) @ApiResponse({ status: 200, description: "Service is healthy" }) diff --git a/server/src/main.ts b/server/src/main.ts index dacb35e..9e7559b 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,7 +1,7 @@ import { NestFactory } from "@nestjs/core"; import { ConfigService } from "@nestjs/config"; import { AppConfig } from "./config/app.config"; -import { ValidationPipe } from "@nestjs/common"; +import { RequestMethod, ValidationPipe } from "@nestjs/common"; import { TraceLogger } from "./common/trace-logger"; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; import { AppModule } from "./app.module"; @@ -11,11 +11,20 @@ import { LoggingInterceptor } from "./interceptors/logging.interceptor"; import { name as pkgName, version as pkgVersion } from "../package.json"; async function bootstrap() { + const API_PREFIX = "api/v1"; const logger = new TraceLogger("Bootstrap"); const app = await NestFactory.create(AppModule, { logger: new TraceLogger("Bootstrap", { timestamp: true }), }); + app.setGlobalPrefix(API_PREFIX, { + exclude: [ + { path: "health", method: RequestMethod.ALL }, + { path: "healthz", method: RequestMethod.ALL }, + { path: "mcp", method: RequestMethod.ALL }, + ], + }); + app.useGlobalPipes(new ValidationPipe({ transform: true })); app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalInterceptors(new TraceInterceptor(), new LoggingInterceptor()); @@ -24,6 +33,15 @@ async function bootstrap() { const port = config.get("PORT"); const nodeEnv = config.get("NODE_ENV"); + app.enableCors({ + origin: [ + "http://localhost:5173", + "http://127.0.0.1:5173", + ], + methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"], + credentials: true, + }); + const swaggerConfig = new DocumentBuilder() .setTitle(pkgName) .setDescription(`${pkgName} API`) @@ -31,14 +49,14 @@ async function bootstrap() { .build(); const document = SwaggerModule.createDocument(app, swaggerConfig); - SwaggerModule.setup("api", app, document); + SwaggerModule.setup("docs", app, document); await app.listen(port); logger.log( `Application "${pkgName}" v${pkgVersion} running on port ${port} [${nodeEnv}]`, ); - logger.log(`Swagger UI available at http://localhost:${port}/api`); + logger.log(`Swagger UI available at http://localhost:${port}/docs`); logger.log(`MCP endpoint available at http://localhost:${port}/mcp`); }