193 lines
5.7 KiB
TypeScript
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();
|