erp-core-backend-v2/src/modules/auth/apiKeys.controller.ts

332 lines
9.3 KiB
TypeScript

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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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();