import { Response, Router } from 'express'; import type { Router as RouterType } from 'express'; import { LiveScopeService, ImageService, GenerationService } from '@/services/core'; import { asyncHandler } from '@/middleware/errorHandler'; import { validateApiKey } from '@/middleware/auth/validateApiKey'; import { requireProjectKey } from '@/middleware/auth/requireProjectKey'; import { rateLimitByApiKey } from '@/middleware/auth/rateLimiter'; import { PAGINATION_LIMITS, ERROR_MESSAGES } from '@/utils/constants'; import { buildPaginationMeta } from '@/utils/helpers'; import { toLiveScopeResponse, toImageResponse } from '@/types/responses'; import type { CreateLiveScopeRequest, ListLiveScopesQuery, UpdateLiveScopeRequest, RegenerateScopeRequest, } from '@/types/requests'; import type { CreateLiveScopeResponse, GetLiveScopeResponse, ListLiveScopesResponse, UpdateLiveScopeResponse, DeleteLiveScopeResponse, RegenerateScopeResponse, } from '@/types/responses'; export const scopesRouter: RouterType = Router(); let scopeService: LiveScopeService; let imageService: ImageService; let generationService: GenerationService; const getScopeService = (): LiveScopeService => { if (!scopeService) { scopeService = new LiveScopeService(); } return scopeService; }; const getImageService = (): ImageService => { if (!imageService) { imageService = new ImageService(); } return imageService; }; const getGenerationService = (): GenerationService => { if (!generationService) { generationService = new GenerationService(); } return generationService; }; /** * POST /api/v1/live/scopes * Create new live scope manually (Section 8.5) * @authentication Project Key required */ scopesRouter.post( '/', validateApiKey, requireProjectKey, rateLimitByApiKey, asyncHandler(async (req: any, res: Response) => { const service = getScopeService(); const { slug, allowNewGenerations, newGenerationsLimit, meta } = req.body as CreateLiveScopeRequest; const projectId = req.apiKey.projectId; // Validate slug format if (!slug || !/^[a-zA-Z0-9_-]+$/.test(slug)) { res.status(400).json({ success: false, error: { message: ERROR_MESSAGES.SCOPE_INVALID_FORMAT, code: 'SCOPE_INVALID_FORMAT', }, }); return; } // Check if scope already exists const existing = await service.getBySlug(projectId, slug); if (existing) { res.status(409).json({ success: false, error: { message: 'Scope with this slug already exists', code: 'SCOPE_ALREADY_EXISTS', }, }); return; } // Create scope const scope = await service.create({ projectId, slug, allowNewGenerations: allowNewGenerations ?? true, newGenerationsLimit: newGenerationsLimit ?? 30, meta: meta || {}, }); // Get with stats const scopeWithStats = await service.getByIdWithStats(scope.id); res.status(201).json({ success: true, data: toLiveScopeResponse(scopeWithStats), }); }), ); /** * GET /api/v1/live/scopes * List all live scopes for a project (Section 8.5) * @authentication Project Key required */ scopesRouter.get( '/', validateApiKey, requireProjectKey, asyncHandler(async (req: any, res: Response) => { const service = getScopeService(); const { slug, limit, offset } = req.query as ListLiveScopesQuery; const projectId = req.apiKey.projectId; const parsedLimit = Math.min( (limit ? parseInt(limit.toString(), 10) : PAGINATION_LIMITS.DEFAULT_LIMIT) || PAGINATION_LIMITS.DEFAULT_LIMIT, PAGINATION_LIMITS.MAX_LIMIT, ); const parsedOffset = (offset ? parseInt(offset.toString(), 10) : 0) || 0; const result = await service.list( { projectId, slug }, parsedLimit, parsedOffset, ); const scopeResponses = result.scopes.map(toLiveScopeResponse); res.json({ success: true, data: scopeResponses, pagination: buildPaginationMeta(result.total, parsedLimit, parsedOffset), }); }), ); /** * GET /api/v1/live/scopes/:slug * Get single live scope by slug (Section 8.5) * @authentication Project Key required */ scopesRouter.get( '/:slug', validateApiKey, requireProjectKey, asyncHandler(async (req: any, res: Response) => { const service = getScopeService(); const { slug } = req.params; const projectId = req.apiKey.projectId; const scopeWithStats = await service.getBySlugWithStats(projectId, slug); res.json({ success: true, data: toLiveScopeResponse(scopeWithStats), }); }), ); /** * PUT /api/v1/live/scopes/:slug * Update live scope settings (Section 8.5) * @authentication Project Key required */ scopesRouter.put( '/:slug', validateApiKey, requireProjectKey, rateLimitByApiKey, asyncHandler(async (req: any, res: Response) => { const service = getScopeService(); const { slug } = req.params; const { allowNewGenerations, newGenerationsLimit, meta } = req.body as UpdateLiveScopeRequest; const projectId = req.apiKey.projectId; // Get scope const scope = await service.getBySlugOrThrow(projectId, slug); // Update scope const updates: { allowNewGenerations?: boolean; newGenerationsLimit?: number; meta?: Record; } = {}; if (allowNewGenerations !== undefined) updates.allowNewGenerations = allowNewGenerations; if (newGenerationsLimit !== undefined) updates.newGenerationsLimit = newGenerationsLimit; if (meta !== undefined) updates.meta = meta; await service.update(scope.id, updates); // Get updated scope with stats const updated = await service.getByIdWithStats(scope.id); res.json({ success: true, data: toLiveScopeResponse(updated), }); }), ); /** * POST /api/v1/live/scopes/:slug/regenerate * Regenerate images in scope (Section 8.5) * @authentication Project Key required */ scopesRouter.post( '/:slug/regenerate', validateApiKey, requireProjectKey, rateLimitByApiKey, asyncHandler(async (req: any, res: Response) => { const scopeService = getScopeService(); const imgService = getImageService(); const genService = getGenerationService(); const { slug } = req.params; const { imageId } = req.body as RegenerateScopeRequest; const projectId = req.apiKey.projectId; // Get scope const scope = await scopeService.getBySlugWithStats(projectId, slug); if (imageId) { // Regenerate specific image const image = await imgService.getById(imageId); if (!image) { res.status(404).json({ success: false, error: { message: ERROR_MESSAGES.IMAGE_NOT_FOUND, code: 'IMAGE_NOT_FOUND', }, }); return; } // Check if image belongs to this scope const imageMeta = image.meta as Record; if (imageMeta['scope'] !== slug) { res.status(400).json({ success: false, error: { message: 'Image does not belong to this scope', code: 'IMAGE_NOT_IN_SCOPE', }, }); return; } // Regenerate the image's generation if (image.generationId) { await genService.regenerate(image.generationId); } const regeneratedImage = await imgService.getById(imageId); res.json({ success: true, data: { regenerated: 1, images: regeneratedImage ? [toImageResponse(regeneratedImage)] : [], }, }); } else { // Regenerate all images in scope if (!scope.images || scope.images.length === 0) { res.json({ success: true, data: { regenerated: 0, images: [], }, }); return; } const regeneratedImages = []; for (const image of scope.images) { if (image.generationId) { await genService.regenerate(image.generationId); const regenerated = await imgService.getById(image.id); if (regenerated) { regeneratedImages.push(toImageResponse(regenerated)); } } } res.json({ success: true, data: { regenerated: regeneratedImages.length, images: regeneratedImages, }, }); } }), ); /** * DELETE /api/v1/live/scopes/:slug * Delete live scope (Section 8.5) * Deletes all images in scope following standard deletion rules * @authentication Project Key required */ scopesRouter.delete( '/:slug', validateApiKey, requireProjectKey, rateLimitByApiKey, asyncHandler(async (req: any, res: Response) => { const scopeService = getScopeService(); const imgService = getImageService(); const { slug } = req.params; const projectId = req.apiKey.projectId; // Get scope with images const scope = await scopeService.getBySlugWithStats(projectId, slug); // Delete all images in scope (follows alias protection rules) if (scope.images) { for (const image of scope.images) { await imgService.hardDelete(image.id); } } // Delete scope record await scopeService.delete(scope.id); res.json({ success: true, data: { id: scope.id }, }); }), );