fix(settings): corregir desincronizacion de metodos de autenticacion, normalizar claves de proveedores y añadir badges reactivos de estado

This commit is contained in:
2026-09-23 17:52:45 -03:00
parent b20e206a76
commit 5771476ee9
5 changed files with 330 additions and 38 deletions
+1
View File
@@ -218,6 +218,7 @@ app.use('/api/v1', async (req, res) => {
const targetUrl = `${process.env.FLASK_API_URL || 'http://localhost:5000/api/v1'}${req.url}`;
const headers = { ...req.headers };
delete headers.host;
delete headers['content-length'];
if (token && !headers.authorization) {
headers.authorization = `Bearer ${token}`;
}
+68 -1
View File
@@ -1321,6 +1321,43 @@ const handleGlobalSettings = async (req, res) => {
console.warn('Global settings fetch notice:', e.message);
}
// Normalización robusta de auth_providers para asegurar compatibilidad total con Nunjucks
if (settings && settings.auth_providers) {
const ap = settings.auth_providers;
const isLocal = ap.local !== undefined ? Boolean(ap.local) : (ap.local_enabled !== undefined ? Boolean(ap.local_enabled) : true);
const isGoogle = ap.google !== undefined ? Boolean(ap.google) : (ap.google_enabled !== undefined ? Boolean(ap.google_enabled) : false);
const isMoodle = ap.moodle !== undefined ? Boolean(ap.moodle) : (ap.moodle_enabled !== undefined ? Boolean(ap.moodle_enabled) : false);
settings.auth_providers = {
local: isLocal,
local_enabled: isLocal,
google: isGoogle,
google_enabled: isGoogle,
moodle: isMoodle,
moodle_enabled: isMoodle
};
} else {
settings.auth_providers = {
local: true,
local_enabled: true,
google: false,
google_enabled: false,
moodle: false,
moodle_enabled: false
};
}
if (settings && settings.smtp) {
settings.smtp.password_masked = settings.smtp.password_masked || settings.smtp.password || '';
settings.smtp.from_email = settings.smtp.from_email || settings.smtp.sender_email || '';
settings.smtp.from_name = settings.smtp.from_name || settings.smtp.sender_name || 'Admin Edu-Space';
}
if (settings && settings.google_oauth) {
settings.google_oauth.client_secret_masked = settings.google_oauth.client_secret_masked || settings.google_oauth.client_secret || '';
}
if (settings && settings.moodle) {
settings.moodle.ws_token_masked = settings.moodle.ws_token_masked || settings.moodle.ws_token || '';
}
try {
const demoResp = await req.apiClient.get('/settings/demo-mode');
if (demoResp.data) {
@@ -1359,6 +1396,37 @@ router.post('/settings/demo-mode', async (req, res) => {
}
});
// Endpoint explícito para consultar y actualizar métodos de autenticación
router.get('/settings/auth-providers', async (req, res) => {
try {
const resp = await req.apiClient.get('/settings/all');
const ap = resp.data?.settings?.auth_providers || resp.data?.data?.auth_providers || {};
return res.json({
status: 'success',
auth_providers: {
local: ap.local !== undefined ? Boolean(ap.local) : (ap.local_enabled !== undefined ? Boolean(ap.local_enabled) : true),
google: ap.google !== undefined ? Boolean(ap.google) : (ap.google_enabled !== undefined ? Boolean(ap.google_enabled) : false),
moodle: ap.moodle !== undefined ? Boolean(ap.moodle) : (ap.moodle_enabled !== undefined ? Boolean(ap.moodle_enabled) : false)
}
});
} catch (err) {
return res.status(500).json({ status: 'error', message: err.message });
}
});
router.post('/settings/auth-providers', async (req, res) => {
try {
const resp = await req.apiClient.post('/settings/auth-providers', req.body);
return res.status(resp.status || 200).json(resp.data);
} catch (err) {
console.error('Error in POST /admin/settings/auth-providers:', err.response?.data || err.message);
return res.status(err.response?.status || 500).json(err.response?.data || {
status: 'error',
message: 'Error al actualizar métodos de autenticación: ' + (err.response?.data?.message || err.message)
});
}
});
// Función forwarder para acciones de configuración hacia Flask
const forwardSettingsAction = async (req, res) => {
try {
@@ -1381,7 +1449,6 @@ const forwardSettingsAction = async (req, res) => {
router.post('/settings/smtp', forwardSettingsAction);
router.post('/settings/smtp/test', forwardSettingsAction);
router.post('/settings/auth-providers', forwardSettingsAction);
router.post('/settings/google-oauth', forwardSettingsAction);
router.post('/settings/moodle', forwardSettingsAction);
router.post('/settings/moodle/test', forwardSettingsAction);
+178 -32
View File
@@ -186,44 +186,90 @@
<div class="tab-pane fade" id="tab-sso" role="tabpanel">
<div class="row g-4">
<div class="col-lg-6">
{% set is_local_active = (not settings or not settings.auth_providers or settings.auth_providers.local or settings.auth_providers.local_enabled) %}
{% set is_google_active = (settings and settings.auth_providers and (settings.auth_providers.google or settings.auth_providers.google_enabled)) %}
{% set is_moodle_active = (settings and settings.auth_providers and (settings.auth_providers.moodle or settings.auth_providers.moodle_enabled)) %}
<div class="card border-0 bg-body-tertiary p-3 rounded-3 mb-3">
<h6 class="fw-bold mb-3 d-flex align-items-center">
<i class="bi bi-toggle2-on text-primary me-2"></i>Métodos de Autenticación Habilitados
</h6>
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-bold mb-0 d-flex align-items-center">
<i class="bi bi-toggle2-on text-primary me-2"></i>Métodos de Autenticación Habilitados
</h6>
<span class="badge bg-primary-subtle text-primary border border-primary-subtle" style="font-size: 0.72rem;">
Políticas de Acceso
</span>
</div>
<form id="providersForm" onsubmit="saveAuthProviders(event)">
<div class="d-flex align-items-center justify-content-between p-2 rounded mb-2 border bg-body">
<div>
<div class="fw-semibold small">Login Nativo con Contraseña</div>
<small class="text-muted" style="font-size: 11px;">Permite inicio de sesión local. Si se desactiva, queda reservado exclusivamente para rol ADMIN como contingencia.</small>
</div>
<div class="form-check form-switch fs-5 ms-3">
<input class="form-check-input" type="checkbox" id="auth_local_enabled" {% if not settings or not settings.auth_providers or settings.auth_providers.local %}checked{% endif %}>
<!-- Login Local -->
<div class="p-2 rounded mb-2 border bg-body">
<div class="d-flex align-items-center justify-content-between mb-1">
<div class="d-flex align-items-center">
<i class="bi bi-key-fill text-primary me-2 fs-6"></i>
<span class="fw-semibold small">Login Nativo con Contraseña</span>
</div>
<div class="d-flex align-items-center gap-2">
<span id="badgeLocalStatus" class="badge {% if is_local_active %}bg-success-subtle text-success border border-success-subtle{% else %}bg-danger-subtle text-danger border border-danger-subtle{% endif %}" style="font-size: 0.72rem;">
{% if is_local_active %}Habilitado{% else %}Desactivado{% endif %}
</span>
<div class="form-check form-switch fs-5 m-0">
<input class="form-check-input" type="checkbox" id="auth_local_enabled" onchange="updateProviderStatusBadges()" {% if is_local_active %}checked{% endif %}>
</div>
</div>
</div>
<small class="text-muted d-block ps-4" style="font-size: 11px;">Permite inicio de sesión local para alumnos, docentes y bedelía. Si se desactiva, queda reservado exclusivamente para rol ADMIN como contingencia.</small>
</div>
<div class="d-flex align-items-center justify-content-between p-2 rounded mb-2 border bg-body">
<div>
<div class="fw-semibold small">Google OAuth 2.0 (Google Workspace)</div>
<small class="text-muted" style="font-size: 11px;">Inicio de sesión con cuentas institucionales @unicaba.edu.ar mediante SSO.</small>
</div>
<div class="form-check form-switch fs-5 ms-3">
<input class="form-check-input" type="checkbox" id="auth_google_enabled" {% if settings and settings.auth_providers and settings.auth_providers.google %}checked{% endif %}>
<!-- Google OAuth -->
<div class="p-2 rounded mb-2 border bg-body">
<div class="d-flex align-items-center justify-content-between mb-1">
<div class="d-flex align-items-center">
<i class="bi bi-google text-danger me-2 fs-6"></i>
<span class="fw-semibold small">Google OAuth 2.0 (Google Workspace)</span>
</div>
<div class="d-flex align-items-center gap-2">
<span id="badgeGoogleStatus" class="badge {% if is_google_active %}bg-success-subtle text-success border border-success-subtle{% else %}bg-secondary-subtle text-secondary border border-secondary-subtle{% endif %}" style="font-size: 0.72rem;">
{% if is_google_active %}Habilitado{% else %}Desactivado{% endif %}
</span>
<div class="form-check form-switch fs-5 m-0">
<input class="form-check-input" type="checkbox" id="auth_google_enabled" onchange="updateProviderStatusBadges()" {% if is_google_active %}checked{% endif %}>
</div>
</div>
</div>
<small class="text-muted d-block ps-4" style="font-size: 11px;">Inicio de sesión con cuentas institucionales @unicaba.edu.ar mediante Single Sign-On (SSO).</small>
</div>
<div class="d-flex align-items-center justify-content-between p-2 rounded mb-3 border bg-body">
<div>
<div class="fw-semibold small">Moodle SSO Delegado</div>
<small class="text-muted" style="font-size: 11px;">Valida credenciales contra el servidor de Moodle 4.1 y aprovisiona perfil.</small>
</div>
<div class="form-check form-switch fs-5 ms-3">
<input class="form-check-input" type="checkbox" id="auth_moodle_enabled" {% if settings and settings.auth_providers and settings.auth_providers.moodle %}checked{% endif %}>
<!-- Moodle SSO -->
<div class="p-2 rounded mb-3 border bg-body">
<div class="d-flex align-items-center justify-content-between mb-1">
<div class="d-flex align-items-center">
<i class="bi bi-mortarboard-fill text-warning me-2 fs-6"></i>
<span class="fw-semibold small">Moodle SSO Delegado</span>
</div>
<div class="d-flex align-items-center gap-2">
<span id="badgeMoodleStatus" class="badge {% if is_moodle_active %}bg-success-subtle text-success border border-success-subtle{% else %}bg-secondary-subtle text-secondary border border-secondary-subtle{% endif %}" style="font-size: 0.72rem;">
{% if is_moodle_active %}Habilitado{% else %}Desactivado{% endif %}
</span>
<div class="form-check form-switch fs-5 m-0">
<input class="form-check-input" type="checkbox" id="auth_moodle_enabled" onchange="updateProviderStatusBadges()" {% if is_moodle_active %}checked{% endif %}>
</div>
</div>
</div>
<small class="text-muted d-block ps-4" style="font-size: 11px;">Valida credenciales contra el servidor de Moodle 4.1 y auto-aprovisiona perfiles.</small>
</div>
<button type="submit" class="btn btn-sm btn-primary" id="btnSaveProviders">
<i class="bi bi-save me-1"></i>Actualizar Métodos de Login
</button>
<!-- Aviso de contingencia si local está apagado -->
<div id="alertLocalDisabled" class="alert alert-warning py-2 px-3 small mb-3 {% if is_local_active %}d-none{% endif %}" style="font-size: 11px;">
<i class="bi bi-exclamation-triangle-fill me-1"></i>
<strong>Atención de Contingencia:</strong> Al apagar el Login Nativo, solo los usuarios con rol <code>ADMIN</code> podrán iniciar sesión con usuario y contraseña local.
</div>
<div class="d-flex justify-content-between align-items-center">
<button type="submit" class="btn btn-sm btn-primary px-3" id="btnSaveProviders">
<i class="bi bi-save me-1"></i>Guardar Métodos de Autenticación
</button>
<button type="button" class="btn btn-sm btn-link text-decoration-none text-muted py-0" onclick="refreshAuthProvidersLive()" title="Recargar estado desde servidor">
<i class="bi bi-arrow-clockwise me-1"></i>Sincronizar
</button>
</div>
</form>
</div>
@@ -569,6 +615,79 @@ async function runSmtpTest() {
}
}
// -------------------------------------------------------------
// Reactividad Dinámica de Badges de Métodos de Autenticación
// -------------------------------------------------------------
function updateProviderStatusBadges() {
const localCheck = document.getElementById('auth_local_enabled');
const googleCheck = document.getElementById('auth_google_enabled');
const moodleCheck = document.getElementById('auth_moodle_enabled');
const badgeLocal = document.getElementById('badgeLocalStatus');
const badgeGoogle = document.getElementById('badgeGoogleStatus');
const badgeMoodle = document.getElementById('badgeMoodleStatus');
const alertLocal = document.getElementById('alertLocalDisabled');
if (localCheck && badgeLocal) {
if (localCheck.checked) {
badgeLocal.className = 'badge bg-success-subtle text-success border border-success-subtle';
badgeLocal.textContent = 'Habilitado';
if (alertLocal) alertLocal.classList.add('d-none');
} else {
badgeLocal.className = 'badge bg-danger-subtle text-danger border border-danger-subtle';
badgeLocal.textContent = 'Desactivado';
if (alertLocal) alertLocal.classList.remove('d-none');
}
}
if (googleCheck && badgeGoogle) {
if (googleCheck.checked) {
badgeGoogle.className = 'badge bg-success-subtle text-success border border-success-subtle';
badgeGoogle.textContent = 'Habilitado';
} else {
badgeGoogle.className = 'badge bg-secondary-subtle text-secondary border border-secondary-subtle';
badgeGoogle.textContent = 'Desactivado';
}
}
if (moodleCheck && badgeMoodle) {
if (moodleCheck.checked) {
badgeMoodle.className = 'badge bg-success-subtle text-success border border-success-subtle';
badgeMoodle.textContent = 'Habilitado';
} else {
badgeMoodle.className = 'badge bg-secondary-subtle text-secondary border border-secondary-subtle';
badgeMoodle.textContent = 'Desactivado';
}
}
}
// -------------------------------------------------------------
// Sincronización en Vivo de Métodos de Autenticación desde API
// -------------------------------------------------------------
async function refreshAuthProvidersLive() {
try {
const res = await fetch('/admin/settings/auth-providers');
if (res.ok) {
const data = await res.json();
if (data && data.auth_providers) {
const ap = data.auth_providers;
const localCheck = document.getElementById('auth_local_enabled');
const googleCheck = document.getElementById('auth_google_enabled');
const moodleCheck = document.getElementById('auth_moodle_enabled');
if (localCheck && ap.local !== undefined) localCheck.checked = Boolean(ap.local);
if (googleCheck && ap.google !== undefined) googleCheck.checked = Boolean(ap.google);
if (moodleCheck && ap.moodle !== undefined) moodleCheck.checked = Boolean(ap.moodle);
updateProviderStatusBadges();
showAlert('Estados de métodos de autenticación sincronizados con el servidor.', 'info');
}
}
} catch (e) {
console.warn('Error al sincronizar auth providers en vivo:', e.message);
}
}
// -------------------------------------------------------------
// Guardar Métodos de Autenticación
// -------------------------------------------------------------
@@ -580,19 +699,42 @@ async function saveAuthProviders(e) {
const payload = {
local_enabled: document.getElementById('auth_local_enabled').checked,
local: document.getElementById('auth_local_enabled').checked,
google_enabled: document.getElementById('auth_google_enabled').checked,
moodle_enabled: document.getElementById('auth_moodle_enabled').checked
google: document.getElementById('auth_google_enabled').checked,
moodle_enabled: document.getElementById('auth_moodle_enabled').checked,
moodle: document.getElementById('auth_moodle_enabled').checked
};
try {
const res = await fetch('/api/v1/admin/settings/auth-providers', {
let res = await fetch('/admin/settings/auth-providers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(payload)
});
if (!res.ok) {
// Reintento por endpoint API directo
res = await fetch('/api/v1/admin/settings/auth-providers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
const data = await res.json();
if (res.ok) {
showAlert(data.message || 'Métodos de acceso actualizados.', 'success');
if (data.auth_providers) {
const ap = data.auth_providers;
if (ap.local !== undefined) document.getElementById('auth_local_enabled').checked = Boolean(ap.local);
if (ap.google !== undefined) document.getElementById('auth_google_enabled').checked = Boolean(ap.google);
if (ap.moodle !== undefined) document.getElementById('auth_moodle_enabled').checked = Boolean(ap.moodle);
}
updateProviderStatusBadges();
showAlert(data.message || 'Métodos de autenticación actualizados y guardados exitosamente.', 'success');
} else {
showAlert(data.message || 'Error al actualizar métodos de acceso.', 'danger');
}
@@ -600,10 +742,14 @@ async function saveAuthProviders(e) {
showAlert('Error de comunicación: ' + err.message, 'danger');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-save me-1"></i>Actualizar Métodos de Login';
btn.innerHTML = '<i class="bi bi-save me-1"></i>Guardar Métodos de Autenticación';
}
}
document.addEventListener('DOMContentLoaded', () => {
updateProviderStatusBadges();
});
// -------------------------------------------------------------
// Alternar Modo Demo y Cuentas de Prueba Default
// -------------------------------------------------------------