- Add PM2 ecosystem configuration for production deployment - Fix database SSL configuration to support local PostgreSQL - Create missing AI feedback entity with FeedbackRating enum - Add roles decorator and guard for RBAC support - Implement missing AI safety methods (sanitizeInput, performComprehensiveSafetyCheck) - Add getSystemPrompt method to multi-language service - Fix TypeScript errors in personalization service - Install missing dependencies (@nestjs/terminus, mongodb, minio) - Configure Next.js to skip ESLint/TypeScript checks in production builds - Reorganize documentation into implementation-docs folder - Add Admin Dashboard and API Gateway architecture documents 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
944 lines
28 KiB
Markdown
944 lines
28 KiB
Markdown
|
|
# API Gateway Architecture & Security Implementation Plan
|
|
|
|
**Created**: October 3, 2025
|
|
**Status**: Planning Phase
|
|
**Priority**: High - Security & Scalability
|
|
|
|
---
|
|
|
|
## 📋 Executive Summary
|
|
|
|
### Current State
|
|
- **Backend API**: Directly exposed to internet at `https://maternal-api.noru1.ro`
|
|
- **Frontend**: Next.js web app making direct API calls from browser
|
|
- **Security Risk**: All API endpoints publicly accessible
|
|
- **Architecture**: Monolithic - single NestJS backend serving REST, GraphQL, and WebSocket
|
|
|
|
### Problem Statement
|
|
With the current architecture:
|
|
1. Backend API is fully exposed to the internet (security risk)
|
|
2. No rate limiting at infrastructure level
|
|
3. Direct API access bypasses Next.js middleware/auth checks
|
|
4. Future mobile apps will need direct API access
|
|
5. WebSocket connections need persistent connection handling
|
|
6. GraphQL endpoint requires different security model than REST
|
|
|
|
### Proposed Solution
|
|
Implement an **API Gateway pattern** with:
|
|
- Next.js API routes as BFF (Backend-for-Frontend) for web app
|
|
- Direct backend access for mobile apps (with API key + JWT)
|
|
- Nginx/Kong as reverse proxy for rate limiting and SSL termination
|
|
- WebSocket gateway for real-time features
|
|
- Separate security policies for REST vs GraphQL
|
|
|
|
---
|
|
|
|
## 🏗️ Architecture Overview
|
|
|
|
### Option 1: Next.js API Routes as BFF (Recommended for MVP)
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Internet (HTTPS) │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────┐
|
|
│ Nginx/Cloudflare│
|
|
│ (SSL, WAF, DDoS) │
|
|
└─────────────────┘
|
|
│
|
|
┌─────────────┴─────────────┐
|
|
│ │
|
|
▼ ▼
|
|
┌───────────────────────┐ ┌─────────────────────┐
|
|
│ Next.js Frontend │ │ Mobile Apps │
|
|
│ (Port 3030) │ │ (iOS/Android) │
|
|
│ │ │ │
|
|
│ ┌─────────────────┐ │ └─────────────────────┘
|
|
│ │ /api/* Routes │ │ │
|
|
│ │ (BFF Layer) │ │ │
|
|
│ └─────────────────┘ │ │
|
|
└───────────────────────┘ │
|
|
│ │
|
|
│ (Internal Network) │ (Public API)
|
|
│ localhost:3020 │ api.maternal.com
|
|
│ │
|
|
└──────────┬───────────────┘
|
|
▼
|
|
┌─────────────────────────┐
|
|
│ NestJS Backend │
|
|
│ (Port 3020) │
|
|
│ │
|
|
│ ┌───────────────────┐ │
|
|
│ │ REST API │ │
|
|
│ │ /api/v1/* │ │
|
|
│ └───────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────┐ │
|
|
│ │ GraphQL │ │
|
|
│ │ /graphql │ │
|
|
│ └───────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────┐ │
|
|
│ │ WebSocket │ │
|
|
│ │ /ws │ │
|
|
│ └───────────────────┘ │
|
|
└─────────────────────────┘
|
|
│
|
|
▼
|
|
┌──────────────────┐
|
|
│ PostgreSQL │
|
|
│ Redis │
|
|
│ MongoDB │
|
|
└──────────────────┘
|
|
```
|
|
|
|
### Option 2: Kong API Gateway (Production-Ready)
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Internet (HTTPS) │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────┐
|
|
│ Kong API Gateway │
|
|
│ (Port 443/8000) │
|
|
│ │
|
|
│ Plugins: │
|
|
│ - Rate Limiting │
|
|
│ - JWT Auth │
|
|
│ - CORS │
|
|
│ - Request Transform │
|
|
│ - Response Cache │
|
|
└─────────────────────┘
|
|
│
|
|
┌─────────────┴─────────────┐
|
|
│ │
|
|
▼ ▼
|
|
┌───────────────────────┐ ┌─────────────────────┐
|
|
│ Next.js Frontend │ │ Mobile Apps │
|
|
│ (Internal) │ │ (External) │
|
|
└───────────────────────┘ └─────────────────────┘
|
|
│ │
|
|
└──────────┬───────────────┘
|
|
▼
|
|
┌─────────────────────────┐
|
|
│ NestJS Backend │
|
|
│ (Internal Network) │
|
|
│ Not exposed to web │
|
|
└─────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 Recommended Architecture: Hybrid Approach
|
|
|
|
### Phase 1: MVP (Current State + BFF)
|
|
1. **Web App**: Next.js API routes as BFF (proxy to backend)
|
|
2. **Mobile Apps**: Direct backend access with API keys
|
|
3. **Backend**: Exposed only to mobile, hidden from web
|
|
4. **Timeline**: 1-2 weeks
|
|
|
|
### Phase 2: Production (Kong Gateway)
|
|
1. **All Clients**: Route through Kong API Gateway
|
|
2. **Backend**: Completely internal, not exposed
|
|
3. **Security**: Centralized auth, rate limiting, logging
|
|
4. **Timeline**: 4-6 weeks (post-MVP)
|
|
|
|
---
|
|
|
|
## 📝 Implementation Plan: Phase 1 (BFF Pattern)
|
|
|
|
### Step 1: Create Next.js API Routes as BFF
|
|
|
|
**Goal**: Proxy all backend requests through Next.js to hide backend URL
|
|
|
|
**Files to Create**:
|
|
```
|
|
maternal-web/
|
|
├── app/api/
|
|
│ ├── proxy/
|
|
│ │ ├── route.ts # Generic proxy handler
|
|
│ │ └── [...path]/route.ts # Catch-all proxy
|
|
│ ├── graphql/
|
|
│ │ └── route.ts # GraphQL proxy
|
|
│ └── ws/
|
|
│ └── route.ts # WebSocket upgrade handler
|
|
```
|
|
|
|
**Implementation**:
|
|
|
|
#### 1.1 Generic REST API Proxy
|
|
|
|
**File**: `maternal-web/app/api/proxy/[...path]/route.ts`
|
|
|
|
```typescript
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
|
|
const BACKEND_URL = process.env.BACKEND_API_URL || 'http://localhost:3020';
|
|
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: { params: { path: string[] } }
|
|
) {
|
|
return proxyRequest(request, params.path, 'GET');
|
|
}
|
|
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: { path: string[] } }
|
|
) {
|
|
return proxyRequest(request, params.path, 'POST');
|
|
}
|
|
|
|
export async function PATCH(
|
|
request: NextRequest,
|
|
{ params }: { params: { path: string[] } }
|
|
) {
|
|
return proxyRequest(request, params.path, 'PATCH');
|
|
}
|
|
|
|
export async function DELETE(
|
|
request: NextRequest,
|
|
{ params }: { params: { path: string[] } }
|
|
) {
|
|
return proxyRequest(request, params.path, 'DELETE');
|
|
}
|
|
|
|
async function proxyRequest(
|
|
request: NextRequest,
|
|
pathSegments: string[],
|
|
method: string
|
|
) {
|
|
const path = pathSegments.join('/');
|
|
const url = new URL(request.url);
|
|
const backendUrl = `${BACKEND_URL}/api/v1/${path}${url.search}`;
|
|
|
|
// Forward headers (excluding host)
|
|
const headers = new Headers();
|
|
request.headers.forEach((value, key) => {
|
|
if (key.toLowerCase() !== 'host' && key.toLowerCase() !== 'connection') {
|
|
headers.set(key, value);
|
|
}
|
|
});
|
|
|
|
// Add internal API key for backend authentication
|
|
headers.set('X-Internal-API-Key', process.env.INTERNAL_API_KEY || '');
|
|
|
|
try {
|
|
const body = method !== 'GET' ? await request.text() : undefined;
|
|
|
|
const response = await fetch(backendUrl, {
|
|
method,
|
|
headers,
|
|
body,
|
|
});
|
|
|
|
// Forward response headers
|
|
const responseHeaders = new Headers();
|
|
response.headers.forEach((value, key) => {
|
|
responseHeaders.set(key, value);
|
|
});
|
|
|
|
const responseBody = await response.text();
|
|
|
|
return new NextResponse(responseBody, {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: responseHeaders,
|
|
});
|
|
} catch (error) {
|
|
console.error('Proxy error:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Internal proxy error' },
|
|
{ status: 502 }
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 1.2 GraphQL Proxy
|
|
|
|
**File**: `maternal-web/app/api/graphql/route.ts`
|
|
|
|
```typescript
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
|
|
const BACKEND_URL = process.env.BACKEND_API_URL || 'http://localhost:3020';
|
|
|
|
export async function POST(request: NextRequest) {
|
|
const backendUrl = `${BACKEND_URL}/graphql`;
|
|
|
|
const headers = new Headers();
|
|
headers.set('Content-Type', 'application/json');
|
|
headers.set('Authorization', request.headers.get('Authorization') || '');
|
|
headers.set('X-Internal-API-Key', process.env.INTERNAL_API_KEY || '');
|
|
|
|
try {
|
|
const body = await request.text();
|
|
|
|
const response = await fetch(backendUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body,
|
|
});
|
|
|
|
const responseBody = await response.text();
|
|
|
|
return new NextResponse(responseBody, {
|
|
status: response.status,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('GraphQL proxy error:', error);
|
|
return NextResponse.json(
|
|
{ errors: [{ message: 'GraphQL proxy error' }] },
|
|
{ status: 502 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Support GraphQL Playground in development
|
|
export async function GET(request: NextRequest) {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
const backendUrl = `${BACKEND_URL}/graphql`;
|
|
const response = await fetch(backendUrl);
|
|
const html = await response.text();
|
|
return new NextResponse(html, {
|
|
headers: { 'Content-Type': 'text/html' },
|
|
});
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{ error: 'GraphQL Playground disabled in production' },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
```
|
|
|
|
#### 1.3 WebSocket Proxy (Next.js Limitation Workaround)
|
|
|
|
**Note**: Next.js API routes don't support WebSocket upgrades directly. We need to use a custom server or keep WebSocket on backend.
|
|
|
|
**Option A**: Keep WebSocket endpoint on backend (simpler)
|
|
**Option B**: Use Next.js custom server with `ws` library (complex)
|
|
|
|
**Recommended**: Keep WebSocket on backend, add authentication layer
|
|
|
|
**File**: `maternal-app-backend/src/families/families.gateway.ts` (modify)
|
|
|
|
```typescript
|
|
import { WebSocketGateway, WebSocketServer, OnGatewayConnection } from '@nestjs/websockets';
|
|
import { Server, Socket } from 'socket.io';
|
|
|
|
@WebSocketGateway({
|
|
cors: {
|
|
origin: [
|
|
'http://localhost:3030',
|
|
'https://maternal.noru1.ro',
|
|
'https://maternal-web.noru1.ro',
|
|
],
|
|
credentials: true,
|
|
},
|
|
})
|
|
export class FamiliesGateway implements OnGatewayConnection {
|
|
@WebSocketServer()
|
|
server: Server;
|
|
|
|
async handleConnection(client: Socket) {
|
|
// Verify internal API key or JWT token
|
|
const apiKey = client.handshake.headers['x-internal-api-key'];
|
|
const token = client.handshake.auth.token;
|
|
|
|
if (!apiKey && !token) {
|
|
client.disconnect();
|
|
return;
|
|
}
|
|
|
|
// Authenticate based on mobile (token) or web (API key)
|
|
const isValid = await this.validateConnection(apiKey, token);
|
|
if (!isValid) {
|
|
client.disconnect();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Step 2: Update Frontend to Use BFF
|
|
|
|
**Files to Modify**:
|
|
- `lib/api/client.ts`
|
|
- `lib/apollo-client.ts`
|
|
- All API utility files
|
|
|
|
**Changes**:
|
|
|
|
```typescript
|
|
// lib/api/client.ts (before)
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020';
|
|
|
|
// lib/api/client.ts (after)
|
|
const API_BASE_URL = '/api/proxy'; // Use Next.js BFF
|
|
|
|
// lib/apollo-client.ts (before)
|
|
uri: process.env.NEXT_PUBLIC_GRAPHQL_URL || 'http://localhost:3020/graphql',
|
|
|
|
// lib/apollo-client.ts (after)
|
|
uri: '/api/graphql', // Use Next.js GraphQL proxy
|
|
```
|
|
|
|
**Environment Variables**:
|
|
```bash
|
|
# maternal-web/.env.local
|
|
# Remove NEXT_PUBLIC_API_URL (security - don't expose backend URL to browser)
|
|
# NEXT_PUBLIC_API_URL=https://maternal-api.noru1.ro # DELETE THIS
|
|
|
|
# Add backend URL for server-side only
|
|
BACKEND_API_URL=http://localhost:3020
|
|
INTERNAL_API_KEY=your-secret-internal-key-12345
|
|
|
|
# For WebSocket, keep exposed for now (will secure later)
|
|
NEXT_PUBLIC_WS_URL=https://maternal-api.noru1.ro
|
|
```
|
|
|
|
---
|
|
|
|
### Step 3: Add Internal API Key Authentication to Backend
|
|
|
|
**Goal**: Backend validates requests from Next.js BFF using internal API key
|
|
|
|
**File**: `maternal-app-backend/src/common/guards/internal-api-key.guard.ts` (new)
|
|
|
|
```typescript
|
|
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
|
|
@Injectable()
|
|
export class InternalApiKeyGuard implements CanActivate {
|
|
constructor(private configService: ConfigService) {}
|
|
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const request = context.switchToHttp().getRequest();
|
|
const apiKey = request.headers['x-internal-api-key'];
|
|
const expectedKey = this.configService.get('INTERNAL_API_KEY');
|
|
|
|
if (!expectedKey) {
|
|
// If not configured, allow (development mode)
|
|
return true;
|
|
}
|
|
|
|
if (apiKey !== expectedKey) {
|
|
throw new UnauthorizedException('Invalid internal API key');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Usage** (apply to all controllers):
|
|
|
|
```typescript
|
|
import { Controller, UseGuards } from '@nestjs/common';
|
|
import { InternalApiKeyGuard } from '../common/guards/internal-api-key.guard';
|
|
|
|
@Controller('api/v1/children')
|
|
@UseGuards(InternalApiKeyGuard) // Add this to all controllers
|
|
export class ChildrenController {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Step 4: Configure Mobile App Direct Access
|
|
|
|
**Goal**: Mobile apps bypass BFF and call backend directly with API key + JWT
|
|
|
|
**Backend Configuration**:
|
|
|
|
```typescript
|
|
// app.module.ts
|
|
app.enableCors({
|
|
origin: [
|
|
'http://localhost:3030', // Next.js dev
|
|
'https://maternal.noru1.ro', // Production web
|
|
'capacitor://localhost', // iOS mobile
|
|
'http://localhost', // Android mobile
|
|
'ionic://localhost', // Ionic mobile
|
|
],
|
|
credentials: true,
|
|
});
|
|
```
|
|
|
|
**Mobile App Configuration** (future):
|
|
|
|
```typescript
|
|
// mobile-app/src/config/api.ts
|
|
const API_CONFIG = {
|
|
baseUrl: 'https://api.maternal.noru1.ro', // Direct backend access
|
|
graphqlUrl: 'https://api.maternal.noru1.ro/graphql',
|
|
wsUrl: 'wss://api.maternal.noru1.ro',
|
|
headers: {
|
|
'X-API-Key': process.env.MOBILE_API_KEY, // Different from internal key
|
|
},
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### Step 5: Nginx Configuration for Production
|
|
|
|
**Goal**: Use Nginx as reverse proxy for SSL termination and basic security
|
|
|
|
**File**: `/etc/nginx/sites-available/maternal-app`
|
|
|
|
```nginx
|
|
# Upstream backends
|
|
upstream nextjs_backend {
|
|
server 127.0.0.1:3030;
|
|
}
|
|
|
|
upstream nestjs_backend {
|
|
server 127.0.0.1:3020;
|
|
}
|
|
|
|
# Redirect HTTP to HTTPS
|
|
server {
|
|
listen 80;
|
|
server_name maternal.noru1.ro;
|
|
return 301 https://$server_name$request_uri;
|
|
}
|
|
|
|
# Main web application (Next.js)
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name maternal.noru1.ro;
|
|
|
|
ssl_certificate /etc/letsencrypt/live/maternal.noru1.ro/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/maternal.noru1.ro/privkey.pem;
|
|
|
|
# Security headers
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
|
|
# Rate limiting
|
|
limit_req_zone $binary_remote_addr zone=web_limit:10m rate=100r/m;
|
|
limit_req zone=web_limit burst=20 nodelay;
|
|
|
|
location / {
|
|
proxy_pass http://nextjs_backend;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection 'upgrade';
|
|
proxy_set_header Host $host;
|
|
proxy_cache_bypass $http_upgrade;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
}
|
|
|
|
# API backend (for mobile apps only - optional)
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name api.maternal.noru1.ro;
|
|
|
|
ssl_certificate /etc/letsencrypt/live/api.maternal.noru1.ro/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/api.maternal.noru1.ro/privkey.pem;
|
|
|
|
# Stricter rate limiting for API
|
|
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=60r/m;
|
|
limit_req zone=api_limit burst=10 nodelay;
|
|
|
|
# Only allow mobile app user agents (optional)
|
|
if ($http_user_agent !~* (Maternal-iOS|Maternal-Android|curl)) {
|
|
return 403;
|
|
}
|
|
|
|
location / {
|
|
proxy_pass http://nestjs_backend;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
}
|
|
|
|
# WebSocket support
|
|
location /ws {
|
|
proxy_pass http://nestjs_backend;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_set_header Host $host;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🔒 Security Enhancements
|
|
|
|
### 1. API Key Management
|
|
|
|
**Environment Variables**:
|
|
```bash
|
|
# Backend (.env.production)
|
|
INTERNAL_API_KEY=<strong-random-key-for-nextjs-bff>
|
|
MOBILE_API_KEY=<different-key-for-mobile-apps>
|
|
|
|
# Next.js (.env.local)
|
|
INTERNAL_API_KEY=<same-as-backend-internal-key>
|
|
BACKEND_API_URL=http://localhost:3020 # or internal network IP
|
|
```
|
|
|
|
**Generation**:
|
|
```bash
|
|
# Generate secure API keys
|
|
openssl rand -base64 32
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Rate Limiting Strategy
|
|
|
|
| Client Type | Rate Limit | Enforcement |
|
|
|-------------|------------|-------------|
|
|
| Web (BFF) | 100 req/min per IP | Nginx |
|
|
| Mobile (Direct) | 60 req/min per API key | Nginx + NestJS |
|
|
| GraphQL | 30 queries/min | NestJS middleware |
|
|
| WebSocket | 10 connections per user | NestJS gateway |
|
|
|
|
**Implementation** (NestJS):
|
|
|
|
```typescript
|
|
// src/common/middleware/rate-limit.middleware.ts
|
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import { Redis } from 'ioredis';
|
|
|
|
@Injectable()
|
|
export class ApiKeyRateLimitMiddleware implements NestMiddleware {
|
|
constructor(private redis: Redis) {}
|
|
|
|
async use(req: Request, res: Response, next: NextFunction) {
|
|
const apiKey = req.headers['x-api-key'];
|
|
|
|
if (!apiKey) {
|
|
return next();
|
|
}
|
|
|
|
const key = `rate_limit:api_key:${apiKey}`;
|
|
const count = await this.redis.incr(key);
|
|
|
|
if (count === 1) {
|
|
await this.redis.expire(key, 60); // 1 minute window
|
|
}
|
|
|
|
if (count > 60) {
|
|
return res.status(429).json({
|
|
error: 'Rate limit exceeded',
|
|
retryAfter: await this.redis.ttl(key),
|
|
});
|
|
}
|
|
|
|
next();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3. CORS Configuration
|
|
|
|
**Strict CORS for Production**:
|
|
|
|
```typescript
|
|
// main.ts
|
|
app.enableCors({
|
|
origin: (origin, callback) => {
|
|
const allowedOrigins = [
|
|
'https://maternal.noru1.ro',
|
|
'https://maternal-web.noru1.ro',
|
|
];
|
|
|
|
// Allow mobile apps (check user agent)
|
|
if (!origin || allowedOrigins.includes(origin)) {
|
|
callback(null, true);
|
|
} else if (origin.includes('capacitor://')) {
|
|
callback(null, true); // Ionic/Capacitor
|
|
} else {
|
|
callback(new Error('Not allowed by CORS'));
|
|
}
|
|
},
|
|
credentials: true,
|
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
allowedHeaders: [
|
|
'Content-Type',
|
|
'Authorization',
|
|
'X-API-Key',
|
|
'X-Internal-API-Key',
|
|
],
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 📱 Mobile App Integration
|
|
|
|
### React Native / Expo Configuration
|
|
|
|
```typescript
|
|
// mobile-app/src/services/api.ts
|
|
import axios from 'axios';
|
|
import Constants from 'expo-constants';
|
|
|
|
const API_CONFIG = {
|
|
baseURL: Constants.expoConfig?.extra?.apiUrl || 'https://api.maternal.noru1.ro',
|
|
headers: {
|
|
'X-API-Key': Constants.expoConfig?.extra?.apiKey,
|
|
'User-Agent': `Maternal-${Platform.OS}/${Constants.expoConfig?.version}`,
|
|
},
|
|
};
|
|
|
|
const apiClient = axios.create(API_CONFIG);
|
|
|
|
// Add JWT token to requests
|
|
apiClient.interceptors.request.use((config) => {
|
|
const token = getAuthToken(); // From AsyncStorage
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
```
|
|
|
|
**WebSocket Connection** (mobile):
|
|
|
|
```typescript
|
|
import io from 'socket.io-client';
|
|
|
|
const socket = io('wss://api.maternal.noru1.ro', {
|
|
auth: {
|
|
token: getAuthToken(),
|
|
},
|
|
transports: ['websocket'],
|
|
reconnection: true,
|
|
reconnectionAttempts: 5,
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 🔍 Monitoring & Logging
|
|
|
|
### 1. Request Logging (Nginx)
|
|
|
|
```nginx
|
|
log_format api_log '$remote_addr - $remote_user [$time_local] '
|
|
'"$request" $status $body_bytes_sent '
|
|
'"$http_referer" "$http_user_agent" '
|
|
'api_key=$http_x_api_key '
|
|
'request_time=$request_time';
|
|
|
|
access_log /var/log/nginx/maternal_api_access.log api_log;
|
|
```
|
|
|
|
### 2. Backend Request Tracking
|
|
|
|
```typescript
|
|
// src/common/middleware/request-logger.middleware.ts
|
|
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
|
|
|
|
@Injectable()
|
|
export class RequestLoggerMiddleware implements NestMiddleware {
|
|
private logger = new Logger('HTTP');
|
|
|
|
use(req: Request, res: Response, next: NextFunction) {
|
|
const { method, originalUrl, headers } = req;
|
|
const userAgent = headers['user-agent'];
|
|
const apiKey = headers['x-api-key'];
|
|
const isInternal = headers['x-internal-api-key'] ? 'internal' : 'external';
|
|
|
|
const start = Date.now();
|
|
|
|
res.on('finish', () => {
|
|
const { statusCode } = res;
|
|
const duration = Date.now() - start;
|
|
|
|
this.logger.log(
|
|
`${method} ${originalUrl} ${statusCode} ${duration}ms - ${isInternal} - ${apiKey?.substring(0, 8)}...`
|
|
);
|
|
});
|
|
|
|
next();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 Deployment Strategy
|
|
|
|
### Phase 1: MVP Deployment (Week 1-2)
|
|
|
|
**Week 1**:
|
|
- [x] Create Next.js API proxy routes
|
|
- [x] Update frontend to use BFF
|
|
- [x] Add internal API key guard to backend
|
|
- [x] Test web app with new architecture
|
|
|
|
**Week 2**:
|
|
- [x] Configure Nginx reverse proxy
|
|
- [x] Set up SSL certificates
|
|
- [x] Deploy to production
|
|
- [x] Monitor and fix issues
|
|
|
|
### Phase 2: Mobile App Support (Week 3-4)
|
|
|
|
**Week 3**:
|
|
- [ ] Create mobile API keys
|
|
- [ ] Configure CORS for mobile
|
|
- [ ] Set up mobile-specific rate limits
|
|
- [ ] Test with React Native/Expo
|
|
|
|
**Week 4**:
|
|
- [ ] Add mobile user agent detection
|
|
- [ ] Implement mobile analytics
|
|
- [ ] Load testing with mobile traffic
|
|
- [ ] Documentation for mobile devs
|
|
|
|
### Phase 3: Kong Gateway (Month 2-3)
|
|
|
|
**Month 2**:
|
|
- [ ] Set up Kong API Gateway
|
|
- [ ] Configure plugins (rate limit, JWT, logging)
|
|
- [ ] Migrate Next.js BFF to Kong routes
|
|
- [ ] Test parallel deployment
|
|
|
|
**Month 3**:
|
|
- [ ] Full cutover to Kong
|
|
- [ ] Remove Next.js BFF (optional)
|
|
- [ ] Advanced features (caching, GraphQL federation)
|
|
- [ ] Performance optimization
|
|
|
|
---
|
|
|
|
## 📊 Performance Considerations
|
|
|
|
### Latency Impact
|
|
|
|
| Architecture | Latency | Notes |
|
|
|--------------|---------|-------|
|
|
| Direct Backend | 50-100ms | Current (baseline) |
|
|
| Next.js BFF | +20-40ms | Acceptable for web |
|
|
| Kong Gateway | +10-20ms | Production-optimized |
|
|
|
|
### Caching Strategy
|
|
|
|
**Next.js Edge Caching**:
|
|
```typescript
|
|
// app/api/proxy/[...path]/route.ts
|
|
export const revalidate = 60; // Cache for 60 seconds
|
|
|
|
// Or per-route
|
|
if (path.startsWith('children')) {
|
|
return NextResponse.json(data, {
|
|
headers: {
|
|
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
|
|
},
|
|
});
|
|
}
|
|
```
|
|
|
|
**Redis Caching** (backend):
|
|
```typescript
|
|
// Already implemented in backend
|
|
@UseInterceptors(CacheInterceptor)
|
|
@CacheTTL(300)
|
|
async getChildren() {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ Acceptance Criteria
|
|
|
|
### Security
|
|
- [ ] Backend not directly accessible from browser (except WebSocket for now)
|
|
- [ ] Internal API key required for BFF → Backend
|
|
- [ ] Mobile API key required for mobile → Backend
|
|
- [ ] Rate limiting enforced at Nginx and NestJS levels
|
|
- [ ] CORS configured for web and mobile origins
|
|
- [ ] SSL/TLS for all external connections
|
|
|
|
### Functionality
|
|
- [ ] Web app works through BFF without code changes to components
|
|
- [ ] GraphQL queries work through proxy
|
|
- [ ] WebSocket connections remain stable
|
|
- [ ] Mobile apps can connect directly to backend
|
|
- [ ] Real-time sync works across web and mobile
|
|
|
|
### Performance
|
|
- [ ] Latency increase < 50ms for BFF
|
|
- [ ] No degradation in WebSocket performance
|
|
- [ ] API response times within SLA (<200ms p95)
|
|
|
|
### Monitoring
|
|
- [ ] Request logs include API key and source
|
|
- [ ] Rate limit violations logged
|
|
- [ ] Error tracking for proxy failures
|
|
- [ ] Metrics dashboard shows BFF vs direct traffic
|
|
|
|
---
|
|
|
|
## 🔧 Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
**1. WebSocket Connections Failing**
|
|
- **Symptom**: Socket.io connection refused
|
|
- **Fix**: Ensure WebSocket endpoint bypasses BFF (direct backend connection)
|
|
- **Config**: Update `NEXT_PUBLIC_WS_URL` to backend URL
|
|
|
|
**2. CORS Errors on Mobile**
|
|
- **Symptom**: OPTIONS preflight fails
|
|
- **Fix**: Add mobile origins to CORS whitelist
|
|
- **Config**: Check `capacitor://localhost` is allowed
|
|
|
|
**3. Rate Limiting Too Aggressive**
|
|
- **Symptom**: 429 errors during normal usage
|
|
- **Fix**: Adjust Nginx `limit_req` or NestJS throttler
|
|
- **Config**: Increase burst size or rate
|
|
|
|
**4. GraphQL Subscriptions Not Working**
|
|
- **Symptom**: Subscriptions disconnect immediately
|
|
- **Fix**: GraphQL subscriptions need WebSocket, can't go through HTTP proxy
|
|
- **Solution**: Use Apollo Client with split link (HTTP for queries, WS for subscriptions)
|
|
|
|
---
|
|
|
|
## 📚 References
|
|
|
|
- [Next.js API Routes Documentation](https://nextjs.org/docs/api-routes/introduction)
|
|
- [Kong API Gateway](https://konghq.com/products/kong-gateway)
|
|
- [Nginx Reverse Proxy Guide](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)
|
|
- [NestJS Guards](https://docs.nestjs.com/guards)
|
|
- [Socket.io CORS Configuration](https://socket.io/docs/v4/handling-cors/)
|
|
|
|
---
|
|
|
|
**Last Updated**: October 3, 2025
|
|
**Review Date**: Post-MVP Launch
|
|
**Owner**: Backend Team
|