447 lines
15 KiB
JavaScript
447 lines
15 KiB
JavaScript
import express from 'express';
|
|
import bcrypt from 'bcrypt';
|
|
import { getDB, getUser, createUser, deleteUser as deleteUserFromDB, getAllUsers, updateUserPassword, getActiveSessions } from '../services/mongodb.js';
|
|
import { basicAuthMiddleware, createSession, invalidateSession, invalidateUserSessions } from '../middlewares/auth.js';
|
|
import { adminAuthMiddleware } from '../middlewares/adminAuth.js';
|
|
import { combineFingerprint } from '../utils/fingerprint.js';
|
|
import { getActiveUsers } from '../services/websocket.js';
|
|
|
|
const router = express.Router();
|
|
|
|
// Endpoint de registro (público)
|
|
router.post('/register', async (req, res) => {
|
|
try {
|
|
const db = getDB();
|
|
if (!db) {
|
|
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
|
}
|
|
|
|
const { username, password, email, planId } = req.body;
|
|
|
|
if (!username || !password) {
|
|
return res.status(400).json({ error: 'username y password son requeridos' });
|
|
}
|
|
|
|
if (username.length < 3) {
|
|
return res.status(400).json({ error: 'El nombre de usuario debe tener al menos 3 caracteres' });
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
return res.status(400).json({ error: 'La contraseña debe tener al menos 6 caracteres' });
|
|
}
|
|
|
|
// Validar email si se proporciona
|
|
if (email) {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(email)) {
|
|
return res.status(400).json({ error: 'Email no válido' });
|
|
}
|
|
}
|
|
|
|
// Verificar si el usuario ya existe
|
|
const existingUser = await getUser(username);
|
|
if (existingUser) {
|
|
return res.status(409).json({ error: 'El usuario ya existe' });
|
|
}
|
|
|
|
// Verificar si es plan de pago y si Stripe está disponible
|
|
const selectedPlanId = planId || 'free';
|
|
const isPaidPlan = selectedPlanId !== 'free';
|
|
|
|
if (isPaidPlan) {
|
|
// Para planes de pago, verificar que Stripe esté configurado
|
|
const { getStripeClient } = await import('../services/stripe.js');
|
|
const stripeClient = getStripeClient();
|
|
|
|
if (!stripeClient) {
|
|
return res.status(503).json({
|
|
error: 'El sistema de pagos no está disponible actualmente',
|
|
message: 'No se pueden procesar planes de pago en este momento. Por favor, intenta con el plan gratuito o contacta con soporte.'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Hashear contraseña y crear usuario
|
|
const passwordHash = await bcrypt.hash(password, 10);
|
|
await createUser({
|
|
username,
|
|
passwordHash,
|
|
email: email || null,
|
|
role: 'user',
|
|
// Si es plan de pago, marcar como pendiente hasta que se complete el pago
|
|
status: isPaidPlan ? 'pending_payment' : 'active',
|
|
});
|
|
|
|
// Crear suscripción inicial
|
|
const { updateUserSubscription } = await import('../services/mongodb.js');
|
|
await updateUserSubscription(username, {
|
|
planId: selectedPlanId,
|
|
status: isPaidPlan ? 'pending' : 'active',
|
|
currentPeriodStart: new Date(),
|
|
currentPeriodEnd: null,
|
|
cancelAtPeriodEnd: false,
|
|
});
|
|
|
|
console.log(`✅ Usuario registrado: ${username} (${selectedPlanId}) - Estado: ${isPaidPlan ? 'pending_payment' : 'active'}`);
|
|
res.json({
|
|
success: true,
|
|
message: 'Usuario creado correctamente',
|
|
username,
|
|
planId: selectedPlanId,
|
|
requiresPayment: isPaidPlan,
|
|
});
|
|
} catch (error) {
|
|
console.error('Error registrando usuario:', error);
|
|
// Manejar error de duplicado
|
|
if (error.code === 11000) {
|
|
return res.status(409).json({ error: 'El usuario ya existe' });
|
|
}
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Endpoint de login (público)
|
|
router.post('/login', async (req, res) => {
|
|
try {
|
|
const db = getDB();
|
|
if (!db) {
|
|
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
|
}
|
|
|
|
const { username, password, fingerprint: clientFingerprint, deviceInfo: clientDeviceInfo } = req.body;
|
|
|
|
if (!username || !password) {
|
|
return res.status(400).json({ error: 'username y password son requeridos' });
|
|
}
|
|
|
|
// Buscar usuario en MongoDB
|
|
const user = await getUser(username);
|
|
|
|
if (!user) {
|
|
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
|
}
|
|
|
|
// Obtener hash de la contraseña
|
|
const passwordHash = user.passwordHash;
|
|
|
|
if (!passwordHash) {
|
|
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
|
}
|
|
|
|
// Verificar contraseña
|
|
const match = await bcrypt.compare(password, passwordHash);
|
|
|
|
if (!match) {
|
|
return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' });
|
|
}
|
|
|
|
// Verificar estado del usuario
|
|
if (user.status === 'pending_payment') {
|
|
return res.status(403).json({
|
|
error: 'Payment pending',
|
|
message: 'Tu cuenta está pendiente de pago. Por favor, completa el proceso de pago para activar tu cuenta.',
|
|
status: 'pending_payment'
|
|
});
|
|
}
|
|
|
|
if (user.status === 'suspended' || user.status === 'disabled') {
|
|
return res.status(403).json({
|
|
error: 'Account suspended',
|
|
message: 'Tu cuenta ha sido suspendida. Contacta con soporte.',
|
|
status: user.status
|
|
});
|
|
}
|
|
|
|
// Generar fingerprint del dispositivo
|
|
const { fingerprint, deviceInfo } = combineFingerprint(clientFingerprint, clientDeviceInfo, req);
|
|
|
|
// Crear sesión/token con fingerprint
|
|
const token = await createSession(username, fingerprint, deviceInfo);
|
|
|
|
// Obtener rol del usuario
|
|
const userRole = user.role || 'user';
|
|
|
|
console.log(`✅ Login exitoso: ${username} (${userRole})`);
|
|
res.json({
|
|
success: true,
|
|
token,
|
|
username,
|
|
role: userRole,
|
|
message: 'Login exitoso'
|
|
});
|
|
} catch (error) {
|
|
console.error('Error en login:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Endpoint de logout (requiere autenticación)
|
|
router.post('/logout', basicAuthMiddleware, async (req, res) => {
|
|
try {
|
|
const token = req.token;
|
|
|
|
if (token) {
|
|
await invalidateSession(token);
|
|
console.log(`✅ Logout exitoso: ${req.user.username}`);
|
|
}
|
|
|
|
res.json({ success: true, message: 'Logout exitoso' });
|
|
} catch (error) {
|
|
console.error('Error en logout:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Verificar token (para validar si la sesión sigue activa)
|
|
router.get('/me', basicAuthMiddleware, async (req, res) => {
|
|
try {
|
|
const user = await getUser(req.user.username);
|
|
res.json({
|
|
success: true,
|
|
username: req.user.username,
|
|
role: user?.role || 'user',
|
|
authenticated: true
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Cambiar contraseña de usuario
|
|
router.post('/change-password', basicAuthMiddleware, async (req, res) => {
|
|
try {
|
|
const db = getDB();
|
|
if (!db) {
|
|
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
|
}
|
|
|
|
const { currentPassword, newPassword } = req.body;
|
|
const username = req.user.username;
|
|
|
|
if (!currentPassword || !newPassword) {
|
|
return res.status(400).json({ error: 'currentPassword y newPassword son requeridos' });
|
|
}
|
|
|
|
if (newPassword.length < 6) {
|
|
return res.status(400).json({ error: 'La nueva contraseña debe tener al menos 6 caracteres' });
|
|
}
|
|
|
|
const user = await getUser(username);
|
|
|
|
if (!user || !user.passwordHash) {
|
|
return res.status(404).json({ error: 'Usuario no encontrado' });
|
|
}
|
|
|
|
// Verificar contraseña actual
|
|
const match = await bcrypt.compare(currentPassword, user.passwordHash);
|
|
if (!match) {
|
|
return res.status(401).json({ error: 'Contraseña actual incorrecta' });
|
|
}
|
|
|
|
// Hashear nueva contraseña y actualizar
|
|
const newPasswordHash = await bcrypt.hash(newPassword, 10);
|
|
await updateUserPassword(username, newPasswordHash);
|
|
|
|
// Invalidar todas las sesiones del usuario (requiere nuevo login)
|
|
await invalidateUserSessions(username);
|
|
|
|
console.log(`✅ Contraseña actualizada para usuario: ${username} (todas las sesiones invalidadas)`);
|
|
res.json({ success: true, message: 'Contraseña actualizada correctamente. Por favor, inicia sesión nuevamente.' });
|
|
} catch (error) {
|
|
console.error('Error cambiando contraseña:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Obtener lista de usuarios (requiere autenticación, admin ve todos, user ve solo suyo)
|
|
router.get('/', basicAuthMiddleware, async (req, res) => {
|
|
try {
|
|
const db = getDB();
|
|
if (!db) {
|
|
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
|
}
|
|
|
|
const users = await getAllUsers(req.user.username);
|
|
|
|
// Convertir ObjectId a string y formatear fechas
|
|
const formattedUsers = users.map(user => {
|
|
const formatted = { ...user };
|
|
formatted._id = user._id?.toString();
|
|
if (user.createdAt && typeof user.createdAt === 'object') {
|
|
formatted.createdAt = user.createdAt.toISOString();
|
|
}
|
|
if (user.updatedAt && typeof user.updatedAt === 'object') {
|
|
formatted.updatedAt = user.updatedAt.toISOString();
|
|
}
|
|
return formatted;
|
|
});
|
|
|
|
// Ordenar por fecha de creación (más recientes primero)
|
|
formattedUsers.sort((a, b) => {
|
|
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
|
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
|
return dateB - dateA;
|
|
});
|
|
|
|
res.json({ users: formattedUsers, total: formattedUsers.length });
|
|
} catch (error) {
|
|
console.error('Error obteniendo usuarios:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Crear nuevo usuario (requiere autenticación admin)
|
|
router.post('/', basicAuthMiddleware, adminAuthMiddleware, async (req, res) => {
|
|
try {
|
|
const db = getDB();
|
|
if (!db) {
|
|
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
|
}
|
|
|
|
const { username, password } = req.body;
|
|
|
|
if (!username || !password) {
|
|
return res.status(400).json({ error: 'username y password son requeridos' });
|
|
}
|
|
|
|
if (username.length < 3) {
|
|
return res.status(400).json({ error: 'El nombre de usuario debe tener al menos 3 caracteres' });
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
return res.status(400).json({ error: 'La contraseña debe tener al menos 6 caracteres' });
|
|
}
|
|
|
|
// Verificar si el usuario ya existe
|
|
const existingUser = await getUser(username);
|
|
if (existingUser) {
|
|
return res.status(409).json({ error: 'El usuario ya existe' });
|
|
}
|
|
|
|
// Hashear contraseña y crear usuario
|
|
const passwordHash = await bcrypt.hash(password, 10);
|
|
await createUser({
|
|
username,
|
|
passwordHash,
|
|
createdBy: req.user.username,
|
|
});
|
|
|
|
console.log(`✅ Usuario creado: ${username} por ${req.user.username}`);
|
|
res.json({ success: true, message: 'Usuario creado correctamente', username });
|
|
} catch (error) {
|
|
console.error('Error creando usuario:', error);
|
|
// Manejar error de duplicado
|
|
if (error.code === 11000) {
|
|
return res.status(409).json({ error: 'El usuario ya existe' });
|
|
}
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Eliminar usuario (requiere autenticación admin)
|
|
router.delete('/:username', basicAuthMiddleware, adminAuthMiddleware, async (req, res) => {
|
|
try {
|
|
const db = getDB();
|
|
if (!db) {
|
|
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
|
}
|
|
|
|
const { username } = req.params;
|
|
const currentUser = req.user.username;
|
|
|
|
// No permitir eliminar el propio usuario
|
|
if (username === currentUser) {
|
|
return res.status(400).json({ error: 'No puedes eliminar tu propio usuario' });
|
|
}
|
|
|
|
// Verificar si el usuario existe
|
|
const user = await getUser(username);
|
|
if (!user) {
|
|
return res.status(404).json({ error: 'Usuario no encontrado' });
|
|
}
|
|
|
|
// Eliminar usuario y sus sesiones
|
|
await deleteUserFromDB(username);
|
|
await invalidateUserSessions(username);
|
|
|
|
// También eliminar sus workers
|
|
const workersCollection = db.collection('workers');
|
|
await workersCollection.deleteOne({ username });
|
|
|
|
console.log(`✅ Usuario eliminado: ${username} por ${currentUser}`);
|
|
res.json({ success: true, message: `Usuario ${username} eliminado correctamente` });
|
|
} catch (error) {
|
|
console.error('Error eliminando usuario:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Obtener usuarios activos/conectados (requiere autenticación)
|
|
router.get('/active', basicAuthMiddleware, async (req, res) => {
|
|
try {
|
|
const db = getDB();
|
|
if (!db) {
|
|
return res.status(500).json({ error: 'MongoDB no está disponible' });
|
|
}
|
|
|
|
// Obtener usuarios activos del WebSocket
|
|
const activeUsersWS = getActiveUsers();
|
|
|
|
// Obtener sesiones activas de la base de datos
|
|
const activeSessions = await getActiveSessions();
|
|
|
|
// Combinar información y eliminar duplicados
|
|
const userMap = new Map();
|
|
|
|
// Añadir usuarios del WebSocket
|
|
for (const user of activeUsersWS) {
|
|
if (!userMap.has(user.username)) {
|
|
userMap.set(user.username, {
|
|
username: user.username,
|
|
role: user.role,
|
|
status: user.status,
|
|
lastActivity: user.lastActivity,
|
|
connectedViaWebSocket: true
|
|
});
|
|
}
|
|
}
|
|
|
|
// Añadir usuarios de sesiones activas (que podrían no estar en WebSocket)
|
|
for (const session of activeSessions) {
|
|
if (!userMap.has(session.username)) {
|
|
const user = await getUser(session.username);
|
|
userMap.set(session.username, {
|
|
username: session.username,
|
|
role: user?.role || 'user',
|
|
status: 'active',
|
|
lastActivity: session.lastActivity?.toISOString() || null,
|
|
connectedViaWebSocket: false,
|
|
deviceInfo: session.deviceInfo
|
|
});
|
|
}
|
|
}
|
|
|
|
// Convertir a array
|
|
const activeUsers = Array.from(userMap.values());
|
|
|
|
// Ordenar por última actividad (más recientes primero)
|
|
activeUsers.sort((a, b) => {
|
|
const dateA = a.lastActivity ? new Date(a.lastActivity).getTime() : 0;
|
|
const dateB = b.lastActivity ? new Date(b.lastActivity).getTime() : 0;
|
|
return dateB - dateA;
|
|
});
|
|
|
|
res.json({
|
|
activeUsers,
|
|
total: activeUsers.length,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
} catch (error) {
|
|
console.error('Error obteniendo usuarios activos:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
export default router;
|
|
|