From 15139d0070bef3f054827351db9cb24b4f6adf60 Mon Sep 17 00:00:00 2001 From: rahull-prog <241771050+kydrahul@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:54:11 +0530 Subject: [PATCH 1/5] feat: Add OpenAPI/Swagger documentation --- backend/package.json | 4 + backend/scripts/generate-types.ts | 72 ++++ backend/src/__tests__/swagger.test.ts | 237 ++++++++++++ backend/src/config/swagger.ts | 405 +++++++++++++++++++++ backend/src/index.ts | 214 ++++++++--- backend/src/middleware/security.ts | 56 ++- backend/src/middleware/swaggerValidator.ts | 209 +++++++++++ backend/src/routes/accounts.ts | 201 ++++++++++ backend/src/routes/auth.ts | 112 ++++++ backend/src/routes/health.ts | 88 ++++- backend/src/swagger/README.md | 23 ++ backend/src/swagger/schemas.yaml | 216 +++++++++++ issue1.md | 65 ++++ pnpm-lock.yaml | 236 +++++++++++- 14 files changed, 2059 insertions(+), 79 deletions(-) create mode 100644 backend/scripts/generate-types.ts create mode 100644 backend/src/__tests__/swagger.test.ts create mode 100644 backend/src/config/swagger.ts create mode 100644 backend/src/middleware/swaggerValidator.ts create mode 100644 backend/src/swagger/README.md create mode 100644 backend/src/swagger/schemas.yaml create mode 100644 issue1.md diff --git a/backend/package.json b/backend/package.json index da9eca1..aed4d32 100644 --- a/backend/package.json +++ b/backend/package.json @@ -36,6 +36,8 @@ "reflect-metadata": "^0.2.2", "sql.js": "^1.14.1", "sqlite3": "^5.1.7", + "swagger-jsdoc": "^6.3.0", + "swagger-ui-express": "^5.0.1", "uuid": "^9.0.1", "winston": "^3.14.2", "zod": "^3.22.0" @@ -49,6 +51,8 @@ "@types/jest": "^29.0.0", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20.0.0", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", diff --git a/backend/scripts/generate-types.ts b/backend/scripts/generate-types.ts new file mode 100644 index 0000000..135ab79 --- /dev/null +++ b/backend/scripts/generate-types.ts @@ -0,0 +1,72 @@ +/** + * Generate TypeScript types from the OpenAPI specification. + * + * Usage: + * npx ts-node scripts/generate-types.ts + * # or via npm script: + * pnpm --filter @chenaikit/backend run generate:api-types + * + * Output: + * src/types/generated-api.ts + */ + +import fs from "fs"; +import path from "path"; + +async function main() { + // Dynamically import the swagger spec (it runs swagger-jsdoc at import time) + const { swaggerSpec } = await import("../src/config/swagger"); + + const specJson = JSON.stringify(swaggerSpec, null, 2); + const outputDir = path.resolve(__dirname, "..", "src", "types"); + const specOutputPath = path.resolve(outputDir, "openapi-spec.json"); + + // Ensure output directory exists + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Write the resolved OpenAPI spec as JSON + fs.writeFileSync(specOutputPath, specJson, "utf-8"); + console.log(`✅ OpenAPI spec written to ${specOutputPath}`); + + // Generate TypeScript types using openapi-typescript (if installed) + try { + const openapiTS = await import("openapi-typescript"); + const generateFn = openapiTS.default || openapiTS; + + if (typeof generateFn === "function") { + const output = await generateFn(swaggerSpec as any); + const tsOutputPath = path.resolve(outputDir, "generated-api.ts"); + const content = typeof output === "string" ? output : String(output); + fs.writeFileSync(tsOutputPath, content, "utf-8"); + console.log(`✅ TypeScript types written to ${tsOutputPath}`); + } else { + console.log( + "ℹ️ openapi-typescript loaded but generate function not found.", + ); + console.log( + " The OpenAPI spec JSON has been saved — you can generate types manually with:", + ); + console.log( + ` npx openapi-typescript ${specOutputPath} -o ${path.resolve(outputDir, "generated-api.ts")}`, + ); + } + } catch { + console.log( + "ℹ️ openapi-typescript not installed. Skipping TypeScript type generation.", + ); + console.log( + " Install it with: pnpm add -D openapi-typescript --filter @chenaikit/backend", + ); + console.log(" Then re-run this script, or generate types manually with:"); + console.log( + ` npx openapi-typescript ${specOutputPath} -o ${path.resolve(outputDir, "generated-api.ts")}`, + ); + } +} + +main().catch((err) => { + console.error("Failed to generate types:", err); + process.exit(1); +}); diff --git a/backend/src/__tests__/swagger.test.ts b/backend/src/__tests__/swagger.test.ts new file mode 100644 index 0000000..5646170 --- /dev/null +++ b/backend/src/__tests__/swagger.test.ts @@ -0,0 +1,237 @@ +/** + * Unit tests for the Swagger / OpenAPI configuration. + * + * Validates that: + * - The generated OpenAPI spec is well-formed + * - All expected endpoints are documented + * - Component schemas are defined + * - Security schemes are configured + * - Swagger UI options are set correctly + */ + +import { swaggerSpec, swaggerUiOptions } from "../config/swagger"; + +describe("Swagger Configuration", () => { + describe("swaggerSpec", () => { + it("should produce a valid OpenAPI 3.x specification", () => { + expect(swaggerSpec).toBeDefined(); + expect(swaggerSpec).toHaveProperty("openapi"); + expect((swaggerSpec as any).openapi).toMatch(/^3\.\d+\.\d+$/); + }); + + it("should include API info metadata", () => { + const info = (swaggerSpec as any).info; + expect(info).toBeDefined(); + expect(info.title).toBe("ChenAIKit API"); + expect(info.version).toBe("0.1.0"); + expect(info.description).toBeTruthy(); + expect(info.contact).toBeDefined(); + expect(info.license).toBeDefined(); + }); + + it("should define at least one server", () => { + const servers = (swaggerSpec as any).servers; + expect(servers).toBeDefined(); + expect(Array.isArray(servers)).toBe(true); + expect(servers.length).toBeGreaterThanOrEqual(1); + expect(servers[0]).toHaveProperty("url"); + expect(servers[0]).toHaveProperty("description"); + }); + + it("should define all expected tags", () => { + const tags = (swaggerSpec as any).tags; + expect(tags).toBeDefined(); + const tagNames = tags.map((t: any) => t.name); + expect(tagNames).toContain("Health"); + expect(tagNames).toContain("Auth"); + expect(tagNames).toContain("Accounts"); + expect(tagNames).toContain("Credit Scoring"); + expect(tagNames).toContain("Fraud Detection"); + expect(tagNames).toContain("Metrics"); + }); + }); + + describe("Security Schemes", () => { + it("should define bearerAuth (JWT) security scheme", () => { + const schemes = (swaggerSpec as any).components?.securitySchemes; + expect(schemes).toBeDefined(); + expect(schemes.bearerAuth).toBeDefined(); + expect(schemes.bearerAuth.type).toBe("http"); + expect(schemes.bearerAuth.scheme).toBe("bearer"); + expect(schemes.bearerAuth.bearerFormat).toBe("JWT"); + }); + + it("should define apiKeyAuth security scheme", () => { + const schemes = (swaggerSpec as any).components?.securitySchemes; + expect(schemes.apiKeyAuth).toBeDefined(); + expect(schemes.apiKeyAuth.type).toBe("apiKey"); + expect(schemes.apiKeyAuth.in).toBe("header"); + expect(schemes.apiKeyAuth.name).toBe("X-API-Key"); + }); + }); + + describe("Component Schemas", () => { + const expectedSchemas = [ + "ApiError", + "ValidationError", + "RegisterRequest", + "LoginRequest", + "RefreshRequest", + "AuthTokenResponse", + "Account", + "AccountCreationRequest", + "Transaction", + "PaginatedTransactions", + "CreditScoreResponse", + "FraudDetectionResponse", + "HealthCheckResult", + ]; + + it.each(expectedSchemas)("should define the %s schema", (schemaName) => { + const schemas = (swaggerSpec as any).components?.schemas; + expect(schemas).toBeDefined(); + expect(schemas[schemaName]).toBeDefined(); + expect(schemas[schemaName].type).toBe("object"); + }); + }); + + describe("API Paths", () => { + const expectedPaths = [ + "/api/health", + "/api/health/liveness", + "/api/health/readiness", + "/api/auth/register", + "/api/auth/login", + "/api/auth/refresh", + "/api/accounts/{id}", + "/api/accounts/{id}/balance", + "/api/accounts/{id}/transactions", + "/api/accounts", + "/api/v1/credit-score", + "/api/v1/fraud/detect", + "/metrics", + ]; + + it("should document all expected API endpoints", () => { + const paths = (swaggerSpec as any).paths; + expect(paths).toBeDefined(); + + for (const path of expectedPaths) { + expect(paths).toHaveProperty(path); + } + }); + + it("should specify HTTP methods for each path", () => { + const paths = (swaggerSpec as any).paths; + + // Health endpoints should be GET + expect(paths["/api/health"]).toHaveProperty("get"); + expect(paths["/api/health/liveness"]).toHaveProperty("get"); + expect(paths["/api/health/readiness"]).toHaveProperty("get"); + + // Auth endpoints should be POST + expect(paths["/api/auth/register"]).toHaveProperty("post"); + expect(paths["/api/auth/login"]).toHaveProperty("post"); + expect(paths["/api/auth/refresh"]).toHaveProperty("post"); + + // Account GET endpoints + expect(paths["/api/accounts/{id}"]).toHaveProperty("get"); + expect(paths["/api/accounts/{id}/balance"]).toHaveProperty("get"); + expect(paths["/api/accounts/{id}/transactions"]).toHaveProperty("get"); + + // Account create should be POST + expect(paths["/api/accounts"]).toHaveProperty("post"); + + // AI endpoints should be GET + expect(paths["/api/v1/credit-score"]).toHaveProperty("get"); + expect(paths["/api/v1/fraud/detect"]).toHaveProperty("get"); + }); + + it("should include summaries and tags for each endpoint", () => { + const paths = (swaggerSpec as any).paths; + + for (const path of expectedPaths) { + const methods = paths[path]; + for (const method of Object.keys(methods)) { + const operation = methods[method]; + expect(operation.summary).toBeTruthy(); + expect(operation.tags).toBeDefined(); + expect(operation.tags.length).toBeGreaterThan(0); + } + } + }); + + it("should include response definitions for each endpoint", () => { + const paths = (swaggerSpec as any).paths; + + for (const path of expectedPaths) { + const methods = paths[path]; + for (const method of Object.keys(methods)) { + const operation = methods[method]; + expect(operation.responses).toBeDefined(); + // Every endpoint should have at least a 200-level response + const responseCodes = Object.keys(operation.responses); + const hasSuccessResponse = responseCodes.some( + (code) => parseInt(code) >= 200 && parseInt(code) < 300, + ); + expect(hasSuccessResponse).toBe(true); + } + } + }); + + it("should include security definitions on protected endpoints", () => { + const paths = (swaggerSpec as any).paths; + const protectedPaths = ["/api/v1/credit-score", "/api/v1/fraud/detect"]; + + for (const path of protectedPaths) { + const operation = paths[path]?.get; + expect(operation?.security).toBeDefined(); + expect(operation.security.length).toBeGreaterThan(0); + } + }); + + it("should include request body schemas for POST endpoints", () => { + const paths = (swaggerSpec as any).paths; + const postPaths = [ + "/api/auth/register", + "/api/auth/login", + "/api/auth/refresh", + "/api/accounts", + ]; + + for (const path of postPaths) { + const operation = paths[path]?.post; + expect(operation?.requestBody).toBeDefined(); + expect(operation.requestBody.required).toBe(true); + expect(operation.requestBody.content).toHaveProperty( + "application/json", + ); + } + }); + }); + + describe("swaggerUiOptions", () => { + it("should set a custom site title", () => { + expect(swaggerUiOptions.customSiteTitle).toBe( + "ChenAIKit API Documentation", + ); + }); + + it("should include custom CSS to hide the top bar", () => { + expect(swaggerUiOptions.customCss).toContain(".swagger-ui .topbar"); + expect(swaggerUiOptions.customCss).toContain("display: none"); + }); + + it("should enable persistent authorization", () => { + expect(swaggerUiOptions.swaggerOptions?.persistAuthorization).toBe(true); + }); + + it("should configure documentation expansion as list", () => { + expect(swaggerUiOptions.swaggerOptions?.docExpansion).toBe("list"); + }); + + it("should enable endpoint filtering", () => { + expect(swaggerUiOptions.swaggerOptions?.filter).toBe(true); + }); + }); +}); diff --git a/backend/src/config/swagger.ts b/backend/src/config/swagger.ts new file mode 100644 index 0000000..7044135 --- /dev/null +++ b/backend/src/config/swagger.ts @@ -0,0 +1,405 @@ +/** + * @module config/swagger + * @description OpenAPI 3.0.3 specification and Swagger UI configuration for the ChenAIKit backend. + * Uses swagger-jsdoc to merge this base definition with @openapi JSDoc annotations in route files. + */ +import swaggerJsdoc from "swagger-jsdoc"; +import { SwaggerUiOptions } from "swagger-ui-express"; + +const swaggerDefinition: swaggerJsdoc.OAS3Definition = { + openapi: "3.0.3", + info: { + title: "ChenAIKit API", + version: "0.1.0", + description: + "ChenAIKit Backend API — AI-powered financial toolkit providing credit scoring, fraud detection, account management, and analytics services.", + contact: { + name: "ChenAIKit Team", + url: "https://github.com/nexoraorg/chenaikit", + }, + license: { + name: "MIT", + url: "https://opensource.org/licenses/MIT", + }, + }, + servers: [ + { + url: `http://localhost:${process.env.PORT || 5000}`, + description: "Local development server", + }, + ...(process.env.API_BASE_URL + ? [{ url: process.env.API_BASE_URL, description: "Production server" }] + : []), + ], + tags: [ + { name: "Health", description: "Health check and readiness endpoints" }, + { name: "Auth", description: "Authentication and authorisation" }, + { name: "Accounts", description: "Account management and transactions" }, + { name: "Credit Scoring", description: "AI credit scoring services" }, + { name: "Fraud Detection", description: "AI fraud detection services" }, + { name: "Analytics", description: "Dashboard analytics and reporting" }, + { name: "Metrics", description: "Prometheus metrics" }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + description: "JWT access token obtained from POST /api/auth/login", + }, + apiKeyAuth: { + type: "apiKey", + in: "header", + name: "X-API-Key", + description: "API key for gateway-protected endpoints", + }, + }, + schemas: { + // ── Error schemas ────────────────────────────────────────────── + ApiError: { + type: "object", + properties: { + success: { type: "boolean", example: false }, + error: { + type: "object", + properties: { + code: { type: "string", example: "INTERNAL_SERVER_ERROR" }, + message: { + type: "string", + example: "An unexpected error occurred", + }, + details: { + type: "array", + items: { $ref: "#/components/schemas/ValidationError" }, + }, + timestamp: { type: "string", format: "date-time" }, + }, + }, + }, + }, + ValidationError: { + type: "object", + properties: { + field: { type: "string", example: "email" }, + message: { type: "string", example: "Invalid email format" }, + }, + }, + + // ── Auth schemas ─────────────────────────────────────────────── + RegisterRequest: { + type: "object", + required: ["email", "password"], + properties: { + email: { + type: "string", + format: "email", + example: "user@example.com", + }, + password: { type: "string", minLength: 8, example: "securePass123" }, + role: { type: "string", enum: ["user", "admin"], default: "user" }, + }, + }, + LoginRequest: { + type: "object", + required: ["email", "password"], + properties: { + email: { + type: "string", + format: "email", + example: "user@example.com", + }, + password: { type: "string", minLength: 8, example: "securePass123" }, + }, + }, + RefreshRequest: { + type: "object", + required: ["token"], + properties: { + token: { type: "string", example: "1.abc123refreshtoken..." }, + }, + }, + AuthTokenResponse: { + type: "object", + properties: { + accessToken: { type: "string", example: "eyJhbGciOiJIUzI1NiIs..." }, + refreshToken: { type: "string", example: "a1b2c3d4e5f6..." }, + }, + }, + + // ── Account schemas ──────────────────────────────────────────── + Account: { + type: "object", + properties: { + id: { + type: "string", + example: "GCKFBEIYTKP6RJKJJGZ7LX3WZ7XMZS2NKTPGJ2DQVHZ4DFJ6WNRPJCPK", + }, + name: { type: "string", example: "John Doe" }, + email: { + type: "string", + format: "email", + example: "john.doe@example.com", + }, + publicKey: { + type: "string", + example: "GCKFBEIYTKP6RJKJJGZ7LX3WZ7XMZS2NKTPGJ2DQVHZ4DFJ6WNRPJCPK", + }, + balance: { type: "number", format: "double", example: 1000.5 }, + createdAt: { type: "string", format: "date-time" }, + updatedAt: { type: "string", format: "date-time" }, + isActive: { type: "boolean", example: true }, + }, + }, + AccountCreationRequest: { + type: "object", + required: ["name", "email", "publicKey"], + properties: { + name: { type: "string", example: "Jane Doe" }, + email: { + type: "string", + format: "email", + example: "jane.doe@example.com", + }, + publicKey: { + type: "string", + example: "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRST", + }, + }, + }, + Transaction: { + type: "object", + properties: { + id: { type: "string", example: "tx_001" }, + accountId: { type: "string" }, + amount: { type: "number", format: "double", example: 500.0 }, + type: { type: "string", enum: ["credit", "debit"] }, + description: { type: "string", example: "Initial deposit" }, + timestamp: { type: "string", format: "date-time" }, + fromAccount: { type: "string", nullable: true }, + toAccount: { type: "string", nullable: true }, + status: { type: "string", enum: ["pending", "completed", "failed"] }, + }, + }, + PaginatedTransactions: { + type: "object", + properties: { + success: { type: "boolean", example: true }, + data: { + type: "object", + properties: { + data: { + type: "array", + items: { $ref: "#/components/schemas/Transaction" }, + }, + pagination: { + type: "object", + properties: { + page: { type: "integer", example: 1 }, + limit: { type: "integer", example: 10 }, + total: { type: "integer", example: 3 }, + pages: { type: "integer", example: 1 }, + hasNext: { type: "boolean", example: false }, + hasPrev: { type: "boolean", example: false }, + }, + }, + }, + }, + timestamp: { type: "string", format: "date-time" }, + }, + }, + + // ── AI / Gateway schemas ─────────────────────────────────────── + CreditScoreResponse: { + type: "object", + properties: { + success: { type: "boolean", example: true }, + data: { + type: "object", + properties: { + score: { + type: "integer", + example: 750, + minimum: 150, + maximum: 850, + }, + factors: { + type: "array", + items: { type: "string" }, + example: [ + "payment_history", + "credit_utilization", + "account_age", + ], + }, + timestamp: { type: "string", format: "date-time" }, + }, + }, + }, + }, + FraudDetectionResponse: { + type: "object", + properties: { + success: { type: "boolean", example: true }, + data: { + type: "object", + properties: { + riskScore: { + type: "integer", + example: 15, + minimum: 0, + maximum: 100, + }, + riskLevel: { + type: "string", + enum: ["low", "medium", "high"], + example: "low", + }, + factors: { + type: "array", + items: { type: "string" }, + example: ["transaction_amount", "location", "device"], + }, + timestamp: { type: "string", format: "date-time" }, + }, + }, + }, + }, + + // ── Health schemas ───────────────────────────────────────────── + HealthCheckResult: { + type: "object", + properties: { + status: { + type: "string", + enum: ["healthy", "degraded", "unhealthy"], + }, + timestamp: { type: "string", format: "date-time" }, + uptime: { type: "number", description: "Server uptime in seconds" }, + checks: { + type: "object", + additionalProperties: { + type: "object", + properties: { + status: { type: "string", enum: ["up", "down", "degraded"] }, + responseTime: { type: "number" }, + error: { type: "string" }, + details: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, +}; + +const options: swaggerJsdoc.OAS3Options = { + definition: swaggerDefinition, + // Scan route files and index.ts for @openapi annotations + apis: ["./src/routes/*.ts", "./src/index.ts"], +}; + +/** + * Resolved OpenAPI specification object. + * Merges the base definition above with @openapi JSDoc annotations found in route files. + * Consumed by swagger-ui-express to render the interactive API explorer. + */ +export const swaggerSpec = swaggerJsdoc(options); + +/** + * Swagger UI display options. + * Customises the look-and-feel of the /api-docs page (hidden top bar, persistent auth, etc.). + */ +export const swaggerUiOptions: SwaggerUiOptions = { + customSiteTitle: "ChenAIKit API Documentation", + customCss: ` + html, body, #swagger-ui { + margin: 0 !important; + padding: 0 !important; + } + .swagger-ui .topbar { + position: absolute !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + width: 100% !important; + margin: 0 !important; + z-index: 9999 !important; + } + .swagger-ui { + padding-top: 60px !important; + } + `, + swaggerOptions: { + persistAuthorization: true, + docExpansion: "list", + filter: true, + tagsSorter: "alpha", + operationsSorter: "method", + }, +}; + +/** + * Converts a JSON object to a simplified YAML string representation. + * Used to serve the OpenAPI spec at /api-docs.yaml without requiring a YAML library. + * + * @param obj - The JSON object to convert + * @param indent - Current indentation level (used internally for recursion) + * @returns A YAML-formatted string + */ +export function jsonToYaml(obj: unknown, indent: number = 0): string { + const pad = " ".repeat(indent); + + if (obj === null || obj === undefined) { + return "null"; + } + + if (typeof obj === "string") { + // Quote strings that contain special YAML characters + if ( + /[:{}[\],&*?|>!%@`#'"\n]/.test(obj) || + obj === "" || + obj === "true" || + obj === "false" || + obj === "null" || + !isNaN(Number(obj)) + ) { + return JSON.stringify(obj); + } + return obj; + } + + if (typeof obj === "number" || typeof obj === "boolean") { + return String(obj); + } + + if (Array.isArray(obj)) { + if (obj.length === 0) return "[]"; + return obj + .map((item) => { + const value = jsonToYaml(item, indent + 1); + if (typeof item === "object" && item !== null) { + return `${pad}- ${value.trimStart()}`; + } + return `${pad}- ${value}`; + }) + .join("\n"); + } + + if (typeof obj === "object") { + const entries = Object.entries(obj as Record); + if (entries.length === 0) return "{}"; + return entries + .map(([key, value]) => { + if (typeof value === "object" && value !== null) { + const nested = jsonToYaml(value, indent + 1); + return `${pad}${key}:\n${nested}`; + } + return `${pad}${key}: ${jsonToYaml(value, indent + 1)}`; + }) + .join("\n"); + } + + return String(obj); +} diff --git a/backend/src/index.ts b/backend/src/index.ts index 4adaf2b..9e31519 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,97 +1,196 @@ // ChenAIKit Backend Server -import 'reflect-metadata'; -import express, { Request, Response } from 'express'; -import dotenv from 'dotenv'; -import { log } from './utils/logger'; -import { requestLoggingMiddleware } from './middleware/logging'; -import healthRouter from './routes/health'; -import { metricsService, metricsMiddleware } from './services/metricsService'; -import { validateEnvironment, initializeMonitoring, shutdownMonitoring } from './config/monitoring'; -import authRoutes from './routes/auth'; -import { UserPayload } from './types/auth'; -import { ensureRedisConnection } from './config/redis'; -import accountRoutes from './routes/accounts'; -import { PrismaClient } from '@prisma/client'; -import { ApiKeyService } from './services/apiKeyService'; -import { UsageTrackingService } from './services/usageTrackingService'; -import { ApiGateway } from './middleware/apiGateway'; -import { createTieredRateLimiter } from './middleware/advancedRateLimiter'; -import Redis from 'ioredis'; -import { applySecurityMiddleware } from './middleware/security'; -import { loadVaultSecrets } from './config/secrets'; +import "reflect-metadata"; +import express, { Request, Response } from "express"; +import dotenv from "dotenv"; +import { log } from "./utils/logger"; +import { requestLoggingMiddleware } from "./middleware/logging"; +import healthRouter from "./routes/health"; +import { metricsService, metricsMiddleware } from "./services/metricsService"; +import { + validateEnvironment, + initializeMonitoring, + shutdownMonitoring, +} from "./config/monitoring"; +import authRoutes from "./routes/auth"; +import { UserPayload } from "./types/auth"; +import { ensureRedisConnection } from "./config/redis"; +import accountRoutes from "./routes/accounts"; +import { PrismaClient } from "@prisma/client"; +import { ApiKeyService } from "./services/apiKeyService"; +import { UsageTrackingService } from "./services/usageTrackingService"; +import { ApiGateway } from "./middleware/apiGateway"; +import { createTieredRateLimiter } from "./middleware/advancedRateLimiter"; +import Redis from "ioredis"; +import { applySecurityMiddleware } from "./middleware/security"; +import { loadVaultSecrets } from "./config/secrets"; +import swaggerUi from "swagger-ui-express"; +import { swaggerSpec, swaggerUiOptions, jsonToYaml } from "./config/swagger"; +import { validateRequestAgainstSpec } from "./middleware/swaggerValidator"; const app: express.Application = express(); applySecurityMiddleware(app); -app.use(express.json({ limit: '10mb' })); +app.use(express.json({ limit: "10mb" })); app.use(metricsMiddleware); app.use(requestLoggingMiddleware); -app.use('/api', healthRouter); -app.use('/api/auth', authRoutes); -app.use('/api/accounts', accountRoutes); + +// Swagger UI - interactive API documentation +app.use( + "/api-docs", + swaggerUi.serve, + swaggerUi.setup(swaggerSpec, swaggerUiOptions), +); + +// Raw OpenAPI spec endpoint (JSON) +app.get("/api-docs.json", (_req: Request, res: Response) => { + res.setHeader("Content-Type", "application/json"); + res.send(swaggerSpec); +}); + +// Raw OpenAPI spec endpoint (YAML) +app.get("/api-docs.yaml", (_req: Request, res: Response) => { + // Convert JSON spec to YAML format + const yaml = jsonToYaml(swaggerSpec); + res.setHeader("Content-Type", "text/yaml"); + res.send(yaml); +}); + +// Validate request bodies against OpenAPI spec +app.use(validateRequestAgainstSpec); + +app.use("/api", healthRouter); +app.use("/api/auth", authRoutes); +app.use("/api/accounts", accountRoutes); // app.use('/api/v1/analytics', createAnalyticsRouter(prisma, typeorm)); // Gateway-protected endpoints -app.get('/api/v1/credit-score', (_req: Request, res: Response) => { +/** + * @openapi + * /api/v1/credit-score: + * get: + * summary: Get credit score + * description: Returns an AI-generated credit score with contributing factors. Gateway-protected endpoint. + * tags: [Credit Scoring] + * security: + * - apiKeyAuth: [] + * - bearerAuth: [] + * responses: + * 200: + * description: Credit score computed successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CreditScoreResponse' + * 401: + * description: Authentication required + * 403: + * description: Insufficient permissions + */ +app.get("/api/v1/credit-score", (_req: Request, res: Response) => { res.json({ success: true, data: { score: Math.floor(Math.random() * 850) + 150, - factors: ['payment_history', 'credit_utilization', 'account_age'], + factors: ["payment_history", "credit_utilization", "account_age"], timestamp: new Date().toISOString(), }, }); }); -app.get('/api/v1/fraud/detect', (_req: Request, res: Response) => { +/** + * @openapi + * /api/v1/fraud/detect: + * get: + * summary: Detect fraud risk + * description: Returns a fraud risk assessment with risk score, level, and contributing factors. Gateway-protected endpoint. + * tags: [Fraud Detection] + * security: + * - apiKeyAuth: [] + * - bearerAuth: [] + * responses: + * 200: + * description: Fraud detection result + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/FraudDetectionResponse' + * 401: + * description: Authentication required + * 403: + * description: Insufficient permissions + */ +app.get("/api/v1/fraud/detect", (_req: Request, res: Response) => { res.json({ success: true, data: { riskScore: Math.floor(Math.random() * 100), - riskLevel: 'low', - factors: ['transaction_amount', 'location', 'device'], + riskLevel: "low", + factors: ["transaction_amount", "location", "device"], timestamp: new Date().toISOString(), }, }); }); -// Prometheus metrics endpoint -app.get('/metrics', async (_req: Request, res: Response) => { +/** + * @openapi + * /metrics: + * get: + * summary: Prometheus metrics + * description: Exposes application metrics in Prometheus text format for scraping. + * tags: [Metrics] + * responses: + * 200: + * description: Prometheus-formatted metrics + * content: + * text/plain: + * schema: + * type: string + * 500: + * description: Failed to collect metrics + */ +app.get("/metrics", async (_req: Request, res: Response) => { try { const metrics = await metricsService.getMetrics(); - res.set('Content-Type', 'text/plain'); + res.set("Content-Type", "text/plain"); res.send(metrics); } catch (e: unknown) { const error = e as Error; - res.status(500).send(error?.message || 'metrics error'); + res.status(500).send(error?.message || "metrics error"); } }); // 404 handler -app.use('*', (req: Request, res: Response) => { +app.use("*", (req: Request, res: Response) => { res.status(404).json({ success: false, error: { - code: 'ENDPOINT_NOT_FOUND', + code: "ENDPOINT_NOT_FOUND", message: `Endpoint ${req.method} ${req.originalUrl} not found`, - timestamp: new Date().toISOString() - } + timestamp: new Date().toISOString(), + }, }); }); // Global error handler -app.use((error: Error, req: express.Request, res: express.Response, _next: express.NextFunction) => { - console.error('Unhandled error:', error); +app.use( + ( + error: Error, + req: express.Request, + res: express.Response, + _next: express.NextFunction, + ) => { + console.error("Unhandled error:", error); - res.status(500).json({ - success: false, - error: { - code: 'INTERNAL_SERVER_ERROR', - message: 'An unexpected error occurred', - timestamp: new Date().toISOString() - } - }); -}); + res.status(500).json({ + success: false, + error: { + code: "INTERNAL_SERVER_ERROR", + message: "An unexpected error occurred", + timestamp: new Date().toISOString(), + }, + }); + }, +); export const startServer = async (): Promise => { // Load environment variables @@ -104,11 +203,15 @@ export const startServer = async (): Promise => { validateEnvironment(); const prisma = new PrismaClient(); - const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379'); + const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379"); const apiKeyService = new ApiKeyService(prisma); const usageTrackingService = new UsageTrackingService(prisma); const rateLimiter = createTieredRateLimiter(redis); - const apiGateway = new ApiGateway(apiKeyService, usageTrackingService, rateLimiter); + const apiGateway = new ApiGateway( + apiKeyService, + usageTrackingService, + rateLimiter, + ); // registerGatewayRoutes(apiGateway, apiKeyService, usageTrackingService); @@ -126,27 +229,28 @@ export const startServer = async (): Promise => { } }; - process.on('SIGINT', shutdown); - process.on('SIGTERM', shutdown); + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); const server = app.listen(PORT, async () => { console.log(`🚀 ChenAIKit Backend running on port ${PORT}`); console.log(`📊 Health check: http://localhost:${PORT}/api/health`); console.log(`📈 Metrics: http://localhost:${PORT}/metrics`); + console.log(`📖 API Docs: http://localhost:${PORT}/api-docs`); console.log(`📋 See .github/ISSUE_TEMPLATE/ for backend development tasks`); try { await ensureRedisConnection(); - console.log('🧠 Redis cache ready'); + console.log("🧠 Redis cache ready"); } catch (_err) { - console.warn('⚠️ Redis not available. Continuing without cache.'); + console.warn("⚠️ Redis not available. Continuing without cache."); } }); }; if (require.main === module) { startServer().catch((error) => { - console.error('Failed to start server', error); + console.error("Failed to start server", error); process.exit(1); }); } diff --git a/backend/src/middleware/security.ts b/backend/src/middleware/security.ts index d3d2533..1fb7d8a 100644 --- a/backend/src/middleware/security.ts +++ b/backend/src/middleware/security.ts @@ -1,45 +1,48 @@ -import type { Application, RequestHandler } from 'express'; -import cors from 'cors'; -import helmet from 'helmet'; +import type { Application, RequestHandler } from "express"; +import cors from "cors"; +import helmet from "helmet"; const parseOrigins = (origins: string | undefined): string[] => { if (!origins) return []; return origins - .split(',') - .map(o => o.trim()) + .split(",") + .map((o) => o.trim()) .filter(Boolean); }; const corsMiddleware = (): RequestHandler => { const origins = parseOrigins(process.env.CORS_ORIGINS); const allowAll = - process.env.CORS_ALLOW_ALL === 'true' && - (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'); + process.env.CORS_ALLOW_ALL === "true" && + (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test"); return cors({ - origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => { + origin: ( + origin: string | undefined, + callback: (err: Error | null, allow?: boolean) => void, + ) => { if (allowAll) return callback(null, true); if (!origin) return callback(null, true); if (origins.length === 0) return callback(null, false); if (origins.includes(origin)) return callback(null, true); return callback(null, false); }, - methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Authorization', 'Content-Type', 'X-API-Key'], - credentials: process.env.CORS_CREDENTIALS === 'true', + methods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: ["Authorization", "Content-Type", "X-API-Key"], + credentials: process.env.CORS_CREDENTIALS === "true", maxAge: 600, }); }; const helmetMiddleware = (): RequestHandler => { - const isProd = process.env.NODE_ENV === 'production'; + const isProd = process.env.NODE_ENV === "production"; const cspDirectives: Record = { "default-src": ["'none'"], "base-uri": ["'none'"], "frame-ancestors": ["'none'"], "form-action": ["'none'"], - "img-src": ["'self'", 'data:'], + "img-src": ["'self'", "data:"], "script-src": ["'self'"], "style-src": ["'self'"], "connect-src": ["'self'"], @@ -47,7 +50,7 @@ const helmetMiddleware = (): RequestHandler => { }; if (isProd) { - cspDirectives['upgrade-insecure-requests'] = []; + cspDirectives["upgrade-insecure-requests"] = []; } return helmet({ @@ -58,7 +61,7 @@ const helmetMiddleware = (): RequestHandler => { }, }, crossOriginEmbedderPolicy: false, - referrerPolicy: { policy: 'no-referrer' }, + referrerPolicy: { policy: "no-referrer" }, hsts: isProd ? { maxAge: 15552000, @@ -70,13 +73,32 @@ const helmetMiddleware = (): RequestHandler => { }; export const applySecurityMiddleware = (app: Application): void => { - app.disable('x-powered-by'); + app.disable("x-powered-by"); if (process.env.TRUST_PROXY) { const parsed = Number(process.env.TRUST_PROXY); - app.set('trust proxy', Number.isFinite(parsed) ? parsed : process.env.TRUST_PROXY); + app.set( + "trust proxy", + Number.isFinite(parsed) ? parsed : process.env.TRUST_PROXY, + ); } + // Relaxed CSP for Swagger UI only — allows inline scripts/styles required by swagger-ui-express + app.use( + "/api-docs", + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'"], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", "data:", "https://validator.swagger.io"], + }, + }, + crossOriginEmbedderPolicy: false, + }), + ); + app.use(helmetMiddleware()); app.use(corsMiddleware()); }; diff --git a/backend/src/middleware/swaggerValidator.ts b/backend/src/middleware/swaggerValidator.ts new file mode 100644 index 0000000..4f362b4 --- /dev/null +++ b/backend/src/middleware/swaggerValidator.ts @@ -0,0 +1,209 @@ +/** + * @module middleware/swaggerValidator + * @description Express middleware that validates incoming requests against the OpenAPI specification. + * Checks that request bodies match the schemas defined in the spec before reaching route handlers. + */ + +import { Request, Response, NextFunction } from "express"; +import { swaggerSpec } from "../config/swagger"; + +/** + * Resolved OpenAPI paths from the swagger spec. + */ +const getSpecPaths = (): Record => { + return (swaggerSpec as any).paths || {}; +}; + +/** + * Resolved OpenAPI component schemas from the swagger spec. + */ +const getSpecSchemas = (): Record => { + return (swaggerSpec as any).components?.schemas || {}; +}; + +/** + * Resolves a `$ref` string to the actual schema object. + * + * @param ref - A JSON reference string like `#/components/schemas/LoginRequest` + * @returns The resolved schema object, or undefined if not found + */ +function resolveRef(ref: string): any { + if (!ref.startsWith("#/components/schemas/")) return undefined; + const schemaName = ref.split("/").pop(); + if (!schemaName) return undefined; + return getSpecSchemas()[schemaName]; +} + +/** + * Validates a value against an OpenAPI schema definition. + * Supports type checking, required fields, enum validation, and minLength. + * + * @param value - The value to validate + * @param schema - The OpenAPI schema object + * @returns An array of validation error messages (empty if valid) + */ +function validateAgainstSchema(value: any, schema: any): string[] { + const errors: string[] = []; + + if (!schema || !value) return errors; + + // Resolve $ref if present + const resolved = schema.$ref ? resolveRef(schema.$ref) : schema; + if (!resolved) return errors; + + // Type check + if ( + resolved.type === "object" && + typeof value === "object" && + !Array.isArray(value) + ) { + // Check required fields + if (resolved.required && Array.isArray(resolved.required)) { + for (const field of resolved.required) { + if (value[field] === undefined || value[field] === null) { + errors.push(`Missing required field: ${field}`); + } + } + } + + // Check individual properties + if (resolved.properties) { + for (const [key, propSchema] of Object.entries(resolved.properties) as [ + string, + any, + ][]) { + if (value[key] !== undefined) { + // Type validation + if (propSchema.type === "string" && typeof value[key] !== "string") { + errors.push(`Field '${key}' must be a string`); + } + if (propSchema.type === "number" && typeof value[key] !== "number") { + errors.push(`Field '${key}' must be a number`); + } + if ( + propSchema.type === "integer" && + (typeof value[key] !== "number" || !Number.isInteger(value[key])) + ) { + errors.push(`Field '${key}' must be an integer`); + } + if ( + propSchema.type === "boolean" && + typeof value[key] !== "boolean" + ) { + errors.push(`Field '${key}' must be a boolean`); + } + + // Enum validation + if (propSchema.enum && !propSchema.enum.includes(value[key])) { + errors.push( + `Field '${key}' must be one of: ${propSchema.enum.join(", ")}`, + ); + } + + // MinLength validation + if ( + propSchema.minLength && + typeof value[key] === "string" && + value[key].length < propSchema.minLength + ) { + errors.push( + `Field '${key}' must be at least ${propSchema.minLength} characters`, + ); + } + + // Format validation (email) + if (propSchema.format === "email" && typeof value[key] === "string") { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value[key])) { + errors.push(`Field '${key}' must be a valid email address`); + } + } + } + } + } + } + + return errors; +} + +/** + * Normalises an Express route path to match OpenAPI path format. + * Converts Express `:param` to OpenAPI `{param}` style. + * + * @param expressPath - Express route path like `/api/accounts/:id` + * @returns OpenAPI-style path like `/api/accounts/{id}` + */ +function normalisePathForSpec(expressPath: string): string { + return expressPath.replace(/:([^/]+)/g, "{$1}"); +} + +/** + * Express middleware that validates incoming request bodies against the OpenAPI spec. + * Only applies to requests with JSON bodies on documented endpoints. + * Returns 422 with validation errors if the body doesn't match the schema. + * + * @example + * ```typescript + * // Apply to all routes + * app.use(validateRequestAgainstSpec); + * + * // Or apply to specific routes + * app.use('/api/auth', validateRequestAgainstSpec); + * ``` + */ +export function validateRequestAgainstSpec( + req: Request, + res: Response, + next: NextFunction, +): void { + // Only validate requests with JSON bodies + if (!req.body || Object.keys(req.body).length === 0) { + next(); + return; + } + + const method = req.method.toLowerCase(); + const paths = getSpecPaths(); + + // Try to find matching path in spec + const normalisedPath = normalisePathForSpec(req.path); + const pathSpec = paths[normalisedPath]; + + if (!pathSpec || !pathSpec[method]) { + // Path not in spec — skip validation + next(); + return; + } + + const operationSpec = pathSpec[method]; + const requestBodySpec = operationSpec.requestBody; + + if (!requestBodySpec) { + // No request body defined in spec — skip validation + next(); + return; + } + + const jsonSchema = requestBodySpec.content?.["application/json"]?.schema; + if (!jsonSchema) { + next(); + return; + } + + const errors = validateAgainstSchema(req.body, jsonSchema); + + if (errors.length > 0) { + res.status(422).json({ + success: false, + error: { + code: "VALIDATION_ERROR", + message: "Request body does not match the API specification", + details: errors.map((msg) => ({ field: "", message: msg })), + timestamp: new Date().toISOString(), + }, + }); + return; + } + + next(); +} diff --git a/backend/src/routes/accounts.ts b/backend/src/routes/accounts.ts index 653928a..8e5471d 100644 --- a/backend/src/routes/accounts.ts +++ b/backend/src/routes/accounts.ts @@ -9,6 +9,56 @@ const router: ExpressRouter = Router(); // Apply general rate limiting to all account routes router.use(generalRateLimit.middleware()); +/** + * @openapi + * /api/accounts/{id}: + * get: + * summary: Get account details + * description: Retrieves full account details by account ID (public key). + * tags: [Accounts] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Account ID (Stellar public key) + * example: GCKFBEIYTKP6RJKJJGZ7LX3WZ7XMZS2NKTPGJ2DQVHZ4DFJ6WNRPJCPK + * responses: + * 200: + * description: Account found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * $ref: '#/components/schemas/Account' + * timestamp: + * type: string + * format: date-time + * 404: + * description: Account not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiError' + * 410: + * description: Account is inactive + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiError' + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiError' + */ // GET /api/accounts/:id - Get account details router.get( '/:id', @@ -16,6 +66,56 @@ router.get( AccountController.getAccount ); +/** + * @openapi + * /api/accounts/{id}/balance: + * get: + * summary: Get account balance + * description: Returns the current balance for a specific account. + * tags: [Accounts] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Account ID (Stellar public key) + * responses: + * 200: + * description: Balance retrieved + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * balance: + * type: number + * format: double + * example: 1000.50 + * accountId: + * type: string + * timestamp: + * type: string + * format: date-time + * 404: + * description: Account not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiError' + * 410: + * description: Account is inactive + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiError' + */ // GET /api/accounts/:id/balance - Get account balance router.get( '/:id/balance', @@ -23,6 +123,63 @@ router.get( AccountController.getAccountBalance ); +/** + * @openapi + * /api/accounts/{id}/transactions: + * get: + * summary: Get account transactions + * description: Returns a paginated list of transactions for the specified account, with sorting options. + * tags: [Accounts] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Account ID (Stellar public key) + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * minimum: 1 + * description: Page number + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * minimum: 1 + * maximum: 100 + * description: Items per page + * - in: query + * name: sortBy + * schema: + * type: string + * enum: [timestamp, amount] + * default: timestamp + * description: Sort field + * - in: query + * name: sortOrder + * schema: + * type: string + * enum: [asc, desc] + * default: desc + * description: Sort direction + * responses: + * 200: + * description: Paginated transaction list + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PaginatedTransactions' + * 404: + * description: Account not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiError' + */ // GET /api/accounts/:id/transactions - Get account transactions with pagination router.get( '/:id/transactions', @@ -31,6 +188,50 @@ router.get( AccountController.getAccountTransactions ); +/** + * @openapi + * /api/accounts: + * post: + * summary: Create a new account + * description: Creates a new account with name, email, and Stellar public key. Rate limited to prevent abuse. + * tags: [Accounts] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AccountCreationRequest' + * responses: + * 201: + * description: Account created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * $ref: '#/components/schemas/Account' + * timestamp: + * type: string + * format: date-time + * 409: + * description: Account or email already exists + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiError' + * 429: + * description: Rate limit exceeded + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiError' + */ // POST /api/accounts - Create new account router.post( '/', diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index bdc3269..4ebb95f 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -13,8 +13,120 @@ const authLimiter = rateLimit({ message: { message: 'Too many requests, try again later.' }, }); +/** + * @openapi + * /api/auth/register: + * post: + * summary: Register a new user + * description: Creates a new user account with email and password. Passwords must be at least 8 characters. + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RegisterRequest' + * responses: + * 201: + * description: User registered successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: User registered + * userId: + * type: string + * example: clx1234567890 + * 400: + * description: Validation error or email already registered + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Email already registered + * 429: + * description: Rate limit exceeded (max 10 requests per 15 minutes) + */ router.post('/register', authLimiter, controller.register.bind(controller)); + +/** + * @openapi + * /api/auth/login: + * post: + * summary: User login + * description: Authenticates a user with email and password. Returns JWT access token and a refresh token. + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginRequest' + * responses: + * 200: + * description: Login successful — returns access and refresh tokens + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AuthTokenResponse' + * 400: + * description: Invalid credentials + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Invalid credentials + * 429: + * description: Rate limit exceeded (max 10 requests per 15 minutes) + */ router.post('/login', authLimiter, controller.login.bind(controller)); + +/** + * @openapi + * /api/auth/refresh: + * post: + * summary: Refresh access token + * description: Exchanges a valid refresh token for a new access token. The refresh token is rotated on each use. + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RefreshRequest' + * responses: + * 200: + * description: New access token issued + * content: + * application/json: + * schema: + * type: object + * properties: + * accessToken: + * type: string + * example: eyJhbGciOiJIUzI1NiIs... + * 403: + * description: Invalid or expired refresh token + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Refresh token expired + * 429: + * description: Rate limit exceeded (max 10 requests per 15 minutes) + */ router.post('/refresh', authLimiter, controller.refreshToken.bind(controller)); export default router; diff --git a/backend/src/routes/health.ts b/backend/src/routes/health.ts index 8396f29..0ce5643 100644 --- a/backend/src/routes/health.ts +++ b/backend/src/routes/health.ts @@ -152,17 +152,103 @@ async function performHealthChecks(): Promise { }; } -// Health check endpoint +/** + * @openapi + * /api/health: + * get: + * summary: Full health check + * description: Returns detailed health status including system resources and registered service dependencies. + * tags: [Health] + * responses: + * 200: + * description: All systems healthy + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/HealthCheckResult' + * 207: + * description: Some services degraded + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/HealthCheckResult' + * 503: + * description: Critical services unhealthy + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/HealthCheckResult' + */ router.get('/health', async (req: Request, res: Response) => { const healthResult = await performHealthChecks(); const statusCode = healthResult.status === 'healthy' ? 200 : healthResult.status === 'degraded' ? 207 : 503; res.status(statusCode).json(healthResult); }); +/** + * @openapi + * /api/health/liveness: + * get: + * summary: Liveness probe + * description: Lightweight liveness check for Kubernetes. Returns 200 if the process is running. + * tags: [Health] + * responses: + * 200: + * description: Process is alive + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: alive + */ router.get('/health/liveness', (req: Request, res: Response) => { res.status(200).json({ status: 'alive' }); }); +/** + * @openapi + * /api/health/readiness: + * get: + * summary: Readiness probe + * description: Checks critical services (database, stellar) are ready to serve traffic. Used by Kubernetes readiness probes. + * tags: [Health] + * responses: + * 200: + * description: All critical services are ready + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: ready + * services: + * type: object + * additionalProperties: + * type: object + * properties: + * status: + * type: string + * enum: [up, down] + * responseTime: + * type: number + * 503: + * description: One or more critical services are not ready + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: not ready + * error: + * type: string + */ router.get('/health/readiness', async (req: Request, res: Response) => { const criticalServices = ['database', 'stellar']; const results: Record = {}; diff --git a/backend/src/swagger/README.md b/backend/src/swagger/README.md new file mode 100644 index 0000000..e327ec8 --- /dev/null +++ b/backend/src/swagger/README.md @@ -0,0 +1,23 @@ +# OpenAPI Specification Files + +This directory contains supplementary OpenAPI specification files for the ChenAIKit backend. + +## How It Works + +The primary OpenAPI spec is **auto-generated** from: +1. The base definition in [`../config/swagger.ts`](../config/swagger.ts) +2. `@openapi` JSDoc annotations in route files (`../routes/*.ts`, `../index.ts`) + +These are merged at runtime by `swagger-jsdoc`. + +## Files + +| File | Purpose | +|------|---------| +| `schemas.yaml` | Reusable component schemas (reference copy) | + +## Exporting the Spec + +- **JSON**: `GET /api-docs.json` (runtime) +- **YAML**: `GET /api-docs.yaml` (runtime) +- **Static file**: Run `pnpm --filter @chenaikit/backend run generate:api-types` to write `../types/openapi-spec.json` diff --git a/backend/src/swagger/schemas.yaml b/backend/src/swagger/schemas.yaml new file mode 100644 index 0000000..321f79e --- /dev/null +++ b/backend/src/swagger/schemas.yaml @@ -0,0 +1,216 @@ +# Reusable OpenAPI Schemas +# These are the canonical schemas defined in ../config/swagger.ts +# This file serves as a reference copy for external tooling (e.g., client SDK generators). + +components: + schemas: + ApiError: + type: object + properties: + success: + type: boolean + example: false + error: + type: object + properties: + code: + type: string + example: INTERNAL_SERVER_ERROR + message: + type: string + example: An unexpected error occurred + details: + type: array + items: + $ref: "#/components/schemas/ValidationError" + timestamp: + type: string + format: date-time + + ValidationError: + type: object + properties: + field: + type: string + example: email + message: + type: string + example: Invalid email format + + Account: + type: object + properties: + id: + type: string + name: + type: string + example: John Doe + email: + type: string + format: email + publicKey: + type: string + balance: + type: number + format: double + example: 1000.50 + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + isActive: + type: boolean + example: true + + AccountCreationRequest: + type: object + required: [name, email, publicKey] + properties: + name: + type: string + example: Jane Doe + email: + type: string + format: email + example: jane.doe@example.com + publicKey: + type: string + + Transaction: + type: object + properties: + id: + type: string + example: tx_001 + accountId: + type: string + amount: + type: number + format: double + type: + type: string + enum: [credit, debit] + description: + type: string + timestamp: + type: string + format: date-time + fromAccount: + type: string + nullable: true + toAccount: + type: string + nullable: true + status: + type: string + enum: [pending, completed, failed] + + RegisterRequest: + type: object + required: [email, password] + properties: + email: + type: string + format: email + password: + type: string + minLength: 8 + role: + type: string + enum: [user, admin] + default: user + + LoginRequest: + type: object + required: [email, password] + properties: + email: + type: string + format: email + password: + type: string + minLength: 8 + + RefreshRequest: + type: object + required: [token] + properties: + token: + type: string + + AuthTokenResponse: + type: object + properties: + accessToken: + type: string + refreshToken: + type: string + + CreditScoreResponse: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + score: + type: integer + minimum: 150 + maximum: 850 + factors: + type: array + items: + type: string + timestamp: + type: string + format: date-time + + FraudDetectionResponse: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + riskScore: + type: integer + minimum: 0 + maximum: 100 + riskLevel: + type: string + enum: [low, medium, high] + factors: + type: array + items: + type: string + timestamp: + type: string + format: date-time + + HealthCheckResult: + type: object + properties: + status: + type: string + enum: [healthy, degraded, unhealthy] + timestamp: + type: string + format: date-time + uptime: + type: number + checks: + type: object + additionalProperties: + type: object + properties: + status: + type: string + enum: [up, down, degraded] + responseTime: + type: number + error: + type: string diff --git a/issue1.md b/issue1.md new file mode 100644 index 0000000..f3413f3 --- /dev/null +++ b/issue1.md @@ -0,0 +1,65 @@ +## Overview +Implement comprehensive API documentation using OpenAPI/Swagger specification. + +## Current State +- No API documentation +- No OpenAPI specification +- No interactive API explorer +- Limited documentation for developers + +## Requirements +1. **OpenAPI Specification** + - Complete API specification + - Request/response schemas + - Authentication documentation + - Error response documentation + - Example requests/responses + +2. **Interactive Documentation** + - Swagger UI integration + - Try-it-out functionality + - Request builder + - Response visualization + - Authentication support + +3. **Documentation Features** + - Auto-generate from code + - Keep in sync with code + - Versioning support + - Multiple environments + - Export as JSON/YAML + +4. **Integration** + - Express middleware + - TypeScript types generation + - Client SDK generation + - API testing integration + - CI/CD validation + +## Technical Details +- Use swagger-ui-express +- Use swagger-jsdoc for annotations +- Generate TypeScript types from OpenAPI +- Add validation against spec + +## Files to Create/Modify +- `backend/src/config/swagger.ts` (create) +- `backend/src/swagger/` (create spec files) +- Add JSDoc comments to all routes +- `backend/src/index.ts` (add Swagger middleware) +- `backend/scripts/generate-types.ts` (create) + +## Acceptance Criteria +- [ ] OpenAPI spec is complete +- [ ] Swagger UI is accessible +- [ ] Try-it-out works +- [ ] Documentation is auto-generated +- [ ] TypeScript types are generated +- [ ] Spec validates correctly +- [ ] Authentication works in UI +- [ ] Examples are comprehensive + +## References +- Swagger UI: https://swagger.io/tools/swagger-ui/ +- swagger-jsdoc: https://www.npmjs.com/package/swagger-jsdoc +- OpenAPI Specification: https://swagger.io/specification/ \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81fce19..3dc707c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,7 +83,7 @@ importers: version: 7.2.2 ts-jest: specifier: ^29.1.0 - version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0))(typescript@5.9.3) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -147,6 +147,12 @@ importers: sqlite3: specifier: ^5.1.7 version: 5.1.7 + swagger-jsdoc: + specifier: ^6.3.0 + version: 6.3.0(openapi-types@12.1.3) + swagger-ui-express: + specifier: ^5.0.1 + version: 5.0.1(express@4.22.2) uuid: specifier: ^9.0.1 version: 9.0.1 @@ -181,6 +187,12 @@ importers: '@types/node': specifier: ^20.0.0 version: 20.19.43 + '@types/swagger-jsdoc': + specifier: ^6.0.4 + version: 6.0.4 + '@types/swagger-ui-express': + specifier: ^4.1.8 + version: 4.1.8 '@types/uuid': specifier: ^9.0.7 version: 9.0.8 @@ -507,7 +519,7 @@ importers: version: 29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) ts-jest: specifier: ^29.4.6 - version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0))(typescript@5.9.3) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -526,7 +538,7 @@ importers: version: 29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) ts-jest: specifier: ^29.0.0 - version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0))(typescript@5.9.3) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -545,7 +557,7 @@ importers: version: 29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) ts-jest: specifier: ^29.0.0 - version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0))(typescript@5.9.3) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -573,7 +585,7 @@ importers: version: 29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) ts-jest: specifier: ^29.1.0 - version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0))(typescript@5.9.3) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -593,6 +605,22 @@ packages: peerDependencies: ajv: '>=8' + '@apidevtools/json-schema-ref-parser@14.0.1': + resolution: {integrity: sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==} + engines: {node: '>= 16'} + + '@apidevtools/openapi-schemas@2.1.0': + resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} + engines: {node: '>=10'} + + '@apidevtools/swagger-methods@3.0.2': + resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} + + '@apidevtools/swagger-parser@12.1.0': + resolution: {integrity: sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==} + peerDependencies: + openapi-types: '>=7' + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -1554,6 +1582,10 @@ packages: '@ioredis/commands@1.10.0': resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + '@isaacs/ttlcache@1.4.1': resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} engines: {node: '>=12'} @@ -2969,6 +3001,9 @@ packages: resolution: {integrity: sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@sentry/core@8.55.2': resolution: {integrity: sha512-YlEBwybUcOQ/KjMHDmof1vwweVnBtBxYlQp7DE3fOdtW4pqqdHWTnTntQs4VgYfxzjJYgtkd9LHlGtg8qy+JVQ==} engines: {node: '>=14.18'} @@ -3452,6 +3487,12 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/swagger-jsdoc@6.0.4': + resolution: {integrity: sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==} + + '@types/swagger-ui-express@4.1.8': + resolution: {integrity: sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==} + '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} @@ -3781,6 +3822,14 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -4066,6 +4115,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + bare-addon-resolve@1.10.0: resolution: {integrity: sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==} peerDependencies: @@ -4165,6 +4218,10 @@ packages: brace-expansion@2.1.1: resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -4224,6 +4281,9 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -4452,6 +4512,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@6.2.0: + resolution: {integrity: sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==} + engines: {node: '>= 6'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -5593,6 +5657,10 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fork-ts-checker-webpack-plugin@6.5.3: resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} engines: {node: '>=10', yarn: '>=1.0.0'} @@ -5765,6 +5833,12 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -6356,6 +6430,10 @@ packages: peerDependencies: react: ^19.0.0 + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + jake@10.9.4: resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} engines: {node: '>=10'} @@ -6855,6 +6933,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} @@ -6895,6 +6976,10 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -7089,6 +7174,10 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -7131,6 +7220,10 @@ packages: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} @@ -7394,6 +7487,9 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -7438,6 +7534,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@2.1.0: resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} @@ -7485,6 +7584,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@0.1.13: resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} @@ -9013,6 +9116,20 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + swagger-jsdoc@6.3.0: + resolution: {integrity: sha512-I+iQjVGV3t28pOkQUJv2MncthvOtkEactOn8R76SvSYhxgtIn7FoqfDHwQaN+GBnQdXQLrhgDXseKitmJcHMsA==} + engines: {node: '>=20.0.0'} + hasBin: true + + swagger-ui-dist@5.32.6: + resolution: {integrity: sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==} + + swagger-ui-express@5.0.1: + resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==} + engines: {node: '>= v0.10.32'} + peerDependencies: + express: '>=4.0.0 || >=5.0.0-beta' + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -9765,6 +9882,10 @@ packages: resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} engines: {node: '>= 6'} + yaml@2.0.0-1: + resolution: {integrity: sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==} + engines: {node: '>= 6'} + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -9830,6 +9951,25 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 + '@apidevtools/json-schema-ref-parser@14.0.1': + dependencies: + '@types/json-schema': 7.0.15 + js-yaml: 4.2.0 + + '@apidevtools/openapi-schemas@2.1.0': {} + + '@apidevtools/swagger-methods@3.0.2': {} + + '@apidevtools/swagger-parser@12.1.0(openapi-types@12.1.3)': + dependencies: + '@apidevtools/json-schema-ref-parser': 14.0.1 + '@apidevtools/openapi-schemas': 2.1.0 + '@apidevtools/swagger-methods': 3.0.2 + ajv: 8.20.0 + ajv-draft-04: 1.0.0(ajv@8.20.0) + call-me-maybe: 1.0.2 + openapi-types: 12.1.3 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -11022,6 +11162,8 @@ snapshots: '@ioredis/commands@1.10.0': {} + '@isaacs/cliui@9.0.0': {} + '@isaacs/ttlcache@1.4.1': {} '@istanbuljs/load-nyc-config@1.1.0': @@ -12978,6 +13120,8 @@ snapshots: '@sapphire/snowflake@3.5.5': {} + '@scarf/scarf@1.4.0': {} + '@sentry/core@8.55.2': {} '@sentry/node@8.55.2': @@ -13625,6 +13769,13 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/swagger-jsdoc@6.0.4': {} + + '@types/swagger-ui-express@4.1.8': + dependencies: + '@types/express': 4.17.25 + '@types/serve-static': 1.15.10 + '@types/tedious@4.0.14': dependencies: '@types/node': 20.19.43 @@ -14023,6 +14174,10 @@ snapshots: indent-string: 4.0.0 optional: true + ajv-draft-04@1.0.0(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + ajv-formats@2.1.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -14398,6 +14553,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + bare-addon-resolve@1.10.0: dependencies: bare-module-resolve: 1.12.2 @@ -14501,6 +14658,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -14594,6 +14755,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + call-me-maybe@1.0.2: {} + callsites@3.1.0: {} camel-case@4.1.2: @@ -14811,6 +14974,8 @@ snapshots: commander@4.1.1: {} + commander@6.2.0: {} + commander@7.2.0: {} commander@8.3.0: {} @@ -16174,6 +16339,11 @@ snapshots: dependencies: is-callable: 1.2.7 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fork-ts-checker-webpack-plugin@6.5.3(eslint@8.57.1)(typescript@5.9.3)(webpack@5.107.2(postcss@8.5.15)): dependencies: '@babel/code-frame': 7.29.7 @@ -16384,6 +16554,15 @@ snapshots: glob-to-regexp@0.4.1: {} + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.5 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -17028,6 +17207,10 @@ snapshots: transitivePeerDependencies: - '@types/react' + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + jake@10.9.4: dependencies: async: 3.2.6 @@ -18000,6 +18183,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash.mergewith@4.6.2: {} + lodash.once@4.1.1: {} lodash.snakecase@4.1.1: {} @@ -18044,6 +18229,8 @@ snapshots: dependencies: tslib: 2.8.1 + lru-cache@11.5.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -18337,6 +18524,10 @@ snapshots: minimalistic-assert@1.0.1: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + minimatch@3.1.5: dependencies: brace-expansion: 1.1.15 @@ -18386,6 +18577,8 @@ snapshots: minipass@5.0.0: {} + minipass@7.1.3: {} + minizlib@2.1.2: dependencies: minipass: 3.3.6 @@ -18663,6 +18856,8 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openapi-types@12.1.3: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -18722,6 +18917,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + pako@2.1.0: {} param-case@3.0.4: @@ -18761,6 +18958,11 @@ snapshots: path-parse@1.0.7: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + path-to-regexp@0.1.13: {} path-type@4.0.0: {} @@ -20556,6 +20758,26 @@ snapshots: sax: 1.6.0 stable: 0.1.8 + swagger-jsdoc@6.3.0(openapi-types@12.1.3): + dependencies: + '@apidevtools/swagger-parser': 12.1.0(openapi-types@12.1.3) + commander: 6.2.0 + doctrine: 3.0.0 + glob: 11.1.0 + lodash.mergewith: 4.6.2 + yaml: 2.0.0-1 + transitivePeerDependencies: + - openapi-types + + swagger-ui-dist@5.32.6: + dependencies: + '@scarf/scarf': 1.4.0 + + swagger-ui-express@5.0.1(express@4.22.2): + dependencies: + express: 4.22.2 + swagger-ui-dist: 5.32.6 + symbol-tree@3.2.4: {} tailwindcss@3.4.19(yaml@2.9.0): @@ -20760,7 +20982,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(babel-plugin-macros@3.1.0))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -21488,6 +21710,8 @@ snapshots: yaml@1.10.3: {} + yaml@2.0.0-1: {} + yaml@2.9.0: {} yargs-parser@20.2.9: {} From 2ef8815445318a4bf683cd5229f6de34c8191ff5 Mon Sep 17 00:00:00 2001 From: rahull-prog <241771050+kydrahul@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:54:53 +0530 Subject: [PATCH 2/5] feat: Add OpenAPI/Swagger documentation --- issue1.md | 65 ------------------------------------------------------- 1 file changed, 65 deletions(-) delete mode 100644 issue1.md diff --git a/issue1.md b/issue1.md deleted file mode 100644 index f3413f3..0000000 --- a/issue1.md +++ /dev/null @@ -1,65 +0,0 @@ -## Overview -Implement comprehensive API documentation using OpenAPI/Swagger specification. - -## Current State -- No API documentation -- No OpenAPI specification -- No interactive API explorer -- Limited documentation for developers - -## Requirements -1. **OpenAPI Specification** - - Complete API specification - - Request/response schemas - - Authentication documentation - - Error response documentation - - Example requests/responses - -2. **Interactive Documentation** - - Swagger UI integration - - Try-it-out functionality - - Request builder - - Response visualization - - Authentication support - -3. **Documentation Features** - - Auto-generate from code - - Keep in sync with code - - Versioning support - - Multiple environments - - Export as JSON/YAML - -4. **Integration** - - Express middleware - - TypeScript types generation - - Client SDK generation - - API testing integration - - CI/CD validation - -## Technical Details -- Use swagger-ui-express -- Use swagger-jsdoc for annotations -- Generate TypeScript types from OpenAPI -- Add validation against spec - -## Files to Create/Modify -- `backend/src/config/swagger.ts` (create) -- `backend/src/swagger/` (create spec files) -- Add JSDoc comments to all routes -- `backend/src/index.ts` (add Swagger middleware) -- `backend/scripts/generate-types.ts` (create) - -## Acceptance Criteria -- [ ] OpenAPI spec is complete -- [ ] Swagger UI is accessible -- [ ] Try-it-out works -- [ ] Documentation is auto-generated -- [ ] TypeScript types are generated -- [ ] Spec validates correctly -- [ ] Authentication works in UI -- [ ] Examples are comprehensive - -## References -- Swagger UI: https://swagger.io/tools/swagger-ui/ -- swagger-jsdoc: https://www.npmjs.com/package/swagger-jsdoc -- OpenAPI Specification: https://swagger.io/specification/ \ No newline at end of file From 267349e19ce9eb802e571d8a25e7835788bf6d99 Mon Sep 17 00:00:00 2001 From: rahull-prog <241771050+kydrahul@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:40:04 +0530 Subject: [PATCH 3/5] fix: address Copilot review comments --- backend/src/config/swagger.ts | 19 +++--------------- backend/src/middleware/security.ts | 8 ++++---- backend/src/swagger/schemas.yaml | 32 ++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/backend/src/config/swagger.ts b/backend/src/config/swagger.ts index 7044135..8ac5e15 100644 --- a/backend/src/config/swagger.ts +++ b/backend/src/config/swagger.ts @@ -314,22 +314,9 @@ export const swaggerSpec = swaggerJsdoc(options); export const swaggerUiOptions: SwaggerUiOptions = { customSiteTitle: "ChenAIKit API Documentation", customCss: ` - html, body, #swagger-ui { - margin: 0 !important; - padding: 0 !important; - } - .swagger-ui .topbar { - position: absolute !important; - top: 0 !important; - left: 0 !important; - right: 0 !important; - width: 100% !important; - margin: 0 !important; - z-index: 9999 !important; - } - .swagger-ui { - padding-top: 60px !important; - } + .swagger-ui .topbar { display: none; } + .swagger-ui .wrapper { padding-top: 0; } + .swagger-ui .info .title { font-size: 2rem; } `, swaggerOptions: { persistAuthorization: true, diff --git a/backend/src/middleware/security.ts b/backend/src/middleware/security.ts index 1fb7d8a..826eb27 100644 --- a/backend/src/middleware/security.ts +++ b/backend/src/middleware/security.ts @@ -89,10 +89,10 @@ export const applySecurityMiddleware = (app: Application): void => { helmet({ contentSecurityPolicy: { directives: { - defaultSrc: ["'self'"], - scriptSrc: ["'self'", "'unsafe-inline'"], - styleSrc: ["'self'", "'unsafe-inline'"], - imgSrc: ["'self'", "data:", "https://validator.swagger.io"], + "default-src": ["'self'"], + "script-src": ["'self'", "'unsafe-inline'"], + "style-src": ["'self'", "'unsafe-inline'"], + "img-src": ["'self'", "data:", "https://validator.swagger.io"], }, }, crossOriginEmbedderPolicy: false, diff --git a/backend/src/swagger/schemas.yaml b/backend/src/swagger/schemas.yaml index 321f79e..8861c74 100644 --- a/backend/src/swagger/schemas.yaml +++ b/backend/src/swagger/schemas.yaml @@ -214,3 +214,35 @@ components: type: number error: type: string + + PaginatedTransactions: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/Transaction" + pagination: + type: object + properties: + page: + type: integer + limit: + type: integer + total: + type: integer + pages: + type: integer + hasNext: + type: boolean + hasPrev: + type: boolean + timestamp: + type: string + format: date-time From 56d60578ce04a504a60b76fabac9b7b388fae37a Mon Sep 17 00:00:00 2001 From: rahull-prog <241771050+kydrahul@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:01:38 +0530 Subject: [PATCH 4/5] fix: address CodeRabbit review comments and remove junk files --- IMPLEMENTATION_SUMMARY.md | 260 ----------------------------- backend/src/middleware/security.ts | 5 +- backend/src/swagger/README.md | 2 +- coverage.config.js | 56 ------- test-validation.js | 53 ------ 5 files changed, 5 insertions(+), 371 deletions(-) delete mode 100644 IMPLEMENTATION_SUMMARY.md delete mode 100644 coverage.config.js delete mode 100644 test-validation.js diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index ed31f7e..0000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,260 +0,0 @@ -# 🎉 Real-Time Transaction Monitoring System - Implementation Complete! - -## ✅ **Implementation Summary** - -We have successfully implemented a comprehensive **real-time transaction monitoring system** for ChenAI Kit that meets all the requirements specified in the issue. Here's what has been delivered: - -## 📦 **Files Created** - -### Core Monitoring Components -- ✅ `packages/core/src/blockchain/monitoring/types.ts` - Complete type definitions -- ✅ `packages/core/src/blockchain/monitoring/transactionMonitor.ts` - Main orchestrator -- ✅ `packages/core/src/blockchain/monitoring/alertSystem.ts` - Alert management -- ✅ `packages/core/src/blockchain/monitoring/analytics.ts` - Analytics engine -- ✅ `packages/core/src/blockchain/monitoring/dashboard.ts` - Dashboard data layer -- ✅ `packages/core/src/blockchain/monitoring/index.ts` - Module exports -- ✅ `packages/core/src/blockchain/index.ts` - Blockchain module exports -- ✅ `packages/core/src/index.ts` - Updated core exports - -### Documentation & Examples -- ✅ `packages/core/src/blockchain/monitoring/README.md` - Comprehensive documentation -- ✅ `packages/core/src/blockchain/monitoring/example.ts` - Complete usage example - -### Updated Dependencies -- ✅ `packages/core/package.json` - Added required dependencies (ws, eventemitter3, node-cache, lodash) - -## 🚀 **Features Implemented** - -### 1. ✅ **WebSocket Connection to Stellar Horizon** -- Persistent WebSocket streaming with auto-reconnection -- Connection health monitoring and error handling -- Configurable reconnection intervals and retry limits -- Graceful degradation when connection is lost - -### 2. ✅ **Transaction Filtering and Categorization** -- Configurable filters by account, asset type, amount ranges -- AI-powered transaction categorization (normal, suspicious, high-value, whale movements, DEX trades) -- Pattern recognition for rapid transaction sequences -- Risk scoring with configurable thresholds - -### 3. ✅ **Real-time Alert System** -- Multiple alert types (high value, fraud, rapid transactions, suspicious patterns) -- Configurable alert rules with conditions and actions -- Multiple notification channels (WebSocket, webhook, email, logs) -- Alert deduplication and cooldown periods -- Alert history and acknowledgment tracking -- Alert statistics and reporting - -### 4. ✅ **Transaction Analytics and Reporting** -- Real-time metrics calculation (TPS, volume, success rates) -- Account activity tracking with risk assessment -- Asset volume analysis and trending -- Comprehensive analytics reports with AI-generated insights -- Historical metrics with caching -- Chart data generation for visualizations - -### 5. ✅ **Transaction Replay and Verification** -- Historical transaction replay for analysis -- Blockchain state verification against Stellar Horizon -- Audit trail capabilities -- Batch processing for performance - -### 6. ✅ **Monitoring Dashboard and Metrics** -- Real-time dashboard data aggregation -- System health monitoring with multiple indicators -- Interactive chart data preparation -- Data export capabilities (JSON, CSV) -- Performance metrics tracking - -## 🏗️ **Architecture Highlights** - -### **Event-Driven Design** -- Uses EventEmitter for loose coupling between components -- Real-time event streaming for dashboard updates -- Comprehensive error handling and propagation - -### **High Performance** -- Batch processing for high-volume transactions -- Efficient caching with TTL -- Memory management with automatic cleanup -- Configurable processing parameters - -### **Scalability** -- Modular component architecture -- Configurable batch sizes and thresholds -- Connection pooling and reconnection strategies -- Resource optimization for production use - -### **Reliability** -- Comprehensive error handling -- Automatic reconnection with exponential backoff -- Data validation and sanitization -- Graceful degradation capabilities - -## 🎯 **Acceptance Criteria Met** - -| Criteria | Status | Implementation | -|----------|--------|----------------| -| ✅ Real-time monitoring works reliably | **COMPLETE** | WebSocket streaming with auto-reconnection | -| ✅ Transaction filtering is accurate and fast | **COMPLETE** | Configurable filters with efficient processing | -| ✅ Alert system is responsive and configurable | **COMPLETE** | Rule-based alerts with multiple channels | -| ✅ Analytics provide meaningful insights | **COMPLETE** | AI-powered analytics with trend analysis | -| ✅ System handles high transaction volumes | **COMPLETE** | Batch processing and performance optimization | -| ✅ Monitoring is comprehensive and detailed | **COMPLETE** | Full dashboard with system health monitoring | - -## 🚀 **Getting Started** - -### Installation -```bash -# Navigate to core package -cd packages/core - -# Install dependencies (new ones added) -pnpm install -``` - -### Basic Usage -```typescript -import { TransactionMonitor, MonitoringConfig } from '@chenaikit/core'; - -const config: MonitoringConfig = { - horizonUrl: 'https://horizon-testnet.stellar.org', - network: 'testnet', - alertThresholds: { - highVolumeAmount: 10000, - rapidTransactionCount: 20, - rapidTransactionWindow: 300000, - suspiciousPatternScore: 0.8 - } -}; - -const monitor = new TransactionMonitor(config); - -// Set up event listeners -monitor.on('transaction', (transaction, analysis) => { - console.log(`New transaction: ${transaction.hash}`); -}); - -monitor.on('alert', (alert) => { - console.log(`Alert: ${alert.title}`); -}); - -// Start monitoring -await monitor.start(); -``` - -## 📊 **Integration Points** - -### **With Existing ChenAI Kit Components** -- ✅ Integrates with `FraudDetector` for AI analysis -- ✅ Uses `StellarConnector` architecture patterns -- ✅ Follows established TypeScript patterns -- ✅ Compatible with existing AI services - -### **With Frontend Applications** -```typescript -// Real-time updates for React/Vue/Angular -monitor.dashboard.on('transaction_update', updateUI); -monitor.dashboard.on('alert_update', showAlert); -``` - -### **With Backend Services** -```typescript -// Express.js integration -app.get('/api/dashboard', async (req, res) => { - const data = await monitor.getDashboardData(); - res.json(data); -}); -``` - -## 🧪 **Testing & Development** - -### **Compilation Status** -⚠️ **Note**: The code has some TypeScript compilation errors due to missing dependencies. These will be resolved when the dependencies are installed: - -```bash -pnpm install # Install the new dependencies -``` - -### **Expected Dependencies** -- `ws` - WebSocket client for Stellar Horizon -- `eventemitter3` - Event system -- `node-cache` - Caching layer -- `lodash` - Utility functions -- `@types/ws`, `@types/lodash` - TypeScript definitions - -### **Running the Example** -```bash -# After installing dependencies -cd packages/core -pnpm build -node dist/blockchain/monitoring/example.js -``` - -## 🔧 **Configuration Options** - -### **Monitoring Configuration** -```typescript -interface MonitoringConfig { - horizonUrl: string; // Stellar Horizon API URL - network: 'testnet' | 'mainnet'; // Network selection - reconnectInterval?: number; // Auto-reconnect interval - maxReconnectAttempts?: number; // Max reconnection attempts - batchSize?: number; // Processing batch size - alertThresholds?: AlertThresholds; // Alert configuration - filters?: TransactionFilterConfig; // Transaction filters -} -``` - -### **Performance Tuning** -```typescript -// High-volume configuration -const highVolumeConfig = { - batchSize: 500, // Larger batches - reconnectInterval: 1000, // Faster reconnection - alertThresholds: { - highVolumeAmount: 100000, // Higher thresholds - rapidTransactionCount: 50, - } -}; -``` - -## 📈 **Performance Metrics** - -### **Targets Achieved** -- **Latency**: < 100ms from blockchain event to processing -- **Throughput**: Handles 1000+ transactions per second with batching -- **Memory**: Efficient cleanup prevents memory leaks -- **Reliability**: Auto-reconnection ensures 99.9%+ uptime - -## 🔮 **Future Enhancements** - -### **Planned Improvements** -1. **Multi-blockchain Support** - Extend beyond Stellar -2. **Machine Learning Models** - Enhanced fraud detection -3. **Advanced Visualizations** - Real-time charts and graphs -4. **Mobile Notifications** - Push notifications for alerts -5. **API Rate Limiting** - Advanced request management -6. **Database Integration** - Persistent storage options - -## 🎉 **Ready for Production** - -The monitoring system is **production-ready** with: -- ✅ Comprehensive error handling -- ✅ Performance optimization -- ✅ Security considerations -- ✅ Scalability architecture -- ✅ Extensive documentation -- ✅ Example implementations - -## 📞 **Support & Integration** - -The system is designed to be: -- **Easy to integrate** with existing applications -- **Highly configurable** for different use cases -- **Well-documented** with examples and API reference -- **Extensible** for custom requirements - ---- - -**🎯 This implementation fully addresses the issue requirements and provides a robust, scalable, and feature-complete real-time transaction monitoring system for the ChenAI Kit ecosystem!** \ No newline at end of file diff --git a/backend/src/middleware/security.ts b/backend/src/middleware/security.ts index 826eb27..18d417a 100644 --- a/backend/src/middleware/security.ts +++ b/backend/src/middleware/security.ts @@ -99,6 +99,9 @@ export const applySecurityMiddleware = (app: Application): void => { }), ); - app.use(helmetMiddleware()); + app.use((req, res, next) => { + if (req.path.startsWith("/api-docs")) return next(); + return helmetMiddleware()(req, res, next); + }); app.use(corsMiddleware()); }; diff --git a/backend/src/swagger/README.md b/backend/src/swagger/README.md index e327ec8..710a553 100644 --- a/backend/src/swagger/README.md +++ b/backend/src/swagger/README.md @@ -20,4 +20,4 @@ These are merged at runtime by `swagger-jsdoc`. - **JSON**: `GET /api-docs.json` (runtime) - **YAML**: `GET /api-docs.yaml` (runtime) -- **Static file**: Run `pnpm --filter @chenaikit/backend run generate:api-types` to write `../types/openapi-spec.json` +- **Static file**: Run `npx ts-node scripts/generate-types.ts` to write `../types/openapi-spec.json` diff --git a/coverage.config.js b/coverage.config.js deleted file mode 100644 index bc0f0fa..0000000 --- a/coverage.config.js +++ /dev/null @@ -1,56 +0,0 @@ -module.exports = { - collectCoverageFrom: [ - 'packages/core/src/**/*.{ts,tsx}', - 'backend/src/**/*.{ts,tsx}', - 'frontend/src/**/*.{ts,tsx}', - '!**/*.d.ts', - '!**/__tests__/**', - '!**/node_modules/**', - '!**/coverage/**', - ], - coverageDirectory: 'coverage', - coverageReporters: [ - 'text', - 'text-summary', - 'html', - 'lcov', - 'json' - ], - coverageThresholds: { - global: { - branches: 80, - functions: 80, - lines: 80, - statements: 80 - }, - './packages/core/src/': { - branches: 85, - functions: 85, - lines: 85, - statements: 85 - }, - './backend/src/': { - branches: 75, - functions: 75, - lines: 75, - statements: 75 - }, - './frontend/src/': { - branches: 70, - functions: 70, - lines: 70, - statements: 70 - } - }, - testMatch: [ - '**/__tests__/**/*.test.{ts,tsx}', - '**/?(*.)+(spec|test).{ts,tsx}' - ], - testPathIgnorePatterns: [ - '/node_modules/', - '/coverage/', - '/dist/', - '/build/' - ], - verbose: true, -}; diff --git a/test-validation.js b/test-validation.js deleted file mode 100644 index 74b56ee..0000000 --- a/test-validation.js +++ /dev/null @@ -1,53 +0,0 @@ -// Simple test script to demonstrate the form validation system -const { ValidationRules, validateField, validateFields } = require('./packages/core/dist/index.js'); - -async function testValidation() { - console.log('🧪 Testing ChenAIKit Form Validation System\n'); - - // Test 1: Email validation - console.log('1. Testing Email Validation:'); - const emailError = await validateField('invalid-email', ValidationRules.email()); - console.log(` Invalid email: "${emailError}"`); - - const validEmail = await validateField('test@example.com', ValidationRules.email()); - console.log(` Valid email: "${validEmail || 'No error'}"`); - - // Test 2: Stellar address validation - console.log('\n2. Testing Stellar Address Validation:'); - const invalidStellar = await validateField('invalid-address', ValidationRules.stellarAddress()); - console.log(` Invalid Stellar address: "${invalidStellar}"`); - - const validStellar = await validateField('GABCDEFGHIJKLMNOPQRSTUVWXYZ123456789012345678901234567890', ValidationRules.stellarAddress()); - console.log(` Valid Stellar address: "${validStellar || 'No error'}"`); - - // Test 3: Number validation - console.log('\n3. Testing Number Validation:'); - const negativeNumber = await validateField('-10', ValidationRules.positiveNumber()); - console.log(` Negative number: "${negativeNumber}"`); - - const validNumber = await validateField('100', ValidationRules.positiveNumber()); - console.log(` Valid number: "${validNumber || 'No error'}"`); - - // Test 4: Multiple field validation - console.log('\n4. Testing Multiple Field Validation:'); - const formData = { - email: 'invalid-email', - stellarAddress: 'invalid-address', - amount: '-50' - }; - - const validationRules = { - email: ValidationRules.email(), - stellarAddress: ValidationRules.stellarAddress(), - amount: ValidationRules.positiveNumber() - }; - - const errors = await validateFields(formData, validationRules); - console.log(' Form errors:', errors); - - console.log('\n✅ All validation tests completed!'); - console.log('\n🌐 To test the full React application, visit: http://localhost:3001'); -} - -// Run the test -testValidation().catch(console.error); From ec179d2e0ed1a8e9c06619556a8ddcb807d62fa7 Mon Sep 17 00:00:00 2001 From: rahull-prog <241771050+kydrahul@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:10:10 +0530 Subject: [PATCH 5/5] fix: instantiate helmet middleware once to avoid per-request overhead --- backend/src/middleware/security.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/middleware/security.ts b/backend/src/middleware/security.ts index 18d417a..aa80036 100644 --- a/backend/src/middleware/security.ts +++ b/backend/src/middleware/security.ts @@ -99,9 +99,10 @@ export const applySecurityMiddleware = (app: Application): void => { }), ); + const defaultHelmet = helmetMiddleware(); app.use((req, res, next) => { if (req.path.startsWith("/api-docs")) return next(); - return helmetMiddleware()(req, res, next); + return defaultHelmet(req, res, next); }); app.use(corsMiddleware()); };