333 lines
14 KiB
JavaScript
333 lines
14 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const bcrypt = require('bcryptjs');
|
|
const User = require('../models/User');
|
|
const Company = require('../models/Company');
|
|
const Department = require('../models/Department');
|
|
|
|
router.get('/login', (req, res) => {
|
|
if (req.session.user) return res.redirect('/');
|
|
const tab = req.query.tab || 'client';
|
|
const googleClientId = process.env.GOOGLE_CLIENT_ID || '171735739774-4vcvuf86nd17ndaloob48r49t50tvers.apps.googleusercontent.com';
|
|
res.render('auth/login', { tab, googleClientId });
|
|
});
|
|
|
|
router.post('/login', async (req, res) => {
|
|
const { email, password, login_type } = req.body;
|
|
if (!email || !password) {
|
|
req.flash('error_msg', 'Por favor ingresa tu correo y contraseña');
|
|
return res.redirect('/login');
|
|
}
|
|
const cleanEmail = email.trim().toLowerCase();
|
|
|
|
// =========================================================================
|
|
// 1. MODO PERSONAL TÉCNICO & STAFF (Superadmin, Admins y Agentes en MariaDB)
|
|
// =========================================================================
|
|
if (login_type === 'staff') {
|
|
try {
|
|
const user = await User.findByEmail(cleanEmail);
|
|
if (!user) {
|
|
req.flash('error_msg', 'Credenciales de personal técnico incorrectas');
|
|
return res.redirect('/login?tab=staff');
|
|
}
|
|
if (!user.is_active) {
|
|
req.flash('error_msg', 'Cuenta técnica desactivada por el administrador');
|
|
return res.redirect('/login?tab=staff');
|
|
}
|
|
// Si es un cliente regular, no debe entrar por la consola técnica
|
|
if (user.rol === 'client') {
|
|
req.flash('error_msg', 'Esta sección es exclusiva para agentes y técnicos. Los clientes deben ingresar en la pestaña de Clientes.');
|
|
return res.redirect('/login?tab=client');
|
|
}
|
|
const company = await Company.findById(user.company_id);
|
|
if (company && !company.is_active) {
|
|
req.flash('error_msg', 'Tu organización se encuentra suspendida');
|
|
return res.redirect('/login?tab=staff');
|
|
}
|
|
const match = await bcrypt.compare(password, user.password);
|
|
if (!match) {
|
|
req.flash('error_msg', 'Credenciales de personal técnico incorrectas');
|
|
return res.redirect('/login?tab=staff');
|
|
}
|
|
req.session.user = {
|
|
id: user.id,
|
|
company_id: user.company_id,
|
|
nombre: user.nombre,
|
|
email: user.email,
|
|
rol: user.rol,
|
|
provider: user.provider || 'local'
|
|
};
|
|
req.flash('success_msg', `Bienvenido al panel operativo, ${user.nombre}`);
|
|
return res.redirect('/');
|
|
} catch (error) {
|
|
console.error('Error login staff:', error);
|
|
req.flash('error_msg', 'Error al procesar el acceso del personal técnico');
|
|
return res.redirect('/login?tab=staff');
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 2. MODO CLIENTES DE LA PLATAFORMA (API-SIGMA-WEBCONTROL)
|
|
// =========================================================================
|
|
const webcontrolUrl = process.env.WEBCONTROL_API_URL || 'http://192.168.10.160:3002';
|
|
try {
|
|
let wcRes;
|
|
try {
|
|
wcRes = await fetch(`${webcontrolUrl}/api/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: cleanEmail, paswd: password })
|
|
});
|
|
} catch (fetchErr) {
|
|
console.warn('[Auth WebControl] Conexión interna falló, reintentando por dominio público:', fetchErr.message);
|
|
wcRes = await fetch('https://api.siax-system.net/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: cleanEmail, paswd: password })
|
|
});
|
|
}
|
|
|
|
const wcData = await wcRes.json();
|
|
|
|
if (wcRes.ok && wcData.success) {
|
|
const wcUser = wcData.usuario || {};
|
|
const userName = wcUser.nombre || wcUser.usuario || cleanEmail.split('@')[0];
|
|
const providerId = String(wcUser.id || cleanEmail);
|
|
|
|
// Sincronizar o aprovisionar usuario en help_desk.users con rol 'client'
|
|
const user = await User.upsertExternal(userName, cleanEmail, 'webcontrol', providerId, null, 1);
|
|
|
|
req.session.user = {
|
|
id: user.id,
|
|
company_id: user.company_id,
|
|
nombre: user.nombre,
|
|
email: user.email,
|
|
rol: user.rol || 'client',
|
|
provider: 'webcontrol',
|
|
webcontrol_token: wcData.token || null
|
|
};
|
|
req.flash('success_msg', `Bienvenido a Help Desk, ${user.nombre}`);
|
|
return res.redirect('/client/dashboard');
|
|
} else {
|
|
// Verificación de cortesía: si un técnico intentó ingresar en la pestaña de clientes
|
|
const localStaff = await User.findByEmail(cleanEmail);
|
|
if (localStaff && localStaff.rol !== 'client') {
|
|
const matchLocal = await bcrypt.compare(password, localStaff.password);
|
|
if (matchLocal) {
|
|
req.session.user = {
|
|
id: localStaff.id,
|
|
company_id: localStaff.company_id,
|
|
nombre: localStaff.nombre,
|
|
email: localStaff.email,
|
|
rol: localStaff.rol,
|
|
provider: 'local'
|
|
};
|
|
req.flash('success_msg', `Bienvenido al panel técnico, ${localStaff.nombre}`);
|
|
return res.redirect('/');
|
|
}
|
|
}
|
|
|
|
req.flash('error_msg', wcData.message || 'Credenciales de Telcotronics / WebControl incorrectas');
|
|
return res.redirect('/login?tab=client');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error login WebControl:', error);
|
|
req.flash('error_msg', 'Error de comunicación con el servicio de autenticación central WebControl');
|
|
return res.redirect('/login?tab=client');
|
|
}
|
|
});
|
|
|
|
// =============================================================================
|
|
// 3. SOCIAL LOGIN — GOOGLE OAUTH
|
|
// =============================================================================
|
|
router.post('/auth/google', async (req, res) => {
|
|
const { credential } = req.body;
|
|
if (!credential) {
|
|
return res.status(400).json({ success: false, message: 'Token de Google requerido' });
|
|
}
|
|
|
|
try {
|
|
const verifyRes = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(credential)}`);
|
|
const payload = await verifyRes.json();
|
|
|
|
if (!verifyRes.ok || !payload.email) {
|
|
return res.status(401).json({ success: false, message: 'Token de Google no válido o expirado' });
|
|
}
|
|
|
|
const cleanEmail = payload.email.trim().toLowerCase();
|
|
const userName = payload.name || cleanEmail.split('@')[0];
|
|
const avatarUrl = payload.picture || null;
|
|
const providerId = payload.sub;
|
|
|
|
// Auto-provisioning automático como cliente
|
|
const user = await User.upsertExternal(userName, cleanEmail, 'google', providerId, avatarUrl, 1);
|
|
|
|
req.session.user = {
|
|
id: user.id,
|
|
company_id: user.company_id,
|
|
nombre: user.nombre,
|
|
email: user.email,
|
|
rol: user.rol || 'client',
|
|
provider: 'google',
|
|
avatar_url: avatarUrl
|
|
};
|
|
|
|
return res.json({ success: true, redirect: '/client/dashboard' });
|
|
} catch (err) {
|
|
console.error('Error Google OAuth:', err.message);
|
|
return res.status(500).json({ success: false, message: 'Error al verificar autenticación con Google' });
|
|
}
|
|
});
|
|
|
|
// =============================================================================
|
|
// 4. SOCIAL LOGIN — FACEBOOK OAUTH
|
|
// =============================================================================
|
|
router.post('/auth/facebook', async (req, res) => {
|
|
const { accessToken } = req.body;
|
|
if (!accessToken) {
|
|
return res.status(400).json({ success: false, message: 'Token de Facebook requerido' });
|
|
}
|
|
|
|
try {
|
|
const fbRes = await fetch(
|
|
`https://graph.facebook.com/me?fields=id,name,email,picture.type(large)&access_token=${encodeURIComponent(accessToken)}`
|
|
);
|
|
const fbData = await fbRes.json();
|
|
|
|
if (fbData.error || !fbData.id) {
|
|
return res.status(401).json({ success: false, message: 'Token de Facebook inválido' });
|
|
}
|
|
|
|
const cleanEmail = fbData.email ? fbData.email.trim().toLowerCase() : `fb_${fbData.id}@sigmac.app`;
|
|
const userName = fbData.name || 'Usuario Facebook';
|
|
const avatarUrl = fbData.picture?.data?.url || null;
|
|
const providerId = fbData.id;
|
|
|
|
// Auto-provisioning automático como cliente
|
|
const user = await User.upsertExternal(userName, cleanEmail, 'facebook', providerId, avatarUrl, 1);
|
|
|
|
req.session.user = {
|
|
id: user.id,
|
|
company_id: user.company_id,
|
|
nombre: user.nombre,
|
|
email: user.email,
|
|
rol: user.rol || 'client',
|
|
provider: 'facebook',
|
|
avatar_url: avatarUrl
|
|
};
|
|
|
|
return res.json({ success: true, redirect: '/client/dashboard' });
|
|
} catch (err) {
|
|
console.error('Error Facebook OAuth:', err.message);
|
|
return res.status(500).json({ success: false, message: 'Error al verificar autenticación con Facebook' });
|
|
}
|
|
});
|
|
|
|
router.get('/register', (req, res) => {
|
|
if (req.session.user) return res.redirect('/');
|
|
res.render('auth/register');
|
|
});
|
|
|
|
router.post('/register', async (req, res) => {
|
|
const { nombre, email, password, confirm_password, company_name } = req.body;
|
|
if (!nombre || !email || !password || !confirm_password) {
|
|
req.flash('error_msg', 'Todos los campos son obligatorios');
|
|
return res.redirect('/register');
|
|
}
|
|
if (password !== confirm_password) {
|
|
req.flash('error_msg', 'Las contraseñas no coinciden');
|
|
return res.redirect('/register');
|
|
}
|
|
if (password.length < 6) {
|
|
req.flash('error_msg', 'La contraseña debe tener al menos 6 caracteres');
|
|
return res.redirect('/register');
|
|
}
|
|
try {
|
|
const existing = await User.findByEmail(email);
|
|
if (existing) {
|
|
req.flash('error_msg', 'El email ya está registrado');
|
|
return res.redirect('/register');
|
|
}
|
|
|
|
// Create company or use default
|
|
let companyId = 1;
|
|
if (company_name) {
|
|
const slug = company_name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').slice(0, 100);
|
|
const slugExists = await Company.findBySlug(slug);
|
|
if (slugExists) {
|
|
req.flash('error_msg', 'El nombre de empresa ya está registrado');
|
|
return res.redirect('/register');
|
|
}
|
|
companyId = await Company.create(company_name, slug, 'free');
|
|
// Seed default departments for new company
|
|
await Department.create(companyId, 'Soporte Técnico', 'Problemas técnicos y de software');
|
|
await Department.create(companyId, 'Facturación', 'Problemas de facturación y pagos');
|
|
await Department.create(companyId, 'General', 'Consultas generales');
|
|
}
|
|
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
await User.create(companyId, nombre, email, hashedPassword, company_name ? 'admin' : 'client');
|
|
req.flash('success_msg', 'Registro exitoso, ahora puedes iniciar sesión');
|
|
res.redirect('/login');
|
|
} catch (error) {
|
|
console.error(error);
|
|
req.flash('error_msg', 'Error al registrarse');
|
|
res.redirect('/register');
|
|
}
|
|
});
|
|
|
|
router.get('/invite/:token', async (req, res) => {
|
|
try {
|
|
const invite = await User.findByInviteToken(req.params.token);
|
|
if (!invite) {
|
|
req.flash('error_msg', 'Invitación inválida o expirada');
|
|
return res.redirect('/login');
|
|
}
|
|
res.render('auth/accept-invite', { token: req.params.token, email: invite.email });
|
|
} catch (error) {
|
|
console.error(error);
|
|
req.flash('error_msg', 'Error al procesar invitación');
|
|
res.redirect('/login');
|
|
}
|
|
});
|
|
|
|
router.post('/invite/:token', async (req, res) => {
|
|
const { nombre, password, confirm_password } = req.body;
|
|
if (!nombre || !password || !confirm_password) {
|
|
req.flash('error_msg', 'Todos los campos son obligatorios');
|
|
return res.redirect(`/invite/${req.params.token}`);
|
|
}
|
|
if (password !== confirm_password) {
|
|
req.flash('error_msg', 'Las contraseñas no coinciden');
|
|
return res.redirect(`/invite/${req.params.token}`);
|
|
}
|
|
try {
|
|
const invite = await User.findByInviteToken(req.params.token);
|
|
if (!invite) {
|
|
req.flash('error_msg', 'Invitación inválida o expirada');
|
|
return res.redirect('/login');
|
|
}
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
await User.update(invite.id, {
|
|
nombre,
|
|
password: hashedPassword,
|
|
is_active: 1,
|
|
invite_token: null,
|
|
invite_token_expires: null
|
|
});
|
|
req.flash('success_msg', 'Cuenta activada, ahora puedes iniciar sesión');
|
|
res.redirect('/login');
|
|
} catch (error) {
|
|
console.error(error);
|
|
req.flash('error_msg', 'Error al aceptar invitación');
|
|
res.redirect(`/invite/${req.params.token}`);
|
|
}
|
|
});
|
|
|
|
router.get('/logout', (req, res) => {
|
|
req.session.destroy();
|
|
res.redirect('/login');
|
|
});
|
|
|
|
module.exports = router;
|