codigo0/backend/src/routes/auth.ts

167 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-01-19 08:10:16 +00:00
/**
* 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<LoginResponse | { error: string }>) => {
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;