import { Response, NextFunction } from 'express'; import { z } from 'zod'; import { apiKeysService, CreateApiKeyDto, UpdateApiKeyDto, ApiKeyFilters } from './apiKeys.service.js'; import { AuthenticatedRequest, ValidationError, ApiResponse } from '../../shared/types/index.js'; // ============================================================================ // VALIDATION SCHEMAS // ============================================================================ const createApiKeySchema = z.object({ name: z.string().min(1, 'Nombre requerido').max(255), scope: z.string().max(100).optional(), allowed_ips: z.array(z.string().ip()).optional(), expiration_days: z.number().int().positive().max(365).optional(), }); const updateApiKeySchema = z.object({ name: z.string().min(1).max(255).optional(), scope: z.string().max(100).nullable().optional(), allowed_ips: z.array(z.string().ip()).nullable().optional(), expiration_date: z.string().datetime().nullable().optional(), is_active: z.boolean().optional(), }); const listApiKeysSchema = z.object({ user_id: z.string().uuid().optional(), is_active: z.enum(['true', 'false']).optional(), scope: z.string().optional(), }); // ============================================================================ // CONTROLLER // ============================================================================ class ApiKeysController { /** * Create a new API key * POST /api/auth/api-keys */ async create(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const validation = createApiKeySchema.safeParse(req.body); if (!validation.success) { throw new ValidationError('Datos inválidos', validation.error.errors); } const dto: CreateApiKeyDto = { ...validation.data, user_id: req.user!.userId, tenant_id: req.user!.tenantId, }; const result = await apiKeysService.create(dto); const response: ApiResponse = { success: true, data: result, message: 'API key creada exitosamente. Guarde la clave, no podrá verla de nuevo.', }; res.status(201).json(response); } catch (error) { next(error); } } /** * List API keys for the current user * GET /api/auth/api-keys */ async list(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const validation = listApiKeysSchema.safeParse(req.query); if (!validation.success) { throw new ValidationError('Parámetros inválidos', validation.error.errors); } const filters: ApiKeyFilters = { tenant_id: req.user!.tenantId, // By default, only show user's own keys unless admin user_id: validation.data.user_id || req.user!.userId, }; // Admins can view all keys in tenant if (validation.data.user_id && req.user!.roles.includes('admin')) { filters.user_id = validation.data.user_id; } if (validation.data.is_active !== undefined) { filters.is_active = validation.data.is_active === 'true'; } if (validation.data.scope) { filters.scope = validation.data.scope; } const apiKeys = await apiKeysService.findAll(filters); const response: ApiResponse = { success: true, data: apiKeys, }; res.json(response); } catch (error) { next(error); } } /** * Get a specific API key * GET /api/auth/api-keys/:id */ async getById(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const { id } = req.params; const apiKey = await apiKeysService.findById(id, req.user!.tenantId); if (!apiKey) { const response: ApiResponse = { success: false, error: 'API key no encontrada', }; res.status(404).json(response); return; } // Check ownership (unless admin) if (apiKey.user_id !== req.user!.userId && !req.user!.roles.includes('admin')) { const response: ApiResponse = { success: false, error: 'No tiene permisos para ver esta API key', }; res.status(403).json(response); return; } const response: ApiResponse = { success: true, data: apiKey, }; res.json(response); } catch (error) { next(error); } } /** * Update an API key * PATCH /api/auth/api-keys/:id */ async update(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const { id } = req.params; const validation = updateApiKeySchema.safeParse(req.body); if (!validation.success) { throw new ValidationError('Datos inválidos', validation.error.errors); } // Check ownership first const existing = await apiKeysService.findById(id, req.user!.tenantId); if (!existing) { const response: ApiResponse = { success: false, error: 'API key no encontrada', }; res.status(404).json(response); return; } if (existing.user_id !== req.user!.userId && !req.user!.roles.includes('admin')) { const response: ApiResponse = { success: false, error: 'No tiene permisos para modificar esta API key', }; res.status(403).json(response); return; } const dto: UpdateApiKeyDto = { ...validation.data, expiration_date: validation.data.expiration_date ? new Date(validation.data.expiration_date) : validation.data.expiration_date === null ? null : undefined, }; const updated = await apiKeysService.update(id, req.user!.tenantId, dto); const response: ApiResponse = { success: true, data: updated, message: 'API key actualizada', }; res.json(response); } catch (error) { next(error); } } /** * Revoke an API key (soft delete) * POST /api/auth/api-keys/:id/revoke */ async revoke(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const { id } = req.params; // Check ownership first const existing = await apiKeysService.findById(id, req.user!.tenantId); if (!existing) { const response: ApiResponse = { success: false, error: 'API key no encontrada', }; res.status(404).json(response); return; } if (existing.user_id !== req.user!.userId && !req.user!.roles.includes('admin')) { const response: ApiResponse = { success: false, error: 'No tiene permisos para revocar esta API key', }; res.status(403).json(response); return; } await apiKeysService.revoke(id, req.user!.tenantId); const response: ApiResponse = { success: true, message: 'API key revocada exitosamente', }; res.json(response); } catch (error) { next(error); } } /** * Delete an API key permanently * DELETE /api/auth/api-keys/:id */ async delete(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const { id } = req.params; // Check ownership first const existing = await apiKeysService.findById(id, req.user!.tenantId); if (!existing) { const response: ApiResponse = { success: false, error: 'API key no encontrada', }; res.status(404).json(response); return; } if (existing.user_id !== req.user!.userId && !req.user!.roles.includes('admin')) { const response: ApiResponse = { success: false, error: 'No tiene permisos para eliminar esta API key', }; res.status(403).json(response); return; } await apiKeysService.delete(id, req.user!.tenantId); const response: ApiResponse = { success: true, message: 'API key eliminada permanentemente', }; res.json(response); } catch (error) { next(error); } } /** * Regenerate an API key (invalidates old key, creates new) * POST /api/auth/api-keys/:id/regenerate */ async regenerate(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const { id } = req.params; // Check ownership first const existing = await apiKeysService.findById(id, req.user!.tenantId); if (!existing) { const response: ApiResponse = { success: false, error: 'API key no encontrada', }; res.status(404).json(response); return; } if (existing.user_id !== req.user!.userId && !req.user!.roles.includes('admin')) { const response: ApiResponse = { success: false, error: 'No tiene permisos para regenerar esta API key', }; res.status(403).json(response); return; } const result = await apiKeysService.regenerate(id, req.user!.tenantId); const response: ApiResponse = { success: true, data: result, message: 'API key regenerada. Guarde la nueva clave, no podrá verla de nuevo.', }; res.json(response); } catch (error) { next(error); } } } export const apiKeysController = new ApiKeysController();