332 lines
9.3 KiB
TypeScript
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();
|