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

193 lines
5.7 KiB
TypeScript

import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import { authService } from './auth.service.js';
import { ApiResponse, AuthenticatedRequest, ValidationError } from '../../shared/types/index.js';
// Validation schemas
const loginSchema = z.object({
email: z.string().email('Email inválido'),
password: z.string().min(6, 'La contraseña debe tener al menos 6 caracteres'),
});
const registerSchema = z.object({
email: z.string().email('Email inválido'),
password: z.string().min(8, 'La contraseña debe tener al menos 8 caracteres'),
// Soporta ambos formatos: full_name (legacy) o firstName+lastName (frontend)
full_name: z.string().min(2, 'El nombre debe tener al menos 2 caracteres').optional(),
firstName: z.string().min(2, 'Nombre debe tener al menos 2 caracteres').optional(),
lastName: z.string().min(2, 'Apellido debe tener al menos 2 caracteres').optional(),
tenant_id: z.string().uuid('Tenant ID inválido').optional(),
companyName: z.string().optional(),
}).refine(
(data) => data.full_name || (data.firstName && data.lastName),
{ message: 'Se requiere full_name o firstName y lastName', path: ['full_name'] }
);
const changePasswordSchema = z.object({
current_password: z.string().min(1, 'Contraseña actual requerida'),
new_password: z.string().min(8, 'La nueva contraseña debe tener al menos 8 caracteres'),
});
const refreshTokenSchema = z.object({
refresh_token: z.string().min(1, 'Refresh token requerido'),
});
export class AuthController {
async login(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const validation = loginSchema.safeParse(req.body);
if (!validation.success) {
throw new ValidationError('Datos inválidos', validation.error.errors);
}
// Extract request metadata for session tracking
const metadata = {
ipAddress: req.ip || req.socket.remoteAddress || 'unknown',
userAgent: req.get('User-Agent') || 'unknown',
};
const result = await authService.login({
...validation.data,
metadata,
});
const response: ApiResponse = {
success: true,
data: result,
message: 'Inicio de sesión exitoso',
};
res.json(response);
} catch (error) {
next(error);
}
}
async register(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const validation = registerSchema.safeParse(req.body);
if (!validation.success) {
throw new ValidationError('Datos inválidos', validation.error.errors);
}
const result = await authService.register(validation.data);
const response: ApiResponse = {
success: true,
data: result,
message: 'Usuario registrado exitosamente',
};
res.status(201).json(response);
} catch (error) {
next(error);
}
}
async refreshToken(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const validation = refreshTokenSchema.safeParse(req.body);
if (!validation.success) {
throw new ValidationError('Datos inválidos', validation.error.errors);
}
// Extract request metadata for session tracking
const metadata = {
ipAddress: req.ip || req.socket.remoteAddress || 'unknown',
userAgent: req.get('User-Agent') || 'unknown',
};
const tokens = await authService.refreshToken(validation.data.refresh_token, metadata);
const response: ApiResponse = {
success: true,
data: { tokens },
message: 'Token renovado exitosamente',
};
res.json(response);
} catch (error) {
next(error);
}
}
async changePassword(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
try {
const validation = changePasswordSchema.safeParse(req.body);
if (!validation.success) {
throw new ValidationError('Datos inválidos', validation.error.errors);
}
const userId = req.user!.userId;
await authService.changePassword(
userId,
validation.data.current_password,
validation.data.new_password
);
const response: ApiResponse = {
success: true,
message: 'Contraseña actualizada exitosamente',
};
res.json(response);
} catch (error) {
next(error);
}
}
async getProfile(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
try {
const userId = req.user!.userId;
const profile = await authService.getProfile(userId);
const response: ApiResponse = {
success: true,
data: profile,
};
res.json(response);
} catch (error) {
next(error);
}
}
async logout(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
try {
// sessionId can come from body (sent by client after login)
const sessionId = req.body?.sessionId;
if (sessionId) {
await authService.logout(sessionId);
}
const response: ApiResponse = {
success: true,
message: 'Sesión cerrada exitosamente',
};
res.json(response);
} catch (error) {
next(error);
}
}
async logoutAll(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
try {
const userId = req.user!.userId;
const sessionsRevoked = await authService.logoutAll(userId);
const response: ApiResponse = {
success: true,
data: { sessionsRevoked },
message: 'Todas las sesiones han sido cerradas',
};
res.json(response);
} catch (error) {
next(error);
}
}
}
export const authController = new AuthController();