feat(auth): remover acceso rapido por roles, autenticacion exclusiva celular + OTP y token con vigencia de 24 horas
This commit is contained in:
parent
94653861a0
commit
ae6a74c140
@ -71,16 +71,15 @@ def main():
|
|||||||
print("[4/4] Enviando notificación de Pacific Trans GO a WhatsApp (+57 316 745 5877)...", flush=True)
|
print("[4/4] Enviando notificación de Pacific Trans GO a WhatsApp (+57 316 745 5877)...", flush=True)
|
||||||
|
|
||||||
text_msg = (
|
text_msg = (
|
||||||
"👑 *Pacific Trans GO: Actualización de Branding, Notificaciones por Usuario & Descarga de App*\n\n"
|
"👑 *Pacific Trans GO: Autenticación Exclusiva por Celular OTP & Token Persistente de 24 Horas*\n\n"
|
||||||
"¡Hola Carlos! Se ha completado y desplegado la actualización solicitada para la plataforma:\n\n"
|
"¡Hola Carlos! Se ha completado la actualización solicitada en el sistema de acceso y seguridad:\n\n"
|
||||||
"📱 *App Oficial:* `Pacific Trans GO`\n"
|
"📱 *App Oficial:* `Pacific Trans GO`\n"
|
||||||
"📦 *Archivo APK:* `PacifictransGo.apk` (7.07 MB - APK Nativo e Independiente)\n"
|
"📦 *Archivo APK:* `PacifictransGo.apk` (APK Nativo e Independiente)\n"
|
||||||
"🆔 *Package ID:* `co.pacifictrans.go` (Etiqueta oficial: Pacific Trans GO)\n\n"
|
"🆔 *Package ID:* `co.pacifictrans.go`\n\n"
|
||||||
"⚡ *Ajustes Implementados:*\n"
|
"⚡ *Novedades de la Versión:*\n"
|
||||||
"• 🚫 *Botón Superior Removido:* Se eliminó el botón 'APK Android' de la barra superior de navegación para un diseño más limpio y despejado.\n"
|
"• 🚫 *Acceso Rápido por Roles Eliminado:* Se eliminaron completamente todos los selectores y accesos rápidos por rol. Nadie puede simular ni ingresar a perfiles sin verificar identidad.\n"
|
||||||
"• 🔔 *Notificaciones y Mensajes por Usuario:* El centro de notificaciones y badge de mensajes pendientes ahora filtra y muestra únicamente los eventos correspondientes al usuario en sesión (+57...). Si no hay sesión activa, muestra 0 y solicita inicio de sesión seguro.\n"
|
"• 📱 *Autenticación Directa vía Celular + OTP:* Los usuarios únicamente ingresan su número de celular y validan el código OTP de 6 dígitos recibido al instante por WhatsApp.\n"
|
||||||
"• 🏷️ *Branding Unificado:* Todos los textos de la plataforma, web, notificaciones de WhatsApp Evolution, recibos Bre-B y la app nativa ahora se presentan con el nombre oficial: *Pacific Trans GO*.\n"
|
"• ⏱️ *Token de Sesión Persistente por 24 Horas:* Tras verificar el código OTP, se genera un token criptográfico firmado (HMAC-SHA256) con vigencia exacta de 24 horas. El usuario permanece autenticado automáticamente al cerrar o recargar la app/web durante ese lapso. Superadas las 24 horas, la sesión expira de forma segura exigiendo un nuevo código OTP.\n\n"
|
||||||
"• 📥 *Botón Inferior 'Descargar App':* El botón principal de descarga al final de la página ahora muestra el texto exacto: *Descargar App*.\n\n"
|
|
||||||
f"🔗 *Enlace de Descarga Directa:*\n👉 {APK_URL}"
|
f"🔗 *Enlace de Descarga Directa:*\n👉 {APK_URL}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1630,63 +1630,36 @@
|
|||||||
Acceso Seguro con Verificación OTP
|
Acceso Seguro con Verificación OTP
|
||||||
</h3>
|
</h3>
|
||||||
<p style="color: var(--text-muted); font-size: 0.88rem;">
|
<p style="color: var(--text-muted); font-size: 0.88rem;">
|
||||||
Para garantizar la seguridad de conductores y pasajeros, validamos tu identidad mediante código OTP por WhatsApp y correo.
|
Ingresa tu número de celular para recibir tu código de seguridad OTP vía WhatsApp.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Switch Modo: Iniciar Sesión / Registro -->
|
<!-- PASO 1: Formulario de Celular -->
|
||||||
<div style="display: flex; gap: 0.5rem; background: rgba(15, 23, 42, 0.9); padding: 4px; border-radius: 10px; margin-bottom: 1.5rem;">
|
|
||||||
<button type="button" id="btn-auth-mode-login" class="admin-pill-btn active" style="flex: 1; text-align: center;">
|
|
||||||
Iniciar Sesión
|
|
||||||
</button>
|
|
||||||
<button type="button" id="btn-auth-mode-register" class="admin-pill-btn" style="flex: 1; text-align: center;">
|
|
||||||
Registrarme
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PASO 1: Formulario de Identificador -->
|
|
||||||
<div id="auth-step-1">
|
<div id="auth-step-1">
|
||||||
<form id="form-auth-request-otp">
|
<form id="form-auth-request-otp">
|
||||||
<div class="form-group" id="group-auth-name" style="display: none;">
|
<div class="form-group" style="text-align: center;">
|
||||||
<label class="form-label" for="auth-input-name">Nombre Completo</label>
|
<label class="form-label" for="auth-input-phone" style="font-size: 0.95rem; font-weight: 700; color: #fff; margin-bottom: 0.5rem;">
|
||||||
<input type="text" class="form-input" id="auth-input-name" placeholder="Ej: María Fernanda Gómez">
|
Número de Celular / WhatsApp (+57)
|
||||||
|
</label>
|
||||||
|
<input type="tel" class="form-input" id="auth-input-phone" placeholder="Ej: 316 745 5877 o 300 123 4567" required autofocus style="font-size: 1.15rem; font-weight: 700; text-align: center; letter-spacing: 0.05em; padding: 0.85rem;">
|
||||||
|
<span style="font-size: 0.76rem; color: var(--text-dim); margin-top: 6px; display: block;">
|
||||||
|
Recibirás un código de 6 dígitos al instante en tu WhatsApp para ingresar de forma segura.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<!-- Campos opcionales ocultos para compatibilidad -->
|
||||||
<label class="form-label" for="auth-input-phone">Número de WhatsApp (+57)</label>
|
<div id="group-auth-name" style="display: none;">
|
||||||
<input type="tel" class="form-input" id="auth-input-phone" placeholder="Ej: 300 123 4567" required>
|
<input type="text" id="auth-input-name" value="">
|
||||||
<span style="font-size: 0.74rem; color: var(--text-dim); margin-top: 4px; display: block;">Recibirás un código de 6 dígitos al instante en tu WhatsApp.</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: none;">
|
||||||
<div class="form-group">
|
<input type="email" id="auth-input-email" value="">
|
||||||
<label class="form-label" for="auth-input-email">Correo Electrónico</label>
|
|
||||||
<input type="email" class="form-input" id="auth-input-email" placeholder="correo@ejemplo.com">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="auth-request-error" style="display: none; color: #f87171; font-size: 0.85rem; margin-bottom: 1rem; text-align: center;"></div>
|
<div id="auth-request-error" style="display: none; color: #f87171; font-size: 0.85rem; margin-bottom: 1rem; text-align: center;"></div>
|
||||||
|
|
||||||
<button type="submit" class="btn-primary" id="btn-submit-send-otp" style="margin-top: 0.5rem;">
|
<button type="submit" class="btn-primary" id="btn-submit-send-otp" style="margin-top: 0.5rem; padding: 0.85rem; font-size: 1rem;">
|
||||||
📲 Enviar Código de Verificación OTP
|
📲 Enviar Código de Verificación OTP
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Selector Rápido de Roles para Auditoría y Pruebas -->
|
|
||||||
<div style="margin-top: 1.25rem; border-top: 1px solid var(--border-glass); padding-top: 1rem; text-align: center;">
|
|
||||||
<div style="font-size: 0.75rem; text-transform: uppercase; color: var(--accent-amber); font-weight: 700; margin-bottom: 0.5rem; letter-spacing: 0.05em;">
|
|
||||||
⚡ Acceso Rápido por Roles & Permisos
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 0.45rem; justify-content: center; flex-wrap: wrap;">
|
|
||||||
<button type="button" class="btn-role-quick-login" data-role="ADMIN" style="background: rgba(236, 72, 153, 0.15); border: 1px solid #ec4899; color: #f472b6; padding: 5px 10px; border-radius: 6px; font-size: 0.78rem; font-weight: 700; cursor: pointer;">
|
|
||||||
🛡️ Administrador (Carlos)
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn-role-quick-login" data-role="CONDUCTOR" style="background: rgba(6, 182, 212, 0.15); border: 1px solid #06b6d4; color: #38bdf8; padding: 5px 10px; border-radius: 6px; font-size: 0.78rem; font-weight: 700; cursor: pointer;">
|
|
||||||
🚗 Conductor (WLZ-890)
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn-role-quick-login" data-role="PASAJERO" style="background: rgba(16, 185, 129, 0.15); border: 1px solid #10b981; color: #34d399; padding: 5px 10px; border-radius: 6px; font-size: 0.78rem; font-weight: 700; cursor: pointer;">
|
|
||||||
👥 Pasajero Particular
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -276,6 +276,7 @@ function initTabs() {
|
|||||||
if (btnAdminLogout) {
|
if (btnAdminLogout) {
|
||||||
btnAdminLogout.addEventListener('click', () => {
|
btnAdminLogout.addEventListener('click', () => {
|
||||||
localStorage.removeItem('pt_token');
|
localStorage.removeItem('pt_token');
|
||||||
|
localStorage.removeItem('pt_token_expires_at');
|
||||||
currentUser = null;
|
currentUser = null;
|
||||||
if (window.updateUserUI) window.updateUserUI(null);
|
if (window.updateUserUI) window.updateUserUI(null);
|
||||||
window.location.hash = '#reservas';
|
window.location.hash = '#reservas';
|
||||||
@ -480,16 +481,23 @@ function initAuthSystem() {
|
|||||||
const nameInput = document.getElementById('auth-input-name');
|
const nameInput = document.getElementById('auth-input-name');
|
||||||
const btnSubmit = document.getElementById('btn-submit-send-otp');
|
const btnSubmit = document.getElementById('btn-submit-send-otp');
|
||||||
|
|
||||||
const phone = phoneInput?.value.trim() || '';
|
const rawPhone = phoneInput?.value.trim() || '';
|
||||||
const email = emailInput?.value.trim() || '';
|
const email = emailInput?.value.trim() || '';
|
||||||
const name = nameInput?.value.trim() || '';
|
const name = nameInput?.value.trim() || '';
|
||||||
|
|
||||||
if (!phone) {
|
let cleanPhone = rawPhone.replace(/\D/g, '');
|
||||||
showError(reqError, 'Ingresa tu número de WhatsApp.');
|
if (cleanPhone.startsWith('57') && cleanPhone.length === 12) {
|
||||||
|
// Prefijo 57 incluido
|
||||||
|
} else if (cleanPhone.length === 10) {
|
||||||
|
cleanPhone = '57' + cleanPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cleanPhone || cleanPhone.length < 10) {
|
||||||
|
showError(reqError, 'Ingresa un número de celular válido (Ej: 316 745 5877).');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentIdentifier = phone;
|
currentIdentifier = cleanPhone;
|
||||||
currentEmail = email;
|
currentEmail = email;
|
||||||
|
|
||||||
btnSubmit.disabled = true;
|
btnSubmit.disabled = true;
|
||||||
@ -500,14 +508,14 @@ function initAuthSystem() {
|
|||||||
const res = await fetch('/api/auth/send-otp', {
|
const res = await fetch('/api/auth/send-otp', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ identifier: phone, email })
|
body: JSON.stringify({ identifier: cleanPhone, email })
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (res.ok && data.success) {
|
if (res.ok && data.success) {
|
||||||
step1.style.display = 'none';
|
step1.style.display = 'none';
|
||||||
step2.style.display = 'block';
|
step2.style.display = 'block';
|
||||||
if (targetLabel) targetLabel.textContent = data.maskedPhone || `+${phone}`;
|
if (targetLabel) targetLabel.textContent = data.maskedPhone || `+${cleanPhone}`;
|
||||||
|
|
||||||
// Si el servidor devolvió preview (entorno local sin SIM de Evolution), mostrar aviso
|
// Si el servidor devolvió preview (entorno local sin SIM de Evolution), mostrar aviso
|
||||||
if (data.codePreview) {
|
if (data.codePreview) {
|
||||||
@ -620,7 +628,9 @@ function initAuthSystem() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (res.ok && data.success) {
|
if (res.ok && data.success) {
|
||||||
|
const expiresAt = data.expiresAt || (Date.now() + 24 * 60 * 60 * 1000);
|
||||||
localStorage.setItem('pt_token', data.token);
|
localStorage.setItem('pt_token', data.token);
|
||||||
|
localStorage.setItem('pt_token_expires_at', String(expiresAt));
|
||||||
currentUser = data.user;
|
currentUser = data.user;
|
||||||
updateUserUI(currentUser);
|
updateUserUI(currentUser);
|
||||||
modal.classList.remove('active');
|
modal.classList.remove('active');
|
||||||
@ -643,74 +653,11 @@ function initAuthSystem() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selector Rápido de Roles para Auditoría y Pruebas
|
|
||||||
document.querySelectorAll('.btn-role-quick-login').forEach(btn => {
|
|
||||||
btn.addEventListener('click', async () => {
|
|
||||||
const role = btn.getAttribute('data-role') || 'ADMIN';
|
|
||||||
|
|
||||||
// El ingreso como Administrador SIEMPRE exige verificación de Token/OTP por WhatsApp
|
|
||||||
if (role === 'ADMIN' || role === 'OPERADOR') {
|
|
||||||
const phoneInput = document.getElementById('auth-phone');
|
|
||||||
if (phoneInput) {
|
|
||||||
phoneInput.value = '3167455877';
|
|
||||||
}
|
|
||||||
authMode = 'login';
|
|
||||||
if (modeLoginBtn) modeLoginBtn.classList.add('active');
|
|
||||||
if (modeRegisterBtn) modeRegisterBtn.classList.remove('active');
|
|
||||||
if (groupName) groupName.style.display = 'none';
|
|
||||||
|
|
||||||
if (reqError) {
|
|
||||||
reqError.textContent = '🔒 Acceso de Administrador: Por políticas de seguridad 2FA, se enviará el código OTP a tu WhatsApp personal (+57 316 745 5877).';
|
|
||||||
reqError.style.color = '#38bdf8';
|
|
||||||
reqError.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enviar OTP vía WhatsApp
|
|
||||||
formStep1.dispatchEvent(new Event('submit'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
btn.disabled = true;
|
|
||||||
const originalText = btn.innerHTML;
|
|
||||||
btn.innerHTML = '⏳ Accediendo...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/auth/quick-role-login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ role })
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success && data.user) {
|
|
||||||
localStorage.setItem('pt_token', data.token);
|
|
||||||
currentUser = data.user;
|
|
||||||
updateUserUI(currentUser);
|
|
||||||
if (modal) modal.classList.remove('active');
|
|
||||||
|
|
||||||
if (role === 'CONDUCTOR') {
|
|
||||||
window.location.hash = '#conductor';
|
|
||||||
window.showView('driver');
|
|
||||||
} else {
|
|
||||||
window.location.hash = '#reservas';
|
|
||||||
window.showView('public');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
alert('Error de acceso rápido: ' + (data.error || 'Intenta de nuevo'));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Quick role error:', e);
|
|
||||||
alert('Error de conexión al servidor');
|
|
||||||
} finally {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = originalText;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cerrar sesión
|
// Cerrar sesión
|
||||||
if (btnLogout) {
|
if (btnLogout) {
|
||||||
btnLogout.addEventListener('click', () => {
|
btnLogout.addEventListener('click', () => {
|
||||||
localStorage.removeItem('pt_token');
|
localStorage.removeItem('pt_token');
|
||||||
|
localStorage.removeItem('pt_token_expires_at');
|
||||||
currentUser = null;
|
currentUser = null;
|
||||||
updateUserUI(null);
|
updateUserUI(null);
|
||||||
window.location.hash = '#reservas';
|
window.location.hash = '#reservas';
|
||||||
@ -718,24 +665,44 @@ function initAuthSystem() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validar sesión persistida
|
// Validar sesión persistida (vigencia de 24 horas)
|
||||||
checkPersistedSession();
|
checkPersistedSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkPersistedSession() {
|
async function checkPersistedSession() {
|
||||||
const token = localStorage.getItem('pt_token');
|
const token = localStorage.getItem('pt_token');
|
||||||
|
const expiresAt = localStorage.getItem('pt_token_expires_at');
|
||||||
|
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
|
// Comprobar lapso de 24 horas en el navegador
|
||||||
|
if (expiresAt && Date.now() > Number(expiresAt)) {
|
||||||
|
console.log('⏰ [Sesión Pacific Trans GO]: El token de 24 horas ha expirado. Requiere autenticación OTP.');
|
||||||
|
localStorage.removeItem('pt_token');
|
||||||
|
localStorage.removeItem('pt_token_expires_at');
|
||||||
|
currentUser = null;
|
||||||
|
updateUserUI(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/auth/me?token=${token}`);
|
const res = await fetch(`/api/auth/me?token=${encodeURIComponent(token)}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.user) {
|
if (data.user) {
|
||||||
currentUser = data.user;
|
currentUser = data.user;
|
||||||
|
if (data.user.tokenExpiresAt) {
|
||||||
|
localStorage.setItem('pt_token_expires_at', String(data.user.tokenExpiresAt));
|
||||||
|
}
|
||||||
updateUserUI(currentUser);
|
updateUserUI(currentUser);
|
||||||
|
console.log(`✅ [Sesión Autenticada]: ${currentUser.nombre} (+${currentUser.telefono}) activo por 24 horas.`);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('pt_token');
|
||||||
|
localStorage.removeItem('pt_token_expires_at');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('pt_token');
|
localStorage.removeItem('pt_token');
|
||||||
|
localStorage.removeItem('pt_token_expires_at');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('⚠️ [Session Verify Error]:', err);
|
console.warn('⚠️ [Session Verify Error]:', err);
|
||||||
|
|||||||
@ -103,29 +103,11 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const token = authHeader.replace(/^Bearer\s+/i, '') || url.searchParams.get('token') || '';
|
const token = authHeader.replace(/^Bearer\s+/i, '') || url.searchParams.get('token') || '';
|
||||||
const user = await authService.getUserFromToken(token);
|
const user = await authService.getUserFromToken(token);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return sendJson(401, { success: false, error: 'Sesión no válida o expirada' });
|
return sendJson(401, { success: false, error: 'Sesión no válida o expirada (lapso de 24 horas superado)' });
|
||||||
}
|
}
|
||||||
return sendJson(200, { success: true, user });
|
return sendJson(200, { success: true, user });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.4. Autenticación: Quick Role Login para pruebas de roles públicos (PROHIBIDO para ADMIN / OPERADOR)
|
|
||||||
if (pathname === '/api/auth/quick-role-login' && req.method === 'POST') {
|
|
||||||
const body = await getJsonBody();
|
|
||||||
const role = body.role || 'PASAJERO';
|
|
||||||
if (role === 'ADMIN' || role === 'OPERADOR') {
|
|
||||||
return sendJson(403, {
|
|
||||||
success: false,
|
|
||||||
error: 'Acceso no permitido: El ingreso al rol Administrador requiere obligatoriamente verificar el código OTP enviado a tu número personal de WhatsApp (+57 316 745 5877).'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = await authService.createSessionForRole(role, body.name);
|
|
||||||
return sendJson(200, { success: true, ...result });
|
|
||||||
} catch (err: any) {
|
|
||||||
return sendJson(403, { success: false, error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper de seguridad por roles y permisos en rutas API
|
// Helper de seguridad por roles y permisos en rutas API
|
||||||
const getToken = (): string => {
|
const getToken = (): string => {
|
||||||
const authHeader = (req.headers['authorization'] as string) || '';
|
const authHeader = (req.headers['authorization'] as string) || '';
|
||||||
|
|||||||
@ -14,6 +14,8 @@ interface OtpRecord {
|
|||||||
|
|
||||||
const OTP_CACHE = new Map<string, OtpRecord>();
|
const OTP_CACHE = new Map<string, OtpRecord>();
|
||||||
const OTP_EXPIRY_MS = 5 * 60 * 1000; // 5 minutos de validez
|
const OTP_EXPIRY_MS = 5 * 60 * 1000; // 5 minutos de validez
|
||||||
|
const SESSION_DURATION_MS = 24 * 60 * 60 * 1000; // 24 horas exactas de validez
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'pt_pacifictrans_go_secret_2026_banrep';
|
||||||
|
|
||||||
// Memoria de tokens para la plataforma web y API con soporte de fallback
|
// Memoria de tokens para la plataforma web y API con soporte de fallback
|
||||||
const SESSION_TOKENS = new Map<string, { userId: string; rol: UserRole; expiresAt: number; cachedUser?: any }>();
|
const SESSION_TOKENS = new Map<string, { userId: string; rol: UserRole; expiresAt: number; cachedUser?: any }>();
|
||||||
@ -99,6 +101,8 @@ export class AuthService {
|
|||||||
}): Promise<{
|
}): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
token?: string;
|
token?: string;
|
||||||
|
expiresAt?: number;
|
||||||
|
expiresIn?: number;
|
||||||
user?: any;
|
user?: any;
|
||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
@ -187,12 +191,22 @@ export class AuthService {
|
|||||||
await user.save();
|
await user.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generar Token de Sesión
|
// Generar Token de Sesión seguro firmado con expiración exacta de 24 horas
|
||||||
const token = `pt_sess_${crypto.randomBytes(24).toString('hex')}`;
|
const expiresAt = Date.now() + SESSION_DURATION_MS;
|
||||||
|
const tokenPayload = {
|
||||||
|
uid: user._id.toString(),
|
||||||
|
phone: cleanPhone,
|
||||||
|
rol: user.rol,
|
||||||
|
exp: expiresAt,
|
||||||
|
};
|
||||||
|
const b64 = Buffer.from(JSON.stringify(tokenPayload)).toString('base64url');
|
||||||
|
const sig = crypto.createHmac('sha256', JWT_SECRET).update(b64).digest('base64url');
|
||||||
|
const token = `pt_${b64}.${sig}`;
|
||||||
|
|
||||||
SESSION_TOKENS.set(token, {
|
SESSION_TOKENS.set(token, {
|
||||||
userId: user._id.toString(),
|
userId: user._id.toString(),
|
||||||
rol: user.rol,
|
rol: user.rol,
|
||||||
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 días
|
expiresAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
const userPayload = {
|
const userPayload = {
|
||||||
@ -208,11 +222,14 @@ export class AuthService {
|
|||||||
esSuperAdmin: isSuper || !!user.esSuperAdmin,
|
esSuperAdmin: isSuper || !!user.esSuperAdmin,
|
||||||
phoneVerified: user.phoneVerified,
|
phoneVerified: user.phoneVerified,
|
||||||
saldoPenalidad: user.saldoPenalidad,
|
saldoPenalidad: user.saldoPenalidad,
|
||||||
|
tokenExpiresAt: expiresAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
token,
|
token,
|
||||||
|
expiresAt,
|
||||||
|
expiresIn: 86400, // 24 horas en segundos
|
||||||
user: userPayload,
|
user: userPayload,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -222,14 +239,43 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
public async getUserFromToken(token: string): Promise<any | null> {
|
public async getUserFromToken(token: string): Promise<any | null> {
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
const session = SESSION_TOKENS.get(token);
|
|
||||||
if (!session || Date.now() > session.expiresAt) {
|
let userId: string | null = null;
|
||||||
if (session) SESSION_TOKENS.delete(token);
|
let expiresAt: number = 0;
|
||||||
return null;
|
|
||||||
|
// 1. Validar token firmado criptográficamente con vigencia estricta de 24 horas
|
||||||
|
if (token.startsWith('pt_') && token.includes('.')) {
|
||||||
|
try {
|
||||||
|
const [b64, sig] = token.slice(3).split('.');
|
||||||
|
const expectedSig = crypto.createHmac('sha256', JWT_SECRET).update(b64).digest('base64url');
|
||||||
|
if (sig === expectedSig) {
|
||||||
|
const payload = JSON.parse(Buffer.from(b64, 'base64url').toString('utf8'));
|
||||||
|
if (Date.now() > payload.exp) {
|
||||||
|
// El token superó el lapso de 24 horas
|
||||||
|
console.log(`⏰ [Token Expirado]: El token de usuario ${payload.uid} ha superado las 24 horas.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
userId = payload.uid;
|
||||||
|
expiresAt = payload.exp;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Formato no válido
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback a SESSION_TOKENS en memoria para tokens anteriores
|
||||||
|
if (!userId) {
|
||||||
|
const session = SESSION_TOKENS.get(token);
|
||||||
|
if (!session || Date.now() > session.expiresAt) {
|
||||||
|
if (session) SESSION_TOKENS.delete(token);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
userId = session.userId;
|
||||||
|
expiresAt = session.expiresAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await User.findById(session.userId);
|
const user = await User.findById(userId);
|
||||||
if (user) {
|
if (user) {
|
||||||
const isSuper = isPlatformSuperAdmin(user.telefono) || isPlatformSuperAdmin(user.email) || !!user.esSuperAdmin;
|
const isSuper = isPlatformSuperAdmin(user.telefono) || isPlatformSuperAdmin(user.email) || !!user.esSuperAdmin;
|
||||||
return {
|
return {
|
||||||
@ -246,13 +292,14 @@ export class AuthService {
|
|||||||
phoneVerified: user.phoneVerified,
|
phoneVerified: user.phoneVerified,
|
||||||
saldoPenalidad: user.saldoPenalidad,
|
saldoPenalidad: user.saldoPenalidad,
|
||||||
isActive: user.isActive,
|
isActive: user.isActive,
|
||||||
|
tokenExpiresAt: expiresAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (session.cachedUser) return session.cachedUser;
|
if (SESSION_TOKENS.get(token)?.cachedUser) return SESSION_TOKENS.get(token)?.cachedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
return session.cachedUser || null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -278,74 +325,6 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
return { allowed: true, user };
|
return { allowed: true, user };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Crea o reactiva una sesión rápida para pruebas (PROHIBIDO para ADMIN / OPERADOR)
|
|
||||||
* Los roles de administración requieren obligatoriamente verificación OTP por WhatsApp
|
|
||||||
*/
|
|
||||||
public async createSessionForRole(role: UserRole, customName?: string): Promise<{ token: string; user: any }> {
|
|
||||||
if (role === 'ADMIN' || role === 'OPERADOR') {
|
|
||||||
throw new Error(
|
|
||||||
'Acceso denegado: El rol Administrador requiere obligatoriamente verificación por código OTP enviado a tu número personal de WhatsApp (+57 316 745 5877).'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let phone = '573187654321';
|
|
||||||
let defaultName = 'Pasajero Frecuente Cali';
|
|
||||||
if (role === 'CONDUCTOR') {
|
|
||||||
phone = '573158923456';
|
|
||||||
defaultName = 'Mario Fernando G. (Renault Duster WLZ-890)';
|
|
||||||
}
|
|
||||||
|
|
||||||
let userDoc: any = null;
|
|
||||||
try {
|
|
||||||
userDoc = await User.findOne({ rol: role });
|
|
||||||
if (!userDoc) {
|
|
||||||
userDoc = await User.create({
|
|
||||||
appwriteId: `pt-${role.toLowerCase()}-${Date.now()}`,
|
|
||||||
nombre: customName || defaultName,
|
|
||||||
telefono: phone,
|
|
||||||
email: `${role.toLowerCase()}@pacifictrans.almeira.com.co`,
|
|
||||||
rol: role,
|
|
||||||
saldoPenalidad: 0,
|
|
||||||
coordenadasDomicilio: { type: 'Point', coordinates: [-76.532, 3.4516] },
|
|
||||||
isActive: true,
|
|
||||||
phoneVerified: true,
|
|
||||||
twoFactorEnabled: true,
|
|
||||||
twoFactorMethod: 'whatsapp_otp',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Continuar con usuario temporal si falla DB temporalmente
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = userDoc?._id ? userDoc._id.toString() : `role-${role.toLowerCase()}-${Date.now()}`;
|
|
||||||
const userPayload = {
|
|
||||||
id: userId,
|
|
||||||
_id: userId,
|
|
||||||
nombre: userDoc?.nombre || customName || defaultName,
|
|
||||||
telefono: userDoc?.telefono || phone,
|
|
||||||
email: userDoc?.email || `${role.toLowerCase()}@pacifictrans.almeira.com.co`,
|
|
||||||
rol: role,
|
|
||||||
esSuperAdmin: false,
|
|
||||||
phoneVerified: true,
|
|
||||||
saldoPenalidad: 0,
|
|
||||||
direccionTexto: 'Cali, Valle del Cauca',
|
|
||||||
};
|
|
||||||
|
|
||||||
const token = `pt_sess_${crypto.randomBytes(24).toString('hex')}`;
|
|
||||||
SESSION_TOKENS.set(token, {
|
|
||||||
userId,
|
|
||||||
rol: role,
|
|
||||||
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
|
||||||
cachedUser: userPayload,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
token,
|
|
||||||
user: userPayload,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authService = AuthService.getInstance();
|
export const authService = AuthService.getInstance();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user