From 91af8c183483078072d18b8aad12499772a74b3f Mon Sep 17 00:00:00 2001 From: Dami24-hub Date: Sat, 25 Apr 2026 14:53:52 +0100 Subject: [PATCH] docs: add comprehensive Swagger annotations for all non-stellar routes #290 --- backend/src/routes/analytics.js | 160 +++++++++- backend/src/routes/auth.js | 147 ++++++++- backend/src/routes/backup.js | 107 ++++++- backend/src/routes/chaos.js | 453 +++++++++++++++++++++++++++- backend/src/routes/compliance.js | 182 ++++++++++- backend/src/routes/loadTesting.js | 293 +++++++++++++++++- backend/src/routes/notifications.js | 175 ++++++++++- backend/src/routes/recovery.js | 301 +++++++++++++++++- backend/src/routes/streaming.js | 131 ++++++++ 9 files changed, 1895 insertions(+), 54 deletions(-) diff --git a/backend/src/routes/analytics.js b/backend/src/routes/analytics.js index adfc8b2..7f23422 100644 --- a/backend/src/routes/analytics.js +++ b/backend/src/routes/analytics.js @@ -6,7 +6,28 @@ const router = Router(); // ── Aggregation ─────────────────────────────────────────────────────────────── -// Daily volume + count summary +/** + * @swagger + * /api/analytics/summary/daily: + * get: + * summary: Get daily transaction volume and count summary + * tags: [Analytics] + * parameters: + * - in: query + * name: from + * schema: { type: string, format: date-time } + * - in: query + * name: to + * schema: { type: string, format: date-time } + * - in: query + * name: userId + * schema: { type: string } + * responses: + * 200: + * description: Daily summary data + * 500: + * description: Server error + */ router.get('/summary/daily', async (req, res) => { try { const { from, to, userId } = req.query; @@ -16,7 +37,28 @@ router.get('/summary/daily', async (req, res) => { } }); -// Overall totals +/** + * @swagger + * /api/analytics/summary/totals: + * get: + * summary: Get overall transaction totals + * tags: [Analytics] + * parameters: + * - in: query + * name: from + * schema: { type: string, format: date-time } + * - in: query + * name: to + * schema: { type: string, format: date-time } + * - in: query + * name: userId + * schema: { type: string } + * responses: + * 200: + * description: Totals data + * 500: + * description: Server error + */ router.get('/summary/totals', async (req, res) => { try { const { from, to, userId } = req.query; @@ -28,6 +70,27 @@ router.get('/summary/totals', async (req, res) => { // ── User Behaviour ──────────────────────────────────────────────────────────── +/** + * @swagger + * /api/analytics/users/{userId}/behaviour: + * get: + * summary: Get behaviour profile for a user + * tags: [Analytics] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: userId + * required: true + * schema: { type: string } + * responses: + * 200: + * description: User behaviour profile + * 401: + * description: Unauthorized + * 500: + * description: Server error + */ router.get('/users/:userId/behaviour', requireAuth, async (req, res) => { try { res.json(await userBehavior.getProfile(req.params.userId)); @@ -38,6 +101,28 @@ router.get('/users/:userId/behaviour', requireAuth, async (req, res) => { // ── Pattern Analysis ────────────────────────────────────────────────────────── +/** + * @swagger + * /api/analytics/patterns: + * get: + * summary: Analyze transaction patterns + * tags: [Analytics] + * parameters: + * - in: query + * name: userId + * schema: { type: string } + * - in: query + * name: from + * schema: { type: string, format: date-time } + * - in: query + * name: to + * schema: { type: string, format: date-time } + * responses: + * 200: + * description: Pattern analysis result + * 500: + * description: Server error + */ router.get('/patterns', async (req, res) => { try { const { userId, from, to } = req.query; @@ -49,6 +134,29 @@ router.get('/patterns', async (req, res) => { // ── Fraud Detection ─────────────────────────────────────────────────────────── +/** + * @swagger + * /api/analytics/fraud/flags: + * get: + * summary: Get fraud detection flags + * tags: [Analytics] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: from + * schema: { type: string, format: date-time } + * - in: query + * name: to + * schema: { type: string, format: date-time } + * responses: + * 200: + * description: Fraud flags + * 401: + * description: Unauthorized + * 500: + * description: Server error + */ router.get('/fraud/flags', requireAuth, async (req, res) => { try { const { from, to } = req.query; @@ -61,6 +169,25 @@ router.get('/fraud/flags', requireAuth, async (req, res) => { // ── Dashboard (combined) ────────────────────────────────────────────────────── +/** + * @swagger + * /api/analytics/dashboard: + * get: + * summary: Get combined analytics dashboard data + * tags: [Analytics] + * parameters: + * - in: query + * name: from + * schema: { type: string, format: date-time } + * - in: query + * name: to + * schema: { type: string, format: date-time } + * responses: + * 200: + * description: Dashboard data (totals, daily, patterns) + * 500: + * description: Server error + */ router.get('/dashboard', async (req, res) => { try { const { from, to } = req.query; @@ -77,6 +204,35 @@ router.get('/dashboard', async (req, res) => { // ── Export ──────────────────────────────────────────────────────────────────── +/** + * @swagger + * /api/analytics/export: + * get: + * summary: Export transaction analytics data + * tags: [Analytics] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: userId + * schema: { type: string } + * - in: query + * name: from + * schema: { type: string, format: date-time } + * - in: query + * name: to + * schema: { type: string, format: date-time } + * - in: query + * name: format + * schema: { type: string, enum: [json, csv], default: json } + * responses: + * 200: + * description: Exported data (JSON or CSV) + * 401: + * description: Unauthorized + * 500: + * description: Server error + */ router.get('/export', requireAuth, async (req, res) => { try { const { userId, from, to, format = 'json' } = req.query; diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 6ba3247..ab4dc06 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -19,7 +19,40 @@ const userRules = [ body('password').isLength({ min: 8 }).withMessage('Password must be at least 8 chars'), ]; -// POST /api/auth/register +/** + * @swagger + * /api/auth/register: + * post: + * summary: Register a new user + * tags: [Auth] + * security: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [username, password] + * properties: + * username: + * type: string + * minLength: 3 + * maxLength: 32 + * password: + * type: string + * minLength: 8 + * responses: + * 201: + * description: User created + * 409: + * description: Username already taken + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 422: + * description: Validation error + */ router.post('/register', userRules, validateBody, async (req, res) => { try { const { username, password } = req.body; @@ -31,7 +64,45 @@ router.post('/register', userRules, validateBody, async (req, res) => { } }); -// POST /api/auth/login +/** + * @swagger + * /api/auth/login: + * post: + * summary: Log in and receive JWT tokens + * tags: [Auth] + * security: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [username, password] + * properties: + * username: + * type: string + * password: + * type: string + * responses: + * 200: + * description: Tokens issued + * content: + * application/json: + * schema: + * type: object + * properties: + * accessToken: { type: string } + * refreshToken: { type: string } + * recovered: { type: boolean } + * 401: + * description: Invalid credentials + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 422: + * description: Validation error + */ router.post('/login', userRules, validateBody, async (req, res) => { const { username, password } = req.body; const user = findUser(username); @@ -62,7 +133,37 @@ router.post('/login', userRules, validateBody, async (req, res) => { }); }); -// POST /api/auth/refresh +/** + * @swagger + * /api/auth/refresh: + * post: + * summary: Refresh access token + * tags: [Auth] + * security: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [refreshToken] + * properties: + * refreshToken: + * type: string + * responses: + * 200: + * description: New access token + * content: + * application/json: + * schema: + * type: object + * properties: + * accessToken: { type: string } + * 400: + * description: refreshToken missing + * 401: + * description: Invalid or expired refresh token + */ router.post('/refresh', (req, res) => { const { refreshToken } = req.body; if (!refreshToken) return res.status(400).json({ error: 'refreshToken required' }); @@ -74,12 +175,48 @@ router.post('/refresh', (req, res) => { } }); -// POST /api/auth/logout (client should discard tokens; server-side blacklist can be added later) +/** + * @swagger + * /api/auth/logout: + * post: + * summary: Log out (client should discard tokens) + * tags: [Auth] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Logged out + * 401: + * description: Unauthorized + */ router.post('/logout', requireAuth, (_req, res) => { res.json({ message: 'Logged out successfully' }); }); -// GET /api/auth/profile +/** + * @swagger + * /api/auth/profile: + * get: + * summary: Get authenticated user profile + * tags: [Auth] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: User profile + * content: + * application/json: + * schema: + * type: object + * properties: + * id: { type: string } + * username: { type: string } + * createdAt: { type: string, format: date-time } + * 401: + * description: Unauthorized + * 404: + * description: User not found + */ router.get('/profile', requireAuth, (req, res) => { const user = getUserById(req.user.sub); if (!user) return res.status(404).json({ error: 'User not found' }); diff --git a/backend/src/routes/backup.js b/backend/src/routes/backup.js index 4e8b03d..a966eae 100644 --- a/backend/src/routes/backup.js +++ b/backend/src/routes/backup.js @@ -10,7 +10,18 @@ import { const router = express.Router(); -// GET /api/backup — list all backup files +/** + * @swagger + * /api/backup: + * get: + * summary: List all backup files + * tags: [Backup] + * responses: + * 200: + * description: List of backup metadata + * 500: + * description: Server error + */ router.get('/', async (_req, res) => { try { res.json(await listBackups()); @@ -19,7 +30,25 @@ router.get('/', async (_req, res) => { } }); -// POST /api/backup — trigger a manual backup +/** + * @swagger + * /api/backup: + * post: + * summary: Trigger a manual backup + * tags: [Backup] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * tag: { type: string, default: manual } + * responses: + * 201: + * description: Backup created + * 500: + * description: Server error + */ router.post('/', async (req, res) => { try { const meta = await createBackup({ tag: req.body?.tag || 'manual' }); @@ -29,7 +58,29 @@ router.post('/', async (req, res) => { } }); -// POST /api/backup/verify — verify checksum of a backup file +/** + * @swagger + * /api/backup/verify: + * post: + * summary: Verify checksum of a backup file + * tags: [Backup] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [file] + * properties: + * file: { type: string } + * responses: + * 200: + * description: Verification result + * 400: + * description: file is required + * 500: + * description: Server error + */ router.post('/verify', async (req, res) => { const { file } = req.body; if (!file) return res.status(400).json({ error: 'file is required' }); @@ -40,7 +91,31 @@ router.post('/verify', async (req, res) => { } }); -// POST /api/backup/restore — restore a backup (supports PITR via targetTime) +/** + * @swagger + * /api/backup/restore: + * post: + * summary: Restore a backup (supports point-in-time recovery) + * tags: [Backup] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [file] + * properties: + * file: { type: string } + * targetTime: { type: string, format: date-time } + * targetDatabase: { type: string } + * responses: + * 200: + * description: Restore result + * 400: + * description: file is required + * 500: + * description: Server error + */ router.post('/restore', async (req, res) => { const { file, targetTime, targetDatabase } = req.body; if (!file) return res.status(400).json({ error: 'file is required' }); @@ -51,7 +126,18 @@ router.post('/restore', async (req, res) => { } }); -// DELETE /api/backup/retention — enforce retention policy immediately +/** + * @swagger + * /api/backup/retention: + * delete: + * summary: Enforce backup retention policy immediately + * tags: [Backup] + * responses: + * 200: + * description: Retention enforced + * 500: + * description: Server error + */ router.delete('/retention', async (_req, res) => { try { res.json(await enforceRetention()); @@ -60,7 +146,16 @@ router.delete('/retention', async (_req, res) => { } }); -// GET /api/backup/metrics — backup health, counters, alerts +/** + * @swagger + * /api/backup/metrics: + * get: + * summary: Get backup health metrics and alerts + * tags: [Backup] + * responses: + * 200: + * description: Backup metrics + */ router.get('/metrics', (_req, res) => { res.json(getMetrics()); }); diff --git a/backend/src/routes/chaos.js b/backend/src/routes/chaos.js index 73c611a..0cfa657 100644 --- a/backend/src/routes/chaos.js +++ b/backend/src/routes/chaos.js @@ -12,7 +12,29 @@ import { const router = express.Router(); -// Failure injection endpoints +/** + * @swagger + * /api/chaos/inject/latency: + * post: + * summary: Inject latency into a target + * tags: [Chaos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [targetId, delayMs] + * properties: + * targetId: { type: string } + * delayMs: { type: integer } + * probability: { type: number, minimum: 0, maximum: 1 } + * responses: + * 200: + * description: Latency injection created + * 400: + * description: Invalid parameters + */ router.post('/inject/latency', (req, res) => { try { const { targetId, delayMs, probability } = req.body; @@ -23,6 +45,30 @@ router.post('/inject/latency', (req, res) => { } }); +/** + * @swagger + * /api/chaos/inject/error: + * post: + * summary: Inject errors into a target + * tags: [Chaos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [targetId, errorRate] + * properties: + * targetId: { type: string } + * errorRate: { type: number } + * errorCode: { type: integer } + * probability: { type: number } + * responses: + * 200: + * description: Error injection created + * 400: + * description: Invalid parameters + */ router.post('/inject/error', (req, res) => { try { const { targetId, errorRate, errorCode, probability } = req.body; @@ -33,6 +79,29 @@ router.post('/inject/error', (req, res) => { } }); +/** + * @swagger + * /api/chaos/inject/packet-loss: + * post: + * summary: Inject packet loss into a target + * tags: [Chaos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [targetId, lossRate] + * properties: + * targetId: { type: string } + * lossRate: { type: number } + * probability: { type: number } + * responses: + * 200: + * description: Packet loss injection created + * 400: + * description: Invalid parameters + */ router.post('/inject/packet-loss', (req, res) => { try { const { targetId, lossRate, probability } = req.body; @@ -43,6 +112,18 @@ router.post('/inject/packet-loss', (req, res) => { } }); +/** + * @swagger + * /api/chaos/failures/active: + * get: + * summary: Get all active failure injections + * tags: [Chaos] + * responses: + * 200: + * description: Active failures + * 500: + * description: Server error + */ router.get('/failures/active', (req, res) => { try { const failures = failureInjector.getActiveFailures(); @@ -52,6 +133,23 @@ router.get('/failures/active', (req, res) => { } }); +/** + * @swagger + * /api/chaos/failures/remove/{injectionId}: + * post: + * summary: Remove a failure injection + * tags: [Chaos] + * parameters: + * - in: path + * name: injectionId + * required: true + * schema: { type: string } + * responses: + * 200: + * description: Injection removed + * 500: + * description: Server error + */ router.post('/failures/remove/:injectionId', (req, res) => { try { failureInjector.removeInjection(req.params.injectionId); @@ -61,7 +159,29 @@ router.post('/failures/remove/:injectionId', (req, res) => { } }); -// Network partition endpoints +/** + * @swagger + * /api/chaos/network/partition: + * post: + * summary: Create a network partition + * tags: [Chaos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [partitionId, affectedServices] + * properties: + * partitionId: { type: string } + * affectedServices: { type: array, items: { type: string } } + * healTime: { type: integer, description: Auto-heal after ms } + * responses: + * 200: + * description: Partition created + * 400: + * description: Invalid parameters + */ router.post('/network/partition', (req, res) => { try { const { partitionId, affectedServices, healTime } = req.body; @@ -72,6 +192,18 @@ router.post('/network/partition', (req, res) => { } }); +/** + * @swagger + * /api/chaos/network/partitions: + * get: + * summary: Get active network partitions + * tags: [Chaos] + * responses: + * 200: + * description: Active partitions + * 500: + * description: Server error + */ router.get('/network/partitions', (req, res) => { try { const partitions = networkPartitionSimulator.getActivePartitions(); @@ -81,6 +213,23 @@ router.get('/network/partitions', (req, res) => { } }); +/** + * @swagger + * /api/chaos/network/heal/{partitionId}: + * post: + * summary: Heal a network partition + * tags: [Chaos] + * parameters: + * - in: path + * name: partitionId + * required: true + * schema: { type: string } + * responses: + * 200: + * description: Partition healed + * 500: + * description: Server error + */ router.post('/network/heal/:partitionId', (req, res) => { try { networkPartitionSimulator.healPartition(req.params.partitionId); @@ -90,7 +239,29 @@ router.post('/network/heal/:partitionId', (req, res) => { } }); -// Service failure endpoints +/** + * @swagger + * /api/chaos/service/fail: + * post: + * summary: Simulate a service failure + * tags: [Chaos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [serviceId, failureType] + * properties: + * serviceId: { type: string } + * failureType: { type: string } + * recoveryTime: { type: integer } + * responses: + * 200: + * description: Service failure simulated + * 400: + * description: Invalid parameters + */ router.post('/service/fail', (req, res) => { try { const { serviceId, failureType, recoveryTime } = req.body; @@ -101,6 +272,18 @@ router.post('/service/fail', (req, res) => { } }); +/** + * @swagger + * /api/chaos/service/failures: + * get: + * summary: Get all service failures + * tags: [Chaos] + * responses: + * 200: + * description: Service failures + * 500: + * description: Server error + */ router.get('/service/failures', (req, res) => { try { const failures = serviceFailureSimulator.getAllFailures(); @@ -110,6 +293,23 @@ router.get('/service/failures', (req, res) => { } }); +/** + * @swagger + * /api/chaos/service/recover/{serviceId}: + * post: + * summary: Recover a failed service + * tags: [Chaos] + * parameters: + * - in: path + * name: serviceId + * required: true + * schema: { type: string } + * responses: + * 200: + * description: Service recovered + * 500: + * description: Server error + */ router.post('/service/recover/:serviceId', (req, res) => { try { serviceFailureSimulator.recoverService(req.params.serviceId); @@ -119,7 +319,29 @@ router.post('/service/recover/:serviceId', (req, res) => { } }); -// Database failure endpoints +/** + * @swagger + * /api/chaos/database/fail: + * post: + * summary: Simulate a database failure + * tags: [Chaos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [databaseId, failureType] + * properties: + * databaseId: { type: string } + * failureType: { type: string } + * recoveryTime: { type: integer } + * responses: + * 200: + * description: Database failure simulated + * 400: + * description: Invalid parameters + */ router.post('/database/fail', (req, res) => { try { const { databaseId, failureType, recoveryTime } = req.body; @@ -130,6 +352,29 @@ router.post('/database/fail', (req, res) => { } }); +/** + * @swagger + * /api/chaos/database/query-failure: + * post: + * summary: Inject query-level database failures + * tags: [Chaos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [queryPattern, failureRate] + * properties: + * queryPattern: { type: string } + * failureRate: { type: number } + * errorMessage: { type: string } + * responses: + * 200: + * description: Query failure injected + * 400: + * description: Invalid parameters + */ router.post('/database/query-failure', (req, res) => { try { const { queryPattern, failureRate, errorMessage } = req.body; @@ -140,7 +385,30 @@ router.post('/database/query-failure', (req, res) => { } }); -// Recovery analysis endpoints +/** + * @swagger + * /api/chaos/recovery/record: + * post: + * summary: Record a recovery time metric + * tags: [Chaos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [serviceId, failureType, downtime] + * properties: + * serviceId: { type: string } + * failureType: { type: string } + * downtime: { type: integer, description: Downtime in ms } + * recoveryActions: { type: array, items: { type: string } } + * responses: + * 200: + * description: Recovery metric recorded + * 400: + * description: Invalid parameters + */ router.post('/recovery/record', (req, res) => { try { const { serviceId, failureType, downtime, recoveryActions } = req.body; @@ -151,6 +419,23 @@ router.post('/recovery/record', (req, res) => { } }); +/** + * @swagger + * /api/chaos/recovery/report/{serviceId}: + * get: + * summary: Get recovery time report for a service + * tags: [Chaos] + * parameters: + * - in: path + * name: serviceId + * required: true + * schema: { type: string } + * responses: + * 200: + * description: Recovery report + * 500: + * description: Server error + */ router.get('/recovery/report/:serviceId', (req, res) => { try { const report = recoveryTimeAnalyzer.getRecoveryReport(req.params.serviceId); @@ -160,7 +445,30 @@ router.get('/recovery/report/:serviceId', (req, res) => { } }); -// Blast radius endpoints +/** + * @swagger + * /api/chaos/blast-radius/limit: + * post: + * summary: Set blast radius limits for chaos experiments + * tags: [Chaos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [limitId] + * properties: + * limitId: { type: string } + * maxAffectedServices: { type: integer } + * maxErrorRate: { type: number } + * maxDowntime: { type: integer } + * responses: + * 200: + * description: Limit set + * 400: + * description: Invalid parameters + */ router.post('/blast-radius/limit', (req, res) => { try { const { limitId, maxAffectedServices, maxErrorRate, maxDowntime } = req.body; @@ -171,6 +479,29 @@ router.post('/blast-radius/limit', (req, res) => { } }); +/** + * @swagger + * /api/chaos/blast-radius/check: + * post: + * summary: Check if a failure injection is within blast radius limits + * tags: [Chaos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [failureId, affectedServices, estimatedErrorRate] + * properties: + * failureId: { type: string } + * affectedServices: { type: array, items: { type: string } } + * estimatedErrorRate: { type: number } + * responses: + * 200: + * description: Check result + * 400: + * description: Invalid parameters + */ router.post('/blast-radius/check', (req, res) => { try { const { failureId, affectedServices, estimatedErrorRate } = req.body; @@ -181,7 +512,30 @@ router.post('/blast-radius/check', (req, res) => { } }); -// Chaos experiment endpoints +/** + * @swagger + * /api/chaos/experiments/create: + * post: + * summary: Create a chaos experiment + * tags: [Chaos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [name] + * properties: + * name: { type: string } + * description: { type: string } + * failureInjections: { type: array, items: { type: object } } + * duration: { type: integer, description: Duration in ms } + * responses: + * 200: + * description: Experiment created + * 500: + * description: Server error + */ router.post('/experiments/create', async (req, res) => { try { const { name, description, failureInjections, duration } = req.body; @@ -192,6 +546,23 @@ router.post('/experiments/create', async (req, res) => { } }); +/** + * @swagger + * /api/chaos/experiments/{experimentId}/run: + * post: + * summary: Run a chaos experiment + * tags: [Chaos] + * parameters: + * - in: path + * name: experimentId + * required: true + * schema: { type: string } + * responses: + * 200: + * description: Experiment result + * 500: + * description: Server error + */ router.post('/experiments/:experimentId/run', async (req, res) => { try { const experiment = await chaosTestAutomation.runExperiment(req.params.experimentId); @@ -201,6 +572,18 @@ router.post('/experiments/:experimentId/run', async (req, res) => { } }); +/** + * @swagger + * /api/chaos/experiments: + * get: + * summary: List all chaos experiments + * tags: [Chaos] + * responses: + * 200: + * description: List of experiments + * 500: + * description: Server error + */ router.get('/experiments', async (req, res) => { try { const experiments = await chaosTestAutomation.getAllExperiments(); @@ -210,7 +593,29 @@ router.get('/experiments', async (req, res) => { } }); -// Reporting endpoints +/** + * @swagger + * /api/chaos/reports/generate: + * post: + * summary: Generate a chaos experiment report + * tags: [Chaos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [experimentId] + * properties: + * experimentId: { type: string } + * results: { type: object } + * metrics: { type: object } + * responses: + * 200: + * description: Report generated + * 500: + * description: Server error + */ router.post('/reports/generate', async (req, res) => { try { const { experimentId, results, metrics } = req.body; @@ -221,6 +626,26 @@ router.post('/reports/generate', async (req, res) => { } }); +/** + * @swagger + * /api/chaos/reports/{experimentId}: + * get: + * summary: Get reports for a chaos experiment + * tags: [Chaos] + * parameters: + * - in: path + * name: experimentId + * required: true + * schema: { type: string } + * - in: query + * name: limit + * schema: { type: integer, default: 10 } + * responses: + * 200: + * description: Experiment reports + * 500: + * description: Server error + */ router.get('/reports/:experimentId', async (req, res) => { try { const limit = parseInt(req.query.limit) || 10; @@ -231,6 +656,18 @@ router.get('/reports/:experimentId', async (req, res) => { } }); +/** + * @swagger + * /api/chaos/reports/summary: + * get: + * summary: Get a summary report across all chaos experiments + * tags: [Chaos] + * responses: + * 200: + * description: Summary report + * 500: + * description: Server error + */ router.get('/reports/summary', async (req, res) => { try { const summary = await chaosReporter.generateSummaryReport(); diff --git a/backend/src/routes/compliance.js b/backend/src/routes/compliance.js index 881aa4a..3e4af5b 100644 --- a/backend/src/routes/compliance.js +++ b/backend/src/routes/compliance.js @@ -13,7 +13,34 @@ const router = Router(); // ── KYC ────────────────────────────────────────────────────────────────────── -// Submit KYC data +/** + * @swagger + * /api/compliance/kyc: + * post: + * summary: Submit KYC data + * tags: [Compliance] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * fullName: { type: string } + * dateOfBirth: { type: string, format: date } + * nationality: { type: string } + * documentType: { type: string } + * documentNumber: { type: string } + * responses: + * 201: + * description: KYC record created + * 400: + * description: Invalid data + * 401: + * description: Unauthorized + */ router.post('/kyc', authMiddleware, async (req, res) => { try { const record = await kycCollector.submitKYC(req.user.id, req.body); @@ -24,14 +51,52 @@ router.post('/kyc', authMiddleware, async (req, res) => { } }); -// Get KYC status +/** + * @swagger + * /api/compliance/kyc/status: + * get: + * summary: Get KYC status for authenticated user + * tags: [Compliance] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: KYC status + * content: + * application/json: + * schema: + * type: object + * properties: + * status: { type: string, enum: [pending, approved, rejected] } + * submittedAt: { type: string, format: date-time } + * updatedAt: { type: string, format: date-time } + * 401: + * description: Unauthorized + * 404: + * description: No KYC record found + */ router.get('/kyc/status', authMiddleware, async (req, res) => { const record = await kycCollector.getKYCRecord(req.user.id); if (!record) return res.status(404).json({ error: 'No KYC record found' }); res.json({ status: record.status, submittedAt: record.submittedAt, updatedAt: record.updatedAt }); }); -// Trigger identity verification +/** + * @swagger + * /api/compliance/kyc/verify: + * post: + * summary: Trigger identity verification + * tags: [Compliance] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Verification result + * 400: + * description: Verification failed + * 401: + * description: Unauthorized + */ router.post('/kyc/verify', authMiddleware, async (req, res) => { try { const result = await identityVerifier.verify(req.user.id); @@ -44,7 +109,38 @@ router.post('/kyc/verify', authMiddleware, async (req, res) => { // ── AML ─────────────────────────────────────────────────────────────────────── -// Screen a transaction for AML flags +/** + * @swagger + * /api/compliance/aml/screen: + * post: + * summary: Screen a transaction for AML flags + * tags: [Compliance] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [transaction] + * properties: + * transaction: + * type: object + * description: Transaction object to screen + * history: + * type: array + * items: { type: object } + * responses: + * 200: + * description: AML screening result + * 400: + * description: transaction is required + * 401: + * description: Unauthorized + * 500: + * description: Server error + */ router.post('/aml/screen', authMiddleware, async (req, res) => { try { const { transaction, history } = req.body; @@ -58,6 +154,20 @@ router.post('/aml/screen', authMiddleware, async (req, res) => { // ── Risk Scoring ────────────────────────────────────────────────────────────── +/** + * @swagger + * /api/compliance/risk/user: + * get: + * summary: Get risk score for authenticated user + * tags: [Compliance] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Risk score result + * 401: + * description: Unauthorized + */ router.get('/risk/user', authMiddleware, async (req, res) => { const result = await riskScorer.scoreUser(req.user.id); res.json(result); @@ -65,6 +175,30 @@ router.get('/risk/user', authMiddleware, async (req, res) => { // ── Audit Trail ─────────────────────────────────────────────────────────────── +/** + * @swagger + * /api/compliance/audit: + * get: + * summary: Get compliance audit trail + * tags: [Compliance] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: from + * schema: { type: string, format: date-time } + * - in: query + * name: to + * schema: { type: string, format: date-time } + * - in: query + * name: eventType + * schema: { type: string } + * responses: + * 200: + * description: Audit trail entries + * 401: + * description: Unauthorized + */ router.get('/audit', authMiddleware, async (req, res) => { const { from, to, eventType } = req.query; const trail = await complianceAudit.getTrail({ userId: req.user.id, from, to, eventType }); @@ -73,6 +207,32 @@ router.get('/audit', authMiddleware, async (req, res) => { // ── Reports ─────────────────────────────────────────────────────────────────── +/** + * @swagger + * /api/compliance/reports: + * post: + * summary: Generate a compliance report + * tags: [Compliance] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * type: { type: string, default: AML_SUMMARY } + * from: { type: string, format: date-time } + * to: { type: string, format: date-time } + * responses: + * 201: + * description: Report generated + * 401: + * description: Unauthorized + * 500: + * description: Server error + */ router.post('/reports', authMiddleware, async (req, res) => { try { const { type, from, to } = req.body; @@ -83,6 +243,20 @@ router.post('/reports', authMiddleware, async (req, res) => { } }); +/** + * @swagger + * /api/compliance/reports: + * get: + * summary: List compliance reports + * tags: [Compliance] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: List of reports + * 401: + * description: Unauthorized + */ router.get('/reports', authMiddleware, async (req, res) => { const reports = await complianceReporting.listReports(); res.json(reports); diff --git a/backend/src/routes/loadTesting.js b/backend/src/routes/loadTesting.js index 3e32dec..b483579 100644 --- a/backend/src/routes/loadTesting.js +++ b/backend/src/routes/loadTesting.js @@ -12,7 +12,40 @@ import { const router = express.Router(); -// Scenario endpoints +/** + * @swagger + * /api/load-testing/scenarios/create: + * post: + * summary: Create a load test scenario + * tags: [LoadTesting] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [name] + * properties: + * name: { type: string } + * description: { type: string } + * requests: + * type: array + * items: + * type: object + * properties: + * method: { type: string } + * path: { type: string } + * body: { type: object } + * weight: { type: number } + * duration: { type: integer, description: Duration in ms } + * rampUp: { type: integer } + * concurrency: { type: integer } + * responses: + * 200: + * description: Scenario created + * 500: + * description: Server error + */ router.post('/scenarios/create', async (req, res) => { try { const { name, description, requests, duration, rampUp, concurrency } = req.body; @@ -31,7 +64,28 @@ router.post('/scenarios/create', async (req, res) => { } }); -// Load test endpoints +/** + * @swagger + * /api/load-testing/run: + * post: + * summary: Run a load test scenario + * tags: [LoadTesting] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [scenarioName] + * properties: + * scenarioName: { type: string } + * baseUrl: { type: string, default: 'http://localhost:3001' } + * responses: + * 200: + * description: Load test results + * 500: + * description: Server error + */ router.post('/run', async (req, res) => { try { const { scenarioName, baseUrl } = req.body; @@ -47,6 +101,26 @@ router.post('/run', async (req, res) => { } }); +/** + * @swagger + * /api/load-testing/results/{scenarioName}: + * get: + * summary: Get latest load test results for a scenario + * tags: [LoadTesting] + * parameters: + * - in: path + * name: scenarioName + * required: true + * schema: { type: string } + * - in: query + * name: limit + * schema: { type: integer, default: 10 } + * responses: + * 200: + * description: Test results + * 500: + * description: Server error + */ router.get('/results/:scenarioName', async (req, res) => { try { const limit = parseInt(req.query.limit) || 10; @@ -57,7 +131,29 @@ router.get('/results/:scenarioName', async (req, res) => { } }); -// Baseline endpoints +/** + * @swagger + * /api/load-testing/baseline/establish: + * post: + * summary: Establish a performance baseline from latest results + * tags: [LoadTesting] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [scenarioName] + * properties: + * scenarioName: { type: string } + * responses: + * 200: + * description: Baseline established + * 400: + * description: No test results found + * 500: + * description: Server error + */ router.post('/baseline/establish', async (req, res) => { try { const { scenarioName } = req.body; @@ -77,6 +173,23 @@ router.post('/baseline/establish', async (req, res) => { } }); +/** + * @swagger + * /api/load-testing/baseline/latest/{scenarioName}: + * get: + * summary: Get the latest performance baseline for a scenario + * tags: [LoadTesting] + * parameters: + * - in: path + * name: scenarioName + * required: true + * schema: { type: string } + * responses: + * 200: + * description: Latest baseline + * 500: + * description: Server error + */ router.get('/baseline/latest/:scenarioName', async (req, res) => { try { const baseline = await PerformanceBaseline.getLatest(req.params.scenarioName); @@ -86,7 +199,29 @@ router.get('/baseline/latest/:scenarioName', async (req, res) => { } }); -// Regression testing endpoints +/** + * @swagger + * /api/load-testing/regression/check: + * post: + * summary: Check for performance regressions against baseline + * tags: [LoadTesting] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [scenarioName] + * properties: + * scenarioName: { type: string } + * responses: + * 200: + * description: Regression report + * 400: + * description: Missing baseline or results + * 500: + * description: Server error + */ router.post('/regression/check', async (req, res) => { try { const { scenarioName } = req.body; @@ -106,7 +241,29 @@ router.post('/regression/check', async (req, res) => { } }); -// Bottleneck analysis endpoints +/** + * @swagger + * /api/load-testing/bottlenecks/analyze: + * post: + * summary: Analyze bottlenecks from latest test results + * tags: [LoadTesting] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [scenarioName] + * properties: + * scenarioName: { type: string } + * responses: + * 200: + * description: Bottlenecks and recommendations + * 400: + * description: No test results found + * 500: + * description: Server error + */ router.post('/bottlenecks/analyze', async (req, res) => { try { const { scenarioName } = req.body; @@ -125,7 +282,30 @@ router.post('/bottlenecks/analyze', async (req, res) => { } }); -// Capacity planning endpoints +/** + * @swagger + * /api/load-testing/capacity/calculate: + * post: + * summary: Calculate current capacity from test results + * tags: [LoadTesting] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [scenarioName] + * properties: + * scenarioName: { type: string } + * targetErrorRate: { type: number, default: 1 } + * responses: + * 200: + * description: Capacity data + * 400: + * description: No test results found + * 500: + * description: Server error + */ router.post('/capacity/calculate', async (req, res) => { try { const { scenarioName, targetErrorRate } = req.body; @@ -142,6 +322,31 @@ router.post('/capacity/calculate', async (req, res) => { } }); +/** + * @swagger + * /api/load-testing/capacity/project: + * post: + * summary: Project future capacity needs based on growth rate + * tags: [LoadTesting] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [scenarioName, growthRate, months] + * properties: + * scenarioName: { type: string } + * growthRate: { type: number, description: Monthly growth rate as decimal } + * months: { type: integer } + * responses: + * 200: + * description: Capacity projection + * 400: + * description: No test results found + * 500: + * description: Server error + */ router.post('/capacity/project', async (req, res) => { try { const { scenarioName, growthRate, months } = req.body; @@ -162,7 +367,29 @@ router.post('/capacity/project', async (req, res) => { } }); -// Performance alerting endpoints +/** + * @swagger + * /api/load-testing/alerts/check: + * post: + * summary: Check performance metrics against alert thresholds + * tags: [LoadTesting] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [scenarioName] + * properties: + * scenarioName: { type: string } + * responses: + * 200: + * description: Alerts triggered + * 400: + * description: No test results found + * 500: + * description: Server error + */ router.post('/alerts/check', async (req, res) => { try { const { scenarioName } = req.body; @@ -181,6 +408,22 @@ router.post('/alerts/check', async (req, res) => { } }); +/** + * @swagger + * /api/load-testing/alerts: + * get: + * summary: Get performance alerts + * tags: [LoadTesting] + * parameters: + * - in: query + * name: limit + * schema: { type: integer, default: 100 } + * responses: + * 200: + * description: Performance alerts + * 500: + * description: Server error + */ router.get('/alerts', async (req, res) => { try { const limit = parseInt(req.query.limit) || 100; @@ -191,6 +434,18 @@ router.get('/alerts', async (req, res) => { } }); +/** + * @swagger + * /api/load-testing/alerts/critical: + * get: + * summary: Get critical performance alerts + * tags: [LoadTesting] + * responses: + * 200: + * description: Critical alerts + * 500: + * description: Server error + */ router.get('/alerts/critical', async (req, res) => { try { const alerts = await performanceAlerting.constructor.getCriticalAlerts(); @@ -200,7 +455,29 @@ router.get('/alerts/critical', async (req, res) => { } }); -// Optimization recommendations endpoints +/** + * @swagger + * /api/load-testing/recommendations: + * post: + * summary: Get optimization recommendations from test results + * tags: [LoadTesting] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [scenarioName] + * properties: + * scenarioName: { type: string } + * responses: + * 200: + * description: Prioritized recommendations + * 400: + * description: No test results found + * 500: + * description: Server error + */ router.post('/recommendations', async (req, res) => { try { const { scenarioName } = req.body; diff --git a/backend/src/routes/notifications.js b/backend/src/routes/notifications.js index 78418dc..7e755d3 100644 --- a/backend/src/routes/notifications.js +++ b/backend/src/routes/notifications.js @@ -32,7 +32,25 @@ const validate = (req, res, next) => { next(); }; -/** GET /api/notifications */ +/** + * @swagger + * /api/notifications: + * get: + * summary: Get in-app notifications for authenticated user + * tags: [Notifications] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: unreadOnly + * schema: { type: boolean } + * description: Return only unread notifications + * responses: + * 200: + * description: List of notifications + * 401: + * description: Unauthorized + */ router.get('/', [ query('unreadOnly').optional().isBoolean().toBoolean(), ], validate, (req, res) => { @@ -41,19 +59,65 @@ router.get('/', [ res.json({ notifications }); }); -/** POST /api/notifications/read-all */ +/** + * @swagger + * /api/notifications/read-all: + * post: + * summary: Mark all notifications as read + * tags: [Notifications] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: All notifications marked as read + * 401: + * description: Unauthorized + */ router.post('/read-all', (req, res) => { const result = markAsRead(req.user.sub, 'all'); res.json(result); }); -/** POST /api/notifications/:id/read */ +/** + * @swagger + * /api/notifications/{id}/read: + * post: + * summary: Mark a notification as read + * tags: [Notifications] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: { type: string } + * responses: + * 200: + * description: Notification marked as read + * 401: + * description: Unauthorized + */ router.post('/:id/read', (req, res) => { const result = markAsRead(req.user.sub, req.params.id); res.json(result); }); -/** GET /api/notifications/preferences */ +/** + * @swagger + * /api/notifications/preferences: + * get: + * summary: Get notification preferences + * tags: [Notifications] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Notification preferences + * 401: + * description: Unauthorized + * 500: + * description: Server error + */ router.get('/preferences', async (req, res) => { try { const prefs = await getPreferences(req.user.sub); @@ -63,7 +127,36 @@ router.get('/preferences', async (req, res) => { } }); -/** PUT /api/notifications/preferences */ +/** + * @swagger + * /api/notifications/preferences: + * put: + * summary: Update notification preferences + * tags: [Notifications] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * notificationsOn: { type: boolean } + * email: { type: boolean } + * push: { type: boolean } + * sms: { type: boolean } + * inApp: { type: boolean } + * quietHoursStart: { type: integer, minimum: 0, maximum: 23 } + * quietHoursEnd: { type: integer, minimum: 0, maximum: 23 } + * responses: + * 200: + * description: Updated preferences + * 401: + * description: Unauthorized + * 500: + * description: Server error + */ router.put('/preferences', [ body('notificationsOn').optional().isBoolean(), body('email').optional().isBoolean(), @@ -82,7 +175,33 @@ router.put('/preferences', [ } }); -/** GET /api/notifications/delivery */ +/** + * @swagger + * /api/notifications/delivery: + * get: + * summary: Get notification delivery history + * tags: [Notifications] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: type + * schema: { type: string } + * - in: query + * name: channel + * schema: { type: string, enum: [email, push, sms, inApp] } + * - in: query + * name: status + * schema: { type: string, enum: [pending, sent, failed, skipped] } + * - in: query + * name: limit + * schema: { type: integer, minimum: 1, maximum: 200 } + * responses: + * 200: + * description: Delivery history + * 401: + * description: Unauthorized + */ router.get('/delivery', [ query('type').optional().isString(), query('channel').optional().isIn(['email', 'push', 'sms', 'inApp']), @@ -94,7 +213,20 @@ router.get('/delivery', [ res.json({ history }); }); -/** GET /api/notifications/stats */ +/** + * @swagger + * /api/notifications/stats: + * get: + * summary: Get notification delivery stats + * tags: [Notifications] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Delivery statistics + * 401: + * description: Unauthorized + */ router.get('/stats', (req, res) => { const stats = getDeliveryStats(req.user.sub); res.json({ stats }); @@ -102,7 +234,34 @@ router.get('/stats', (req, res) => { import { saveSubscription, getSubscription } from '../notifications/webPush.js'; -/** POST /api/notifications/push/subscribe */ +/** + * @swagger + * /api/notifications/push/subscribe: + * post: + * summary: Subscribe to web push notifications + * tags: [Notifications] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [subscription] + * properties: + * subscription: + * type: object + * properties: + * endpoint: { type: string, format: uri } + * publicKey: + * type: string + * responses: + * 201: + * description: Subscribed + * 401: + * description: Unauthorized + */ router.post('/push/subscribe', [ body('subscription').isObject(), body('subscription.endpoint').isURL(), diff --git a/backend/src/routes/recovery.js b/backend/src/routes/recovery.js index 4b6ea68..5feb153 100644 --- a/backend/src/routes/recovery.js +++ b/backend/src/routes/recovery.js @@ -24,7 +24,31 @@ const ip = (req) => req.ip || req.headers['x-forwarded-for'] || 'unknown'; // ── Recovery Phrase ────────────────────────────────────────────────────────── -// POST /api/recovery/phrase/setup — generate and store a recovery phrase +/** + * @swagger + * /api/recovery/phrase/setup: + * post: + * summary: Generate and store a recovery phrase (shown once) + * tags: [Recovery] + * security: + * - bearerAuth: [] + * responses: + * 201: + * description: Recovery phrase generated + * content: + * application/json: + * schema: + * type: object + * properties: + * phrase: { type: string } + * warning: { type: string } + * 401: + * description: Unauthorized + * 409: + * description: Phrase already configured + * 500: + * description: Server error + */ router.post('/phrase/setup', requireAuth, async (req, res) => { try { const userId = req.user.sub; @@ -42,19 +66,76 @@ router.post('/phrase/setup', requireAuth, async (req, res) => { } }); -// GET /api/recovery/phrase/status +/** + * @swagger + * /api/recovery/phrase/status: + * get: + * summary: Check if a recovery phrase is configured + * tags: [Recovery] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Phrase configuration status + * content: + * application/json: + * schema: + * type: object + * properties: + * configured: { type: boolean } + * 401: + * description: Unauthorized + */ router.get('/phrase/status', requireAuth, (req, res) => { res.json({ configured: hasRecoveryPhrase(req.user.sub) }); }); // ── Recovery Contacts ──────────────────────────────────────────────────────── -// GET /api/recovery/contacts +/** + * @swagger + * /api/recovery/contacts: + * get: + * summary: List recovery contacts + * tags: [Recovery] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Recovery contacts + * 401: + * description: Unauthorized + */ router.get('/contacts', requireAuth, (req, res) => { res.json({ contacts: getContacts(req.user.sub) }); }); -// POST /api/recovery/contacts +/** + * @swagger + * /api/recovery/contacts: + * post: + * summary: Add a recovery contact + * tags: [Recovery] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [email, name] + * properties: + * email: { type: string, format: email } + * name: { type: string, maxLength: 64 } + * responses: + * 201: + * description: Contact added + * 400: + * description: Invalid data or limit reached + * 401: + * description: Unauthorized + */ router.post( '/contacts', requireAuth, @@ -71,7 +152,27 @@ router.post( } ); -// POST /api/recovery/contacts/:contactId/confirm +/** + * @swagger + * /api/recovery/contacts/{contactId}/confirm: + * post: + * summary: Confirm a recovery contact + * tags: [Recovery] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: contactId + * required: true + * schema: { type: string, format: uuid } + * responses: + * 200: + * description: Contact confirmed + * 401: + * description: Unauthorized + * 404: + * description: Contact not found + */ router.post('/contacts/:contactId/confirm', requireAuth, param('contactId').isUUID(), validate, (req, res) => { @@ -84,7 +185,27 @@ router.post('/contacts/:contactId/confirm', requireAuth, } ); -// DELETE /api/recovery/contacts/:contactId +/** + * @swagger + * /api/recovery/contacts/{contactId}: + * delete: + * summary: Remove a recovery contact + * tags: [Recovery] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: contactId + * required: true + * schema: { type: string, format: uuid } + * responses: + * 200: + * description: Contact removed + * 401: + * description: Unauthorized + * 404: + * description: Contact not found + */ router.delete('/contacts/:contactId', requireAuth, param('contactId').isUUID(), validate, (req, res) => { @@ -99,7 +220,31 @@ router.delete('/contacts/:contactId', requireAuth, // ── Recovery Workflow ──────────────────────────────────────────────────────── -// POST /api/recovery/initiate — start a recovery request (unauthenticated — user locked out) +/** + * @swagger + * /api/recovery/initiate: + * post: + * summary: Initiate account recovery (unauthenticated) + * tags: [Recovery] + * security: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [userId, method] + * properties: + * userId: { type: string } + * method: { type: string, enum: [phrase, social] } + * responses: + * 201: + * description: Recovery initiated with 24h time-lock + * 400: + * description: Invalid request + * 404: + * description: User not found + */ router.post( '/initiate', body('userId').notEmpty(), @@ -125,7 +270,37 @@ router.post( } ); -// POST /api/recovery/:requestId/verify-phrase — verify recovery phrase +/** + * @swagger + * /api/recovery/{requestId}/verify-phrase: + * post: + * summary: Verify recovery phrase for a recovery request + * tags: [Recovery] + * security: [] + * parameters: + * - in: path + * name: requestId + * required: true + * schema: { type: string, format: uuid } + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [phrase] + * properties: + * phrase: { type: string } + * responses: + * 200: + * description: Phrase verified + * 400: + * description: Wrong method or request locked + * 401: + * description: Invalid phrase + * 404: + * description: Request not found + */ router.post( '/:requestId/verify-phrase', param('requestId').isUUID(), @@ -155,7 +330,35 @@ router.post( } ); -// POST /api/recovery/:requestId/social-approve — a contact approves recovery +/** + * @swagger + * /api/recovery/{requestId}/social-approve: + * post: + * summary: A recovery contact approves a social recovery request + * tags: [Recovery] + * security: [] + * parameters: + * - in: path + * name: requestId + * required: true + * schema: { type: string, format: uuid } + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [contactId] + * properties: + * contactId: { type: string, format: uuid } + * responses: + * 200: + * description: Approval recorded + * 400: + * description: Wrong method or invalid request + * 404: + * description: Request not found + */ router.post( '/:requestId/social-approve', param('requestId').isUUID(), @@ -188,7 +391,33 @@ router.post( } ); -// POST /api/recovery/:requestId/complete — finalize recovery after time-lock +/** + * @swagger + * /api/recovery/{requestId}/complete: + * post: + * summary: Complete recovery and set new password (after time-lock) + * tags: [Recovery] + * security: [] + * parameters: + * - in: path + * name: requestId + * required: true + * schema: { type: string, format: uuid } + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [newPassword] + * properties: + * newPassword: { type: string, minLength: 8 } + * responses: + * 200: + * description: Recovery complete + * 400: + * description: Time-lock not elapsed or request invalid + */ router.post( '/:requestId/complete', param('requestId').isUUID(), @@ -206,7 +435,27 @@ router.post( } ); -// POST /api/recovery/:requestId/cancel +/** + * @swagger + * /api/recovery/{requestId}/cancel: + * post: + * summary: Cancel an active recovery request + * tags: [Recovery] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: requestId + * required: true + * schema: { type: string, format: uuid } + * responses: + * 200: + * description: Recovery cancelled + * 400: + * description: Cannot cancel + * 401: + * description: Unauthorized + */ router.post('/:requestId/cancel', requireAuth, param('requestId').isUUID(), validate, async (req, res) => { @@ -220,13 +469,39 @@ router.post('/:requestId/cancel', requireAuth, } ); -// GET /api/recovery/status — get active recovery request for authenticated user +/** + * @swagger + * /api/recovery/status: + * get: + * summary: Get active recovery request for authenticated user + * tags: [Recovery] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Active recovery request or null + * 401: + * description: Unauthorized + */ router.get('/status', requireAuth, (req, res) => { const active = getActiveRequest(req.user.sub); res.json({ active: active || null }); }); -// GET /api/recovery/history — full request history for authenticated user +/** + * @swagger + * /api/recovery/history: + * get: + * summary: Get recovery request history for authenticated user + * tags: [Recovery] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Recovery request history + * 401: + * description: Unauthorized + */ router.get('/history', requireAuth, (req, res) => { res.json({ requests: getUserRequests(req.user.sub) }); }); diff --git a/backend/src/routes/streaming.js b/backend/src/routes/streaming.js index 367782a..d77de58 100644 --- a/backend/src/routes/streaming.js +++ b/backend/src/routes/streaming.js @@ -30,6 +30,38 @@ const streamRules = { ], }; +/** + * @swagger + * /api/streaming: + * post: + * summary: Create a streaming payment + * tags: [Streaming] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [senderPublicKey, recipientPublicKey, rateAmount] + * properties: + * senderPublicKey: { type: string } + * recipientPublicKey: { type: string } + * assetCode: { type: string, default: XLM } + * rateAmount: { type: number, description: Amount per interval } + * intervalSeconds: { type: integer, minimum: 10, default: 60 } + * endTime: { type: string, format: date-time } + * responses: + * 201: + * description: Stream created + * 400: + * description: Validation error + * 500: + * description: Server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.post('/', streamRules.create, validate, async (req, res) => { try { const stream = await StreamingService.createStream(req.body); @@ -40,6 +72,23 @@ router.post('/', streamRules.create, validate, async (req, res) => { } }); +/** + * @swagger + * /api/streaming: + * get: + * summary: List streaming payments + * tags: [Streaming] + * parameters: + * - in: query + * name: senderPublicKey + * schema: { type: string } + * description: Filter by sender public key + * responses: + * 200: + * description: List of streams + * 500: + * description: Server error + */ router.get('/', async (req, res) => { try { const { senderPublicKey } = req.query; @@ -57,6 +106,18 @@ router.get('/', async (req, res) => { } }); +/** + * @swagger + * /api/streaming/analytics: + * get: + * summary: Get streaming payment analytics + * tags: [Streaming] + * responses: + * 200: + * description: Analytics data + * 500: + * description: Server error + */ router.get('/analytics', async (req, res) => { try { const analytics = await StreamingService.getStreamAnalytics(); @@ -67,6 +128,25 @@ router.get('/analytics', async (req, res) => { } }); +/** + * @swagger + * /api/streaming/{id}: + * get: + * summary: Get a streaming payment by ID + * tags: [Streaming] + * parameters: + * - in: path + * name: id + * required: true + * schema: { type: string, format: uuid } + * responses: + * 200: + * description: Stream details + * 404: + * description: Stream not found + * 500: + * description: Server error + */ router.get('/:id', streamRules.idParam, validate, async (req, res) => { try { const stream = await StreamingService.prisma.paymentStream.findUnique({ @@ -80,6 +160,23 @@ router.get('/:id', streamRules.idParam, validate, async (req, res) => { } }); +/** + * @swagger + * /api/streaming/{id}/pause: + * post: + * summary: Pause a streaming payment + * tags: [Streaming] + * parameters: + * - in: path + * name: id + * required: true + * schema: { type: string, format: uuid } + * responses: + * 200: + * description: Stream paused + * 500: + * description: Server error + */ router.post('/:id/pause', streamRules.idParam, validate, async (req, res) => { try { const stream = await StreamingService.pauseStream(req.params.id); @@ -89,6 +186,23 @@ router.post('/:id/pause', streamRules.idParam, validate, async (req, res) => { } }); +/** + * @swagger + * /api/streaming/{id}/resume: + * post: + * summary: Resume a paused streaming payment + * tags: [Streaming] + * parameters: + * - in: path + * name: id + * required: true + * schema: { type: string, format: uuid } + * responses: + * 200: + * description: Stream resumed + * 500: + * description: Server error + */ router.post('/:id/resume', streamRules.idParam, validate, async (req, res) => { try { const stream = await StreamingService.resumeStream(req.params.id); @@ -98,6 +212,23 @@ router.post('/:id/resume', streamRules.idParam, validate, async (req, res) => { } }); +/** + * @swagger + * /api/streaming/{id}/cancel: + * post: + * summary: Cancel a streaming payment + * tags: [Streaming] + * parameters: + * - in: path + * name: id + * required: true + * schema: { type: string, format: uuid } + * responses: + * 200: + * description: Stream cancelled + * 500: + * description: Server error + */ router.post('/:id/cancel', streamRules.idParam, validate, async (req, res) => { try { const stream = await StreamingService.cancelStream(req.params.id);