-
Número de WhatsApp (+57)
-
-
Recibirás un código de 6 dígitos al instante en tu WhatsApp.
+
+
+
-
-
diff --git a/transporte-cali-buenaventura/backend/public/js/app.js b/transporte-cali-buenaventura/backend/public/js/app.js
index faa316f..434e4f3 100644
--- a/transporte-cali-buenaventura/backend/public/js/app.js
+++ b/transporte-cali-buenaventura/backend/public/js/app.js
@@ -276,6 +276,7 @@ function initTabs() {
if (btnAdminLogout) {
btnAdminLogout.addEventListener('click', () => {
localStorage.removeItem('pt_token');
+ localStorage.removeItem('pt_token_expires_at');
currentUser = null;
if (window.updateUserUI) window.updateUserUI(null);
window.location.hash = '#reservas';
@@ -480,16 +481,23 @@ function initAuthSystem() {
const nameInput = document.getElementById('auth-input-name');
const btnSubmit = document.getElementById('btn-submit-send-otp');
- const phone = phoneInput?.value.trim() || '';
+ const rawPhone = phoneInput?.value.trim() || '';
const email = emailInput?.value.trim() || '';
const name = nameInput?.value.trim() || '';
- if (!phone) {
- showError(reqError, 'Ingresa tu número de WhatsApp.');
+ let cleanPhone = rawPhone.replace(/\D/g, '');
+ 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;
}
- currentIdentifier = phone;
+ currentIdentifier = cleanPhone;
currentEmail = email;
btnSubmit.disabled = true;
@@ -500,14 +508,14 @@ function initAuthSystem() {
const res = await fetch('/api/auth/send-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ identifier: phone, email })
+ body: JSON.stringify({ identifier: cleanPhone, email })
});
const data = await res.json();
if (res.ok && data.success) {
step1.style.display = 'none';
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
if (data.codePreview) {
@@ -620,7 +628,9 @@ function initAuthSystem() {
const data = await res.json();
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_expires_at', String(expiresAt));
currentUser = data.user;
updateUserUI(currentUser);
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
if (btnLogout) {
btnLogout.addEventListener('click', () => {
localStorage.removeItem('pt_token');
+ localStorage.removeItem('pt_token_expires_at');
currentUser = null;
updateUserUI(null);
window.location.hash = '#reservas';
@@ -718,24 +665,44 @@ function initAuthSystem() {
});
}
- // Validar sesión persistida
+ // Validar sesión persistida (vigencia de 24 horas)
checkPersistedSession();
}
async function checkPersistedSession() {
const token = localStorage.getItem('pt_token');
+ const expiresAt = localStorage.getItem('pt_token_expires_at');
+
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 {
- const res = await fetch(`/api/auth/me?token=${token}`);
+ const res = await fetch(`/api/auth/me?token=${encodeURIComponent(token)}`);
if (res.ok) {
const data = await res.json();
if (data.user) {
currentUser = data.user;
+ if (data.user.tokenExpiresAt) {
+ localStorage.setItem('pt_token_expires_at', String(data.user.tokenExpiresAt));
+ }
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 {
localStorage.removeItem('pt_token');
+ localStorage.removeItem('pt_token_expires_at');
}
} catch (err) {
console.warn('⚠️ [Session Verify Error]:', err);
diff --git a/transporte-cali-buenaventura/backend/src/index.ts b/transporte-cali-buenaventura/backend/src/index.ts
index 1957ef6..42106ea 100644
--- a/transporte-cali-buenaventura/backend/src/index.ts
+++ b/transporte-cali-buenaventura/backend/src/index.ts
@@ -103,29 +103,11 @@ const server = http.createServer(async (req, res) => {
const token = authHeader.replace(/^Bearer\s+/i, '') || url.searchParams.get('token') || '';
const user = await authService.getUserFromToken(token);
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 });
}
- // 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
const getToken = (): string => {
const authHeader = (req.headers['authorization'] as string) || '';
diff --git a/transporte-cali-buenaventura/backend/src/services/auth.service.ts b/transporte-cali-buenaventura/backend/src/services/auth.service.ts
index 52fe316..1ba105d 100644
--- a/transporte-cali-buenaventura/backend/src/services/auth.service.ts
+++ b/transporte-cali-buenaventura/backend/src/services/auth.service.ts
@@ -14,6 +14,8 @@ interface OtpRecord {
const OTP_CACHE = new Map
();
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
const SESSION_TOKENS = new Map();
@@ -99,6 +101,8 @@ export class AuthService {
}): Promise<{
success: boolean;
token?: string;
+ expiresAt?: number;
+ expiresIn?: number;
user?: any;
error?: string;
}> {
@@ -187,12 +191,22 @@ export class AuthService {
await user.save();
}
- // Generar Token de Sesión
- const token = `pt_sess_${crypto.randomBytes(24).toString('hex')}`;
+ // Generar Token de Sesión seguro firmado con expiración exacta de 24 horas
+ 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, {
userId: user._id.toString(),
rol: user.rol,
- expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 días
+ expiresAt,
});
const userPayload = {
@@ -208,11 +222,14 @@ export class AuthService {
esSuperAdmin: isSuper || !!user.esSuperAdmin,
phoneVerified: user.phoneVerified,
saldoPenalidad: user.saldoPenalidad,
+ tokenExpiresAt: expiresAt,
};
return {
success: true,
token,
+ expiresAt,
+ expiresIn: 86400, // 24 horas en segundos
user: userPayload,
};
}
@@ -222,14 +239,43 @@ export class AuthService {
*/
public async getUserFromToken(token: string): Promise {
if (!token) return null;
- const session = SESSION_TOKENS.get(token);
- if (!session || Date.now() > session.expiresAt) {
- if (session) SESSION_TOKENS.delete(token);
- return null;
+
+ let userId: string | null = null;
+ let expiresAt: number = 0;
+
+ // 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 {
- const user = await User.findById(session.userId);
+ const user = await User.findById(userId);
if (user) {
const isSuper = isPlatformSuperAdmin(user.telefono) || isPlatformSuperAdmin(user.email) || !!user.esSuperAdmin;
return {
@@ -246,13 +292,14 @@ export class AuthService {
phoneVerified: user.phoneVerified,
saldoPenalidad: user.saldoPenalidad,
isActive: user.isActive,
+ tokenExpiresAt: expiresAt,
};
}
} 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 };
}
-
- /**
- * 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();