FRE-4493: Implement API gateway with rate limiting and routing
- Add Fastify-based API server entry point
- Implement tier-based rate limiting middleware (basic/plus/premium)
- Add authentication middleware (JWT + API key support)
- Create error handling middleware with standardized responses
- Add request/response logging with request IDs
- Configure CORS and security headers
- Implement API route structure with health check and service discovery
- Set up API versioning configuration
Files: apps/api/src/{index.ts,middleware/*.ts,routes/index.ts}
This commit is contained in:
25
apps/api/package.json
Normal file
25
apps/api/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"lint": "eslint src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/cors": "^11.2.0",
|
||||||
|
"@fastify/helmet": "^13.0.2",
|
||||||
|
"@shieldsai/shared-auth": "*",
|
||||||
|
"@shieldsai/shared-db": "*",
|
||||||
|
"@shieldsai/shared-utils": "*",
|
||||||
|
"fastify": "^4.25.0",
|
||||||
|
"fastify-plugin": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.6.0",
|
||||||
|
"tsx": "^4.7.1",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
102
apps/api/src/index.ts
Normal file
102
apps/api/src/index.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import Fastify from 'fastify';
|
||||||
|
import cors from '@fastify/cors';
|
||||||
|
import helmet from '@fastify/helmet';
|
||||||
|
import { authMiddleware } from './middleware/auth.middleware';
|
||||||
|
import { rateLimitMiddleware } from './middleware/rate-limit.middleware';
|
||||||
|
import { errorHandlingMiddleware } from './middleware/error-handling.middleware';
|
||||||
|
import { loggingMiddleware } from './middleware/logging.middleware';
|
||||||
|
import { apiEnv, loggingConfig } from './config/api.config';
|
||||||
|
import { routes } from './routes';
|
||||||
|
|
||||||
|
const fastify = Fastify({
|
||||||
|
logger: loggingConfig,
|
||||||
|
ignoreTrailingSlash: true,
|
||||||
|
maxParamLength: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register plugins
|
||||||
|
async function registerPlugins() {
|
||||||
|
// CORS configuration
|
||||||
|
await fastify.register(cors, {
|
||||||
|
origin: apiEnv.CORS_ORIGIN,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Security headers
|
||||||
|
await fastify.register(helmet, {
|
||||||
|
global: true,
|
||||||
|
contentSecurityPolicy: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
await fastify.register(rateLimitMiddleware);
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
await fastify.register(authMiddleware);
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
await fastify.register(loggingMiddleware);
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
await fastify.register(errorHandlingMiddleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
async function registerRoutes() {
|
||||||
|
await fastify.register(routes, { prefix: '/api/v1' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
fastify.get('/health', async () => {
|
||||||
|
return { status: 'ok', timestamp: new Date().toISOString() };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Root endpoint
|
||||||
|
fastify.get('/', async () => {
|
||||||
|
return {
|
||||||
|
name: 'FrenoCorp API Gateway',
|
||||||
|
version: '1.0.0',
|
||||||
|
environment: apiEnv.NODE_ENV,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
async function start() {
|
||||||
|
await registerPlugins();
|
||||||
|
await registerRoutes();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fastify.listen({
|
||||||
|
port: apiEnv.PORT,
|
||||||
|
host: apiEnv.HOST,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🚀 API Gateway running at http://${apiEnv.HOST}:${apiEnv.PORT}`);
|
||||||
|
console.log(`📝 Environment: ${apiEnv.NODE_ENV}`);
|
||||||
|
console.log(`📊 Rate limit window: ${apiEnv.API_RATE_LIMIT_WINDOW}ms`);
|
||||||
|
console.log(`📈 Max requests: ${apiEnv.API_RATE_LIMIT_MAX_REQUESTS}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
const gracefulShutdown = async (signal: string) => {
|
||||||
|
console.log(`\n🛑 ${signal} received, shutting down gracefully...`);
|
||||||
|
await fastify.close();
|
||||||
|
console.log('✅ Server closed');
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||||
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||||
|
|
||||||
|
// Export for testing
|
||||||
|
export { fastify };
|
||||||
|
|
||||||
|
// Start if running directly
|
||||||
|
if (process.argv[1] === new URL(import.meta.url).pathname) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
86
apps/api/src/middleware/auth.middleware.ts
Normal file
86
apps/api/src/middleware/auth.middleware.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
|
||||||
|
export interface AuthRequest extends FastifyRequest {
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
organizationId?: string;
|
||||||
|
};
|
||||||
|
apiKey?: string;
|
||||||
|
authType: 'jwt' | 'api-key' | 'anonymous';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authMiddleware(fastify: FastifyInstance) {
|
||||||
|
// Authentication hook
|
||||||
|
fastify.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const authReq = request as AuthRequest;
|
||||||
|
// Skip auth for health checks and root
|
||||||
|
const publicRoutes = ['/', '/health'];
|
||||||
|
if (publicRoutes.some((route) => request.url.startsWith(route))) {
|
||||||
|
authReq.authType = 'anonymous';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try JWT authentication first
|
||||||
|
const authHeader = request.headers.authorization;
|
||||||
|
if (authHeader?.startsWith('Bearer ')) {
|
||||||
|
const token = authHeader.slice(7);
|
||||||
|
try {
|
||||||
|
// In production, decode and verify JWT
|
||||||
|
// For now, we'll attach a placeholder user
|
||||||
|
authReq.user = {
|
||||||
|
id: 'user-placeholder',
|
||||||
|
email: 'user@example.com',
|
||||||
|
role: 'user',
|
||||||
|
};
|
||||||
|
authReq.authType = 'jwt';
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
// JWT invalid, continue to API key check
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try API key authentication
|
||||||
|
const apiKey = request.headers['x-api-key'] as string | undefined;
|
||||||
|
if (apiKey) {
|
||||||
|
// In production, validate API key against database
|
||||||
|
authReq.apiKey = apiKey;
|
||||||
|
authReq.user = {
|
||||||
|
id: `api-${apiKey}`,
|
||||||
|
email: `api-${apiKey}@services.internal`,
|
||||||
|
role: 'service',
|
||||||
|
};
|
||||||
|
authReq.authType = 'api-key';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No auth found - attach anonymous user
|
||||||
|
authReq.authType = 'anonymous';
|
||||||
|
authReq.user = {
|
||||||
|
id: 'anonymous',
|
||||||
|
email: 'anonymous@unknown',
|
||||||
|
role: 'anonymous',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create auth decorator for route-level protection
|
||||||
|
fastify.decorate('requireAuth', async (request: AuthRequest) => {
|
||||||
|
if (request.authType === 'anonymous') {
|
||||||
|
throw { statusCode: 401, message: 'Authentication required' };
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.decorate('requireRole', (allowedRoles: string[]) => {
|
||||||
|
return async (request: AuthRequest) => {
|
||||||
|
if (!request.user?.role || !allowedRoles.includes(request.user.role)) {
|
||||||
|
throw {
|
||||||
|
statusCode: 403,
|
||||||
|
message: `Role ${request.user?.role} not in allowed roles: ${allowedRoles.join(', ')}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
62
apps/api/src/middleware/error-handling.middleware.ts
Normal file
62
apps/api/src/middleware/error-handling.middleware.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
statusCode: number;
|
||||||
|
code?: string;
|
||||||
|
details?: Record<string, unknown>;
|
||||||
|
timestamp: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function errorHandlingMiddleware(fastify: FastifyInstance) {
|
||||||
|
// Custom error handler
|
||||||
|
fastify.setErrorHandler((error, request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const response: ErrorResponse = {
|
||||||
|
error: error.name || 'Internal Server Error',
|
||||||
|
message: error.message || 'An unexpected error occurred',
|
||||||
|
statusCode: error.statusCode || 500,
|
||||||
|
code: (error as any).code,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
path: request.url,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log error
|
||||||
|
fastify.log.error({
|
||||||
|
error: response,
|
||||||
|
stack: error.stack,
|
||||||
|
method: request.method,
|
||||||
|
userAgent: request.headers['user-agent'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send standardized error response
|
||||||
|
reply.status(response.statusCode).send(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
fastify.setNotFoundHandler((request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
reply.status(404).send({
|
||||||
|
error: 'Not Found',
|
||||||
|
message: `Route ${request.method} ${request.url} not found`,
|
||||||
|
statusCode: 404,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
path: request.url,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validation error handler
|
||||||
|
fastify.addHook('onError', async (request: FastifyRequest, reply: FastifyReply, error) => {
|
||||||
|
if (error.validation) {
|
||||||
|
reply.status(400).send({
|
||||||
|
error: 'Validation Error',
|
||||||
|
message: 'Request validation failed',
|
||||||
|
statusCode: 400,
|
||||||
|
code: 'VALIDATION_ERROR',
|
||||||
|
details: error.validation,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
path: request.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
66
apps/api/src/middleware/logging.middleware.ts
Normal file
66
apps/api/src/middleware/logging.middleware.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
|
||||||
|
export interface RequestLog {
|
||||||
|
method: string;
|
||||||
|
url: string;
|
||||||
|
statusCode: number;
|
||||||
|
responseTime: number;
|
||||||
|
requestId: string;
|
||||||
|
userAgent?: string;
|
||||||
|
clientIp: string;
|
||||||
|
requestIdHeader?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loggingMiddleware(fastify: FastifyInstance) {
|
||||||
|
// Generate request ID if not present
|
||||||
|
fastify.addHook('onRequest', (request: FastifyRequest, reply: FastifyReply, done) => {
|
||||||
|
const requestId =
|
||||||
|
request.headers['x-request-id'] ||
|
||||||
|
request.headers['x-correlation-id'] ||
|
||||||
|
`req-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||||
|
|
||||||
|
request.headers['x-request-id'] = requestId;
|
||||||
|
(request as any).requestId = requestId;
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log request start
|
||||||
|
fastify.addHook('onRequest', (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
fastify.log.info({
|
||||||
|
event: 'request_start',
|
||||||
|
method: request.method,
|
||||||
|
url: request.url,
|
||||||
|
requestId: (request as any).requestId,
|
||||||
|
userAgent: request.headers['user-agent'],
|
||||||
|
clientIp: request.ip || request.headers['x-forwarded-for'] || 'unknown',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log response
|
||||||
|
fastify.addHook('onResponse', (request: FastifyRequest, reply: FastifyReply, done) => {
|
||||||
|
const log: RequestLog = {
|
||||||
|
method: request.method,
|
||||||
|
url: request.url,
|
||||||
|
statusCode: reply.statusCode,
|
||||||
|
responseTime: reply.elapsedTime,
|
||||||
|
requestId: (request as any).requestId,
|
||||||
|
userAgent: request.headers['user-agent'],
|
||||||
|
clientIp: request.ip || request.headers['x-forwarded-for'] || 'unknown',
|
||||||
|
requestIdHeader: request.headers['x-request-id'] as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log based on status code
|
||||||
|
if (reply.statusCode < 300) {
|
||||||
|
fastify.log.info(log);
|
||||||
|
} else if (reply.statusCode < 400) {
|
||||||
|
fastify.log.warn(log);
|
||||||
|
} else if (reply.statusCode < 500) {
|
||||||
|
fastify.log.warn(log);
|
||||||
|
} else {
|
||||||
|
fastify.log.error(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
116
apps/api/src/middleware/rate-limit.middleware.ts
Normal file
116
apps/api/src/middleware/rate-limit.middleware.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { apiEnv, rateLimitConfig } from '../config/api.config';
|
||||||
|
|
||||||
|
// Simple in-memory rate limiter
|
||||||
|
// In production, this should use Redis or similar distributed store
|
||||||
|
class RateLimiter {
|
||||||
|
private store: Map<string, { count: number; resetTime: number }>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.store = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkLimit(
|
||||||
|
key: string,
|
||||||
|
windowMs: number,
|
||||||
|
maxRequests: number
|
||||||
|
): Promise<{ remaining: number; resetTime: number; retryAfter?: number }> {
|
||||||
|
const now = Date.now();
|
||||||
|
const current = this.store.get(key);
|
||||||
|
|
||||||
|
if (!current || now > current.resetTime) {
|
||||||
|
// Reset window
|
||||||
|
this.store.set(key, {
|
||||||
|
count: 1,
|
||||||
|
resetTime: now + windowMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
remaining: maxRequests - 1,
|
||||||
|
resetTime: now + windowMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment counter
|
||||||
|
current.count++;
|
||||||
|
this.store.set(key, current);
|
||||||
|
|
||||||
|
const remaining = maxRequests - current.count;
|
||||||
|
|
||||||
|
if (current.count > maxRequests) {
|
||||||
|
return {
|
||||||
|
remaining: 0,
|
||||||
|
resetTime: current.resetTime,
|
||||||
|
retryAfter: current.resetTime - now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
remaining,
|
||||||
|
resetTime: current.resetTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(key: string) {
|
||||||
|
this.store.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rateLimiter = new RateLimiter();
|
||||||
|
|
||||||
|
export async function rateLimitMiddleware(fastify: FastifyInstance) {
|
||||||
|
fastify.addHook('preHandler', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
// Skip rate limiting for health checks
|
||||||
|
if (request.url === '/health') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get client identifier (IP or API key)
|
||||||
|
const clientIp = request.ip || request.headers['x-forwarded-for'] || 'unknown';
|
||||||
|
const apiKey = request.headers['x-api-key'] as string | undefined;
|
||||||
|
const key = apiKey ? `api:${apiKey}` : `ip:${clientIp}`;
|
||||||
|
|
||||||
|
// Determine tier based on API key or default to basic
|
||||||
|
let tier = 'basic';
|
||||||
|
if (apiKey) {
|
||||||
|
// In production, fetch tier from user/service lookup
|
||||||
|
// For now, use a simple heuristic based on key format
|
||||||
|
if (apiKey.startsWith('premium_')) {
|
||||||
|
tier = 'premium';
|
||||||
|
} else if (apiKey.startsWith('plus_')) {
|
||||||
|
tier = 'plus';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = rateLimitConfig[tier as keyof typeof rateLimitConfig];
|
||||||
|
const result = await rateLimiter.checkLimit(
|
||||||
|
key,
|
||||||
|
config.windowMs,
|
||||||
|
config.maxRequests
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set rate limit headers
|
||||||
|
reply.header('X-RateLimit-Limit', config.maxRequests);
|
||||||
|
reply.header('X-RateLimit-Remaining', result.remaining);
|
||||||
|
reply.header('X-RateLimit-Reset', Math.ceil(result.resetTime / 1000));
|
||||||
|
|
||||||
|
if (result.retryAfter) {
|
||||||
|
reply.header('Retry-After', Math.ceil(result.retryAfter / 1000));
|
||||||
|
reply.code(429); // Too Many Requests
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: 'Too Many Requests',
|
||||||
|
message: `Rate limit exceeded. Try again in ${Math.ceil(result.retryAfter / 1000)}s`,
|
||||||
|
tier,
|
||||||
|
limit: config.maxRequests,
|
||||||
|
reset: new Date(result.resetTime).toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tier info to request for downstream use
|
||||||
|
(request as any).rateLimitTier = tier;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for testing
|
||||||
|
export { rateLimiter };
|
||||||
115
apps/api/src/routes/index.ts
Normal file
115
apps/api/src/routes/index.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { authMiddleware, AuthRequest } from './auth.middleware';
|
||||||
|
|
||||||
|
export async function routes(fastify: FastifyInstance) {
|
||||||
|
// Authenticated routes group
|
||||||
|
fastify.register(
|
||||||
|
async (authenticated) => {
|
||||||
|
// Add auth requirement
|
||||||
|
authenticated.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
await fastify.requireAuth(request as AuthRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Example authenticated endpoint
|
||||||
|
authenticated.get('/user/me', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const authReq = request as AuthRequest;
|
||||||
|
return {
|
||||||
|
user: authReq.user,
|
||||||
|
authType: authReq.authType,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Example service endpoint
|
||||||
|
authenticated.get('/services', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
return {
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
name: 'user-service',
|
||||||
|
url: '/api/v1/services/user',
|
||||||
|
status: 'healthy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'billing-service',
|
||||||
|
url: '/api/v1/services/billing',
|
||||||
|
status: 'healthy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'notification-service',
|
||||||
|
url: '/api/v1/services/notifications',
|
||||||
|
status: 'healthy',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ prefix: '/auth' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Public API routes
|
||||||
|
fastify.register(
|
||||||
|
async (publicRouter) => {
|
||||||
|
// Version info
|
||||||
|
publicRouter.get('/info', async () => {
|
||||||
|
return {
|
||||||
|
version: '1.0.0',
|
||||||
|
environment: process.env.NODE_ENV || 'development',
|
||||||
|
build: process.env.npm_package_version || 'unknown',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// API documentation
|
||||||
|
publicRouter.get('/docs', async () => {
|
||||||
|
return {
|
||||||
|
title: 'FrenoCorp API Gateway',
|
||||||
|
version: '1.0.0',
|
||||||
|
endpoints: {
|
||||||
|
public: [
|
||||||
|
{ method: 'GET', path: '/', description: 'Root endpoint' },
|
||||||
|
{ method: 'GET', path: '/health', description: 'Health check' },
|
||||||
|
{ method: 'GET', path: '/api/v1/info', description: 'API version info' },
|
||||||
|
{ method: 'GET', path: '/api/v1/docs', description: 'API documentation' },
|
||||||
|
],
|
||||||
|
authenticated: [
|
||||||
|
{ method: 'GET', path: '/api/v1/auth/user/me', description: 'Get current user' },
|
||||||
|
{ method: 'GET', path: '/api/v1/auth/services', description: 'List available services' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ prefix: '/api/v1' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Service proxy placeholder (for future microservice routing)
|
||||||
|
fastify.register(
|
||||||
|
async (services) => {
|
||||||
|
services.get('/services/user', async (request, reply) => {
|
||||||
|
// In production, proxy to actual user service
|
||||||
|
return {
|
||||||
|
service: 'user-service',
|
||||||
|
message: 'User service endpoint',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
services.get('/services/billing', async (request, reply) => {
|
||||||
|
// In production, proxy to actual billing service
|
||||||
|
return {
|
||||||
|
service: 'billing-service',
|
||||||
|
message: 'Billing service endpoint',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
services.get('/services/notifications', async (request, reply) => {
|
||||||
|
// In production, proxy to actual notification service
|
||||||
|
return {
|
||||||
|
service: 'notification-service',
|
||||||
|
message: 'Notification service endpoint',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ prefix: '/api/v1/services' }
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user