login realizado
This commit is contained in:
parent
c819bb5aab
commit
be8b0d0c38
585
public.php
585
public.php
@ -6,6 +6,7 @@ chdir(__DIR__);
|
|||||||
|
|
||||||
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
|
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
|
||||||
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
|
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
|
||||||
|
use Ubnt\UcrmPluginSdk\Service\UcrmSecurity;
|
||||||
use SmsNotifier\Service\PaymentIntentService;
|
use SmsNotifier\Service\PaymentIntentService;
|
||||||
|
|
||||||
// Carga manual del servicio
|
// Carga manual del servicio
|
||||||
@ -72,6 +73,119 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
// 0. LOGIN SCREEN SUPPORT & AUTHENTICATION
|
||||||
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
// Determinar si el usuario necesita login (si no tiene sesión UCRM activa)
|
||||||
|
$security = UcrmSecurity::create();
|
||||||
|
$currentUser = $security->getUser();
|
||||||
|
$needsLogin = !$currentUser;
|
||||||
|
|
||||||
|
$nmsBaseUrl = "https://{$ipServer}/nms/api/v2.1";
|
||||||
|
|
||||||
|
// 0a. NMS Login (POST username + password)
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_GET['action']) && $_GET['action'] === 'nms_login') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$username = $input['username'] ?? '';
|
||||||
|
$password = $input['password'] ?? '';
|
||||||
|
|
||||||
|
if (!$username || !$password) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Se requieren usuario y contraseña.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$nmsClient = new \GuzzleHttp\Client(['verify' => false, 'http_errors' => false]);
|
||||||
|
$resp = $nmsClient->post("{$nmsBaseUrl}/user/login", [
|
||||||
|
'json' => ['username' => $username, 'password' => $password],
|
||||||
|
'headers' => ['Content-Type' => 'application/json'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$statusCode = $resp->getStatusCode();
|
||||||
|
$body = json_decode($resp->getBody()->getContents(), true);
|
||||||
|
$authToken = $resp->getHeaderLine('x-auth-token');
|
||||||
|
|
||||||
|
if ($statusCode === 200 && $authToken) {
|
||||||
|
$logger->info("NMS Login OK for user: {$username}");
|
||||||
|
echo json_encode(['success' => true, 'token' => $authToken, 'user' => $body]);
|
||||||
|
} elseif ($statusCode === 201) {
|
||||||
|
$logger->info("NMS Login requires 2FA for user: {$username}");
|
||||||
|
http_response_code(201);
|
||||||
|
echo json_encode(['requires2FA' => true, 'twoFactorToken' => $body]);
|
||||||
|
} else {
|
||||||
|
$logger->warning("NMS Login failed for user: {$username} (HTTP {$statusCode})");
|
||||||
|
http_response_code($statusCode ?: 401);
|
||||||
|
echo json_encode(['error' => $body['message'] ?? 'Credenciales inválidas.', 'statusCode' => $statusCode]);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$logger->error("NMS Login exception: " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Error de conexión con el servidor UISP.']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0b. NMS TOTP Login
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_GET['action']) && $_GET['action'] === 'nms_login_totp') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$nmsClient = new \GuzzleHttp\Client(['verify' => false, 'http_errors' => false]);
|
||||||
|
$resp = $nmsClient->post("{$nmsBaseUrl}/user/login/totpauth", [
|
||||||
|
'json' => $input,
|
||||||
|
'headers' => ['Content-Type' => 'application/json'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$statusCode = $resp->getStatusCode();
|
||||||
|
$body = json_decode($resp->getBody()->getContents(), true);
|
||||||
|
$authToken = $resp->getHeaderLine('x-auth-token');
|
||||||
|
|
||||||
|
if ($statusCode === 200 && $authToken) {
|
||||||
|
$logger->info("NMS 2FA Login OK");
|
||||||
|
echo json_encode(['success' => true, 'token' => $authToken, 'user' => $body]);
|
||||||
|
} else {
|
||||||
|
http_response_code($statusCode ?: 401);
|
||||||
|
echo json_encode(['error' => $body['message'] ?? 'Código TOTP inválido.']);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Error de conexión con el servidor UISP.']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0c. NMS Verify Session
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action']) && $_GET['action'] === 'nms_verify_session') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$token = $_SERVER['HTTP_X_AUTH_TOKEN'] ?? '';
|
||||||
|
if (!$token) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'No token provided.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$nmsClient = new \GuzzleHttp\Client(['verify' => false, 'http_errors' => false]);
|
||||||
|
$resp = $nmsClient->get("{$nmsBaseUrl}/user", [
|
||||||
|
'headers' => ['x-auth-token' => $token],
|
||||||
|
]);
|
||||||
|
if ($resp->getStatusCode() === 200) {
|
||||||
|
echo json_encode(['success' => true, 'user' => json_decode($resp->getBody()->getContents(), true)]);
|
||||||
|
} else {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Sesión inválida o expirada.']);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Error verificando sesión.']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// API HANDLERS
|
// API HANDLERS
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
if ($_POST['action'] === 'save_installers') {
|
if ($_POST['action'] === 'save_installers') {
|
||||||
@ -988,10 +1102,272 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
|
|||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Login Overlay ─────────────────────────────────────── */
|
||||||
|
#loginOverlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 99999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
transition: opacity 0.5s ease, visibility 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginOverlay.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
width: 400px;
|
||||||
|
max-width: 92vw;
|
||||||
|
background: rgba(30, 41, 59, 0.85);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 48px 36px 36px;
|
||||||
|
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04);
|
||||||
|
text-align: center;
|
||||||
|
animation: loginSlideIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loginSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px) scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
object-fit: contain;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
filter: drop-shadow(0 4px 20px rgba(99, 102, 241, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0 0 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-field {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-field label {
|
||||||
|
display: block;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-field input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: rgba(15, 23, 42, 0.7);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #f1f5f9;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.25s, box-shadow 0.25s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-field input:focus {
|
||||||
|
border-color: #6366f1;
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-field input::placeholder {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 13px;
|
||||||
|
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s, box-shadow 0.25s;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 8px 30px rgba(99, 102, 241, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-error {
|
||||||
|
background: rgba(239, 68, 68, 0.12);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.25);
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-error.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: loginSpin 0.6s linear infinite;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loginSpin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginTotpSection {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginTotpSection.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
margin-top: 24px;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer a {
|
||||||
|
color: #6366f1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Particles background */
|
||||||
|
.login-particles {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-particles span {
|
||||||
|
position: absolute;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(99, 102, 241, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: loginFloat 12s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loginFloat {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0) translateX(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
10% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
90% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(-400px) translateX(100px);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<!-- ━━━ Login Overlay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
|
||||||
|
<div id="loginOverlay">
|
||||||
|
<div class="login-particles" id="loginParticles"></div>
|
||||||
|
<div class="login-card">
|
||||||
|
<img src="?action=image&file=whatsapp-logo.png" class="login-logo" alt="SIIP">
|
||||||
|
<h2 class="login-title">SIIP Notifications</h2>
|
||||||
|
<p class="login-subtitle">Inicia sesión con tu cuenta de UISP</p>
|
||||||
|
|
||||||
|
<div class="login-error" id="loginError"></div>
|
||||||
|
|
||||||
|
<!-- Formulario principal -->
|
||||||
|
<div id="loginFormSection">
|
||||||
|
<div class="login-field">
|
||||||
|
<label for="loginUsername">Usuario</label>
|
||||||
|
<input type="text" id="loginUsername" placeholder="Ingresa tu usuario" autocomplete="username" autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="login-field">
|
||||||
|
<label for="loginPassword">Contraseña</label>
|
||||||
|
<input type="password" id="loginPassword" placeholder="Ingresa tu contraseña" autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<button class="login-btn" id="loginBtn" onclick="handleLogin()">Iniciar Sesión</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sección 2FA (oculta por defecto) -->
|
||||||
|
<div id="loginTotpSection">
|
||||||
|
<div class="login-field">
|
||||||
|
<label for="loginTotpCode">Código de autenticación (2FA)</label>
|
||||||
|
<input type="text" id="loginTotpCode" placeholder="Código de 6 dígitos" maxlength="6" inputmode="numeric" autocomplete="one-time-code">
|
||||||
|
</div>
|
||||||
|
<button class="login-btn" id="loginTotpBtn" onclick="handleTotpLogin()">Verificar Código</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-footer">
|
||||||
|
Acceso restringido a administradores UISP · SIIP © <?php echo date('Y'); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- HEADER -->
|
<!-- HEADER -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@ -1482,6 +1858,9 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
|
|||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const NEEDS_LOGIN = <?php echo $needsLogin ? 'true' : 'false'; ?>;
|
||||||
|
let SYSTEM_USER_ID = <?php echo $currentUser ? $currentUser->userId : 'null'; ?>;
|
||||||
|
|
||||||
const store = {
|
const store = {
|
||||||
installers: <?php echo json_encode($installersData['instaladores']); ?>,
|
installers: <?php echo json_encode($installersData['instaladores']); ?>,
|
||||||
theme: localStorage.getItem('theme') || 'light',
|
theme: localStorage.getItem('theme') || 'light',
|
||||||
@ -2213,6 +2592,212 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
|
|||||||
return `<span style="background-color: ${bg}; color: ${color}; padding: 4px 10px; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; text-transform: capitalize;">${label}</span>`;
|
return `<span style="background-color: ${bg}; color: ${color}; padding: 4px 10px; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; text-transform: capitalize;">${label}</span>`;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
// ━━━ Login Logic ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
(function() {
|
||||||
|
const ALLOWED_ROLES = ['admin', 'superadmin'];
|
||||||
|
let _twoFactorData = null;
|
||||||
|
|
||||||
|
// Generar partículas decorativas
|
||||||
|
const particlesEl = document.getElementById('loginParticles');
|
||||||
|
if (particlesEl) {
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const s = document.createElement('span');
|
||||||
|
s.style.left = Math.random() * 100 + '%';
|
||||||
|
s.style.top = (60 + Math.random() * 40) + '%';
|
||||||
|
s.style.animationDelay = (Math.random() * 12) + 's';
|
||||||
|
s.style.animationDuration = (8 + Math.random() * 8) + 's';
|
||||||
|
particlesEl.appendChild(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoginError(msg) {
|
||||||
|
const el = document.getElementById('loginError');
|
||||||
|
el.textContent = msg;
|
||||||
|
el.classList.add('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoginError() {
|
||||||
|
document.getElementById('loginError').classList.remove('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLoginLoading(loading) {
|
||||||
|
const btn = document.getElementById('loginBtn');
|
||||||
|
const totpBtn = document.getElementById('loginTotpBtn');
|
||||||
|
if (loading) {
|
||||||
|
btn.disabled = true;
|
||||||
|
totpBtn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="login-spinner"></span>Autenticando...';
|
||||||
|
totpBtn.innerHTML = '<span class="login-spinner"></span>Verificando...';
|
||||||
|
} else {
|
||||||
|
btn.disabled = false;
|
||||||
|
totpBtn.disabled = false;
|
||||||
|
btn.textContent = 'Iniciar Sesión';
|
||||||
|
totpBtn.textContent = 'Verificar Código';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLoginSuccess(token, user) {
|
||||||
|
sessionStorage.setItem('nms_auth_token', token);
|
||||||
|
sessionStorage.setItem('nms_user', JSON.stringify(user));
|
||||||
|
// Actualizar SYSTEM_USER_ID global
|
||||||
|
SYSTEM_USER_ID = user.id || user.userId || null;
|
||||||
|
console.log('Login OK — User:', user.username, 'Role:', user.role, 'ID:', SYSTEM_USER_ID);
|
||||||
|
|
||||||
|
// Ocultar overlay con animación
|
||||||
|
const overlay = document.getElementById('loginOverlay');
|
||||||
|
overlay.classList.add('hidden');
|
||||||
|
setTimeout(() => {
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.handleLogin = async function() {
|
||||||
|
hideLoginError();
|
||||||
|
const username = document.getElementById('loginUsername').value.trim();
|
||||||
|
const password = document.getElementById('loginPassword').value;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
showLoginError('Ingresa usuario y contraseña.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoginLoading(true);
|
||||||
|
try {
|
||||||
|
const resp = await fetch('?action=nms_login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
if (data.success && data.token) {
|
||||||
|
// Verificar rol
|
||||||
|
const role = data.user?.role || '';
|
||||||
|
if (!ALLOWED_ROLES.includes(role)) {
|
||||||
|
showLoginError(`Acceso denegado. Tu rol (${role}) no tiene permisos para este portal.`);
|
||||||
|
setLoginLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onLoginSuccess(data.token, data.user);
|
||||||
|
} else if (data.requires2FA) {
|
||||||
|
_twoFactorData = data.twoFactorToken;
|
||||||
|
document.getElementById('loginFormSection').style.display = 'none';
|
||||||
|
document.getElementById('loginTotpSection').classList.add('visible');
|
||||||
|
document.getElementById('loginTotpCode').focus();
|
||||||
|
setLoginLoading(false);
|
||||||
|
} else {
|
||||||
|
showLoginError(data.error || 'Credenciales inválidas.');
|
||||||
|
setLoginLoading(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showLoginError('Error de conexión. Intenta de nuevo.');
|
||||||
|
setLoginLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.handleTotpLogin = async function() {
|
||||||
|
hideLoginError();
|
||||||
|
const code = document.getElementById('loginTotpCode').value.trim();
|
||||||
|
if (!code || code.length < 6) {
|
||||||
|
showLoginError('Ingresa el código de 6 dígitos.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoginLoading(true);
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
..._twoFactorData,
|
||||||
|
totpCode: code
|
||||||
|
};
|
||||||
|
const resp = await fetch('?action=nms_login_totp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success && data.token) {
|
||||||
|
const role = data.user?.role || '';
|
||||||
|
if (!ALLOWED_ROLES.includes(role)) {
|
||||||
|
showLoginError(`Acceso denegado. Tu rol (${role}) no tiene permisos.`);
|
||||||
|
setLoginLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onLoginSuccess(data.token, data.user);
|
||||||
|
} else {
|
||||||
|
showLoginError(data.error || 'Código TOTP inválido.');
|
||||||
|
setLoginLoading(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showLoginError('Error de conexión. Intenta de nuevo.');
|
||||||
|
setLoginLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verificar sesión almacenada al cargar
|
||||||
|
async function verifyStoredSession() {
|
||||||
|
const token = sessionStorage.getItem('nms_auth_token');
|
||||||
|
if (!token) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch('?action=nms_verify_session', {
|
||||||
|
headers: {
|
||||||
|
'x-auth-token': token
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success && data.user) {
|
||||||
|
const role = data.user?.role || '';
|
||||||
|
if (ALLOWED_ROLES.includes(role)) {
|
||||||
|
onLoginSuccess(token, data.user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* token inválido, mostrar login */
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionStorage.removeItem('nms_auth_token');
|
||||||
|
sessionStorage.removeItem('nms_user');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter key handlers
|
||||||
|
document.getElementById('loginPassword')?.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter') handleLogin();
|
||||||
|
});
|
||||||
|
document.getElementById('loginUsername')?.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter') document.getElementById('loginPassword').focus();
|
||||||
|
});
|
||||||
|
document.getElementById('loginTotpCode')?.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter') handleTotpLogin();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inicialización: decidir si mostrar login o portal
|
||||||
|
if (NEEDS_LOGIN) {
|
||||||
|
// No hay sesión UCRM, intentar con token almacenado
|
||||||
|
verifyStoredSession().then(valid => {
|
||||||
|
if (!valid) {
|
||||||
|
document.getElementById('loginOverlay').style.display = 'flex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Sesión UCRM activa, ocultar login overlay
|
||||||
|
const overlay = document.getElementById('loginOverlay');
|
||||||
|
overlay.classList.add('hidden');
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user