/** * Rutas de autenticación */ import express, { Request, Response } from 'express'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { query } from '../../config/database.js'; import { validateSecurityConfig } from '../config/security.js'; import { authLimiter } from '../middleware/rate-limit.js'; import { validateBody } from '../middleware/validate.js'; import { loginSchema } from '../validators/auth.js'; // TypeScript const router = express.Router(); // ✅ VALIDACIÓN DE JWT_SECRET (sin fallback débil) const securityConfig = validateSecurityConfig(); const JWT_SECRET: string = securityConfig.JWT_SECRET; const JWT_EXPIRES_IN: string = securityConfig.JWT_EXPIRES_IN; interface LoginRequest { email: string; password: string; } interface LoginResponse { token: string; user: { id: string; email: string; username: string; role: string; }; expiresIn: number; } interface UserRow { id: string; email: string; username: string; password_hash: string; role: string; is_active: boolean; created_at?: Date; last_login?: Date; } interface JwtPayload { userId: string; email: string; role: string; } /** * POST /api/auth/login * Login de usuario * ✅ Rate limiting: 5 intentos por 15 minutos por IP * ✅ Validación de inputs con Zod */ router.post('/login', authLimiter, validateBody(loginSchema), async (req: Request<{}, LoginResponse, LoginRequest>, res: Response) => { try { // ✅ Datos ya validados por Zod middleware const { email, password } = req.body; // Buscar usuario const result = await query( `SELECT id, email, username, password_hash, role, is_active FROM tes_content.users WHERE email = $1`, [email.toLowerCase()] ); if (result.rows.length === 0) { res.status(401).json({ error: 'Credenciales inválidas' }); return; } const user = result.rows[0] as UserRow; if (!user.is_active) { res.status(401).json({ error: 'Usuario inactivo' }); return; } // Verificar contraseña const validPassword = await bcrypt.compare(password, user.password_hash); if (!validPassword) { res.status(401).json({ error: 'Credenciales inválidas' }); return; } // Actualizar último login await query( `UPDATE tes_content.users SET last_login = NOW() WHERE id = $1`, [user.id] ); // Generar token // JWT_SECRET está validado al startup, nunca será vacío if (!JWT_SECRET || JWT_SECRET.length === 0) { throw new Error('JWT_SECRET no configurado'); } const payload: JwtPayload = { userId: user.id, email: user.email, role: user.role }; const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } as jwt.SignOptions); // Retornar respuesta res.json({ token, user: { id: user.id, email: user.email, username: user.username, role: user.role, }, expiresIn: 24 * 60 * 60, // 24 horas en segundos }); } catch (error) { console.error('Error en login:', error); res.status(500).json({ error: 'Error interno del servidor' }); } }); /** * GET /api/auth/me * Obtener información del usuario actual */ router.get('/me', async (req: Request, res: Response) => { try { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { res.status(401).json({ error: 'Token no proporcionado' }); return; } const token = authHeader.substring(7); // ✅ JWT_SECRET validado al startup, sin fallback débil const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload; const result = await query( `SELECT id, email, username, role, is_active, created_at, last_login FROM tes_content.users WHERE id = $1`, [decoded.userId] ); if (result.rows.length === 0) { res.status(401).json({ error: 'Usuario no encontrado' }); return; } res.json({ user: result.rows[0] as UserRow }); } catch (error) { if (error instanceof Error) { if (error.name === 'JsonWebTokenError') { res.status(401).json({ error: 'Token inválido' }); return; } } res.status(500).json({ error: 'Error interno del servidor' }); } }); export default router;