login realizado

This commit is contained in:
DANYDHSV 2026-02-19 12:28:15 -06:00
parent c819bb5aab
commit be8b0d0c38

View File

@ -6,6 +6,7 @@ chdir(__DIR__);
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\UcrmSecurity;
use SmsNotifier\Service\PaymentIntentService;
// 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
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($_POST['action'] === 'save_installers') {
@ -988,10 +1102,272 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
border-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>
</head>
<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">
<!-- HEADER -->
<div class="header">
@ -1482,6 +1858,9 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
<div id="toast"></div>
<script>
const NEEDS_LOGIN = <?php echo $needsLogin ? 'true' : 'false'; ?>;
let SYSTEM_USER_ID = <?php echo $currentUser ? $currentUser->userId : 'null'; ?>;
const store = {
installers: <?php echo json_encode($installersData['instaladores']); ?>,
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>`;
}
</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>
</html>