feat(phase5): integracion auth hibrida, email dinamico y moodle 4.1 backend con tolerancia a fallos
This commit is contained in:
@@ -0,0 +1,777 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Configuración Global del Sistema - Admin Edu-Space{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.settings-nav-tabs .nav-link {
|
||||
color: var(--bs-secondary-color);
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
padding: 0.75rem 1.25rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.settings-nav-tabs .nav-link.active {
|
||||
color: #B43E8E;
|
||||
border-bottom: 2px solid #B43E8E;
|
||||
background: transparent;
|
||||
font-weight: 600;
|
||||
}
|
||||
[data-bs-theme="dark"] .settings-nav-tabs .nav-link.active {
|
||||
color: #FF8AB6;
|
||||
border-bottom-color: #FF8AB6;
|
||||
}
|
||||
.config-card {
|
||||
border-radius: 12px;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
.stat-badge-pill {
|
||||
border-radius: 30px;
|
||||
font-size: 0.82rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
}
|
||||
.dlq-alert {
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
.pass-toggle-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-4 pb-5 px-3 px-md-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h4 mb-1 d-flex align-items-center">
|
||||
<i class="bi bi-gear-wide-connected text-primary me-2"></i>
|
||||
<span>Panel de Configuración Global</span>
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">Gestión centralizada de Servidor de Correo SMTP, Métodos de Autenticación SSO y Sincronización Moodle 4.1</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="refreshQueueStats()" id="btnRefreshStats">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Actualizar Estado
|
||||
</button>
|
||||
<a href="/dashboard" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feedback Alerts -->
|
||||
<div id="settingsAlertContainer"></div>
|
||||
|
||||
<!-- Navegación por Pestañas -->
|
||||
<div class="card border-0 shadow-sm mb-4 config-card">
|
||||
<div class="card-header bg-transparent border-bottom px-3 pt-2 pb-0">
|
||||
<ul class="nav settings-nav-tabs" id="settingsTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="tab-smtp-btn" data-bs-toggle="tab" data-bs-target="#tab-smtp" type="button" role="tab">
|
||||
<i class="bi bi-envelope-at me-2 text-warning"></i>Servidor de Correo (SMTP)
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-sso-btn" data-bs-toggle="tab" data-bs-target="#tab-sso" type="button" role="tab">
|
||||
<i class="bi bi-shield-lock me-2 text-info"></i>Métodos de Login & Google OAuth
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-moodle-btn" data-bs-toggle="tab" data-bs-target="#tab-moodle" type="button" role="tab">
|
||||
<i class="bi bi-mortarboard me-2 text-danger"></i>Moodle 4.1 & Cola de Sync
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<div class="tab-content" id="settingsTabsContent">
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- PESTAÑA 1: SERVIDOR DE CORREO (SMTP) -->
|
||||
<!-- ========================================== -->
|
||||
<div class="tab-pane fade show active" id="tab-smtp" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<form id="smtpForm" onsubmit="saveSmtpConfig(event)">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label small fw-semibold">Servidor Host SMTP</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-hdd-network"></i></span>
|
||||
<input type="text" class="form-control" id="smtp_host" name="host" placeholder="smtp.gmail.com" value="{{ settings.smtp.host if settings and settings.smtp else 'smtp.gmail.com' }}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-semibold">Puerto</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-hash"></i></span>
|
||||
<input type="number" class="form-control" id="smtp_port" name="port" placeholder="587" value="{{ settings.smtp.port if settings and settings.smtp else '587' }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-semibold">Usuario / Cuenta de Envío</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-person"></i></span>
|
||||
<input type="text" class="form-control" id="smtp_user" name="user" placeholder="notificaciones@unicaba.edu.ar" value="{{ settings.smtp.user if settings and settings.smtp else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-semibold">Contraseña / App Password (Cifrada)</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||
<input type="password" class="form-control" id="smtp_password" name="password" placeholder="••••••••••••" value="{{ settings.smtp.password_masked if settings and settings.smtp else '' }}">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="togglePassVisibility('smtp_password')">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted" style="font-size: 11px;">Almacenada con cifrado simétrico Fernet. Ingrese nuevo valor solo si desea cambiarla.</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-semibold">Protocolo de Seguridad</label>
|
||||
<select class="form-select form-select-sm" id="smtp_security" name="security">
|
||||
<option value="tls" {% if not settings or not settings.smtp or settings.smtp.security == 'tls' %}selected{% endif %}>TLS / STARTTLS (Puerto 587)</option>
|
||||
<option value="ssl" {% if settings and settings.smtp and settings.smtp.security == 'ssl' %}selected{% endif %}>SSL Directo (Puerto 465)</option>
|
||||
<option value="none" {% if settings and settings.smtp and settings.smtp.security == 'none' %}selected{% endif %}>Sin Cifrado (Puerto 25)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-semibold">Email Remitente (From)</label>
|
||||
<input type="email" class="form-control form-control-sm" id="smtp_from_email" name="from_email" placeholder="notificaciones@unicaba.edu.ar" value="{{ settings.smtp.from_email if settings and settings.smtp else '' }}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-semibold">Nombre Remitente</label>
|
||||
<input type="text" class="form-control form-control-sm" id="smtp_from_name" name="from_name" placeholder="Admin Edu-Space UniCABA" value="{{ settings.smtp.from_name if settings and settings.smtp else 'Admin Edu-Space' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-3 border-top d-flex gap-2 flex-wrap">
|
||||
<button type="submit" class="btn btn-sm btn-primary px-3" id="btnSaveSmtp">
|
||||
<i class="bi bi-check2-circle me-1"></i>Guardar Parámetros SMTP
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-success px-3" data-bs-toggle="modal" data-bs-target="#testSmtpModal">
|
||||
<i class="bi bi-send-check me-1"></i>Probar Conexión SMTP
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card bg-body-tertiary border-0 p-3 rounded-3 h-100">
|
||||
<h6 class="fw-bold mb-2 d-flex align-items-center">
|
||||
<i class="bi bi-info-circle text-primary me-2"></i>Instrucciones de Correo
|
||||
</h6>
|
||||
<p class="small text-muted mb-2">Este módulo administra el transporte dinámico para el envío de correos transaccionales:</p>
|
||||
<ul class="small text-muted ps-3 mb-3">
|
||||
<li>Notificación de asignación de comisiones a profesores.</li>
|
||||
<li>Convocatorias a mesas de exámenes finales.</li>
|
||||
<li>Alertas de auditoría e incidentes de seguridad.</li>
|
||||
</ul>
|
||||
<div class="alert alert-info py-2 px-3 small mb-0">
|
||||
<i class="bi bi-shield-lock-fill me-1"></i>
|
||||
<strong>Google Workspace:</strong> Use contraseñas de aplicación (App Passwords) generadas desde la consola de Google si tiene 2FA activo.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- PESTAÑA 2: MÉTODOS DE LOGIN & GOOGLE OAUTH -->
|
||||
<!-- ========================================== -->
|
||||
<div class="tab-pane fade" id="tab-sso" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<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>
|
||||
<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 %}>
|
||||
</div>
|
||||
</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 %}>
|
||||
</div>
|
||||
</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 %}>
|
||||
</div>
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card border p-3 rounded-3">
|
||||
<h6 class="fw-bold mb-3 d-flex align-items-center">
|
||||
<i class="bi bi-google text-danger me-2"></i>Parámetros Google OAuth 2.0
|
||||
</h6>
|
||||
<form id="googleOauthForm" onsubmit="saveGoogleOAuth(event)">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-semibold">Client ID</label>
|
||||
<input type="text" class="form-control form-control-sm" id="google_client_id" placeholder="apps.googleusercontent.com" value="{{ settings.google_oauth.client_id if settings and settings.google_oauth else '' }}">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-semibold">Client Secret (Cifrado)</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="password" class="form-control" id="google_client_secret" placeholder="••••••••••••" value="{{ settings.google_oauth.client_secret_masked if settings and settings.google_oauth else '' }}">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="togglePassVisibility('google_client_secret')">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-semibold">Dominios Permitidos (separados por coma)</label>
|
||||
<input type="text" class="form-control form-control-sm" id="google_allowed_domains" placeholder="unicaba.edu.ar, lasalle.edu.ar" value="{{ settings.google_oauth.allowed_domains if settings and settings.google_oauth else 'unicaba.edu.ar' }}">
|
||||
<small class="text-muted" style="font-size: 11px;">Restricción arquitectónica estricta similar al módulo de AlumnosLS.</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Ruta de Redirección Autorizada (Callback URL)</label>
|
||||
<input type="text" class="form-control form-control-sm" id="google_callback_url" value="{{ settings.google_oauth.callback_url if settings and settings.google_oauth else '/auth/google/callback' }}">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-sm btn-primary" id="btnSaveGoogle">
|
||||
<i class="bi bi-check-circle me-1"></i>Guardar Credenciales Google
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- PESTAÑA 3: MOODLE 4.1 & COLA ASÍNCRONA -->
|
||||
<!-- ========================================== -->
|
||||
<div class="tab-pane fade" id="tab-moodle" role="tabpanel">
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="card border p-3 rounded-3 h-100">
|
||||
<h6 class="fw-bold mb-3 d-flex align-items-center">
|
||||
<i class="bi bi-cpu text-primary me-2"></i>Conexión Web Services Moodle 4.1
|
||||
</h6>
|
||||
<form id="moodleConfigForm" onsubmit="saveMoodleConfig(event)">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-semibold">URL Base del Servidor Moodle</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-link-45deg"></i></span>
|
||||
<input type="url" class="form-control" id="moodle_server_url" placeholder="http://10.0.0.207/moodle" value="{{ settings.moodle.server_url if settings and settings.moodle else 'http://10.0.0.207/moodle' }}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-semibold">Token de Web Services (Cifrado)</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||
<input type="password" class="form-control" id="moodle_ws_token" placeholder="••••••••••••" value="{{ settings.moodle.ws_token_masked if settings and settings.moodle else '' }}" required>
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="togglePassVisibility('moodle_ws_token')">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label small fw-semibold">Timeout (segundos)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="moodle_timeout" value="{{ settings.moodle.timeout if settings and settings.moodle else '10' }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label small fw-semibold">Intervalo Cron (min)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="moodle_sync_interval" value="{{ settings.moodle.sync_interval_minutes if settings and settings.moodle else '15' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="moodle_auto_sync" {% if not settings or not settings.moodle or settings.moodle.auto_sync_enabled %}checked{% endif %}>
|
||||
<label class="form-check-label small fw-semibold" for="moodle_auto_sync">Habilitar Sincronización Automática en Background</label>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-sm btn-primary" id="btnSaveMoodle">
|
||||
<i class="bi bi-save me-1"></i>Guardar Parámetros
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="testMoodleConn()" id="btnTestMoodle">
|
||||
<i class="bi bi-broadcast me-1"></i>Probar Conexión
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card border p-3 rounded-3 h-100 bg-body-tertiary">
|
||||
<h6 class="fw-bold mb-3 d-flex align-items-center">
|
||||
<i class="bi bi-bar-chart-steps text-info me-2"></i>Monitor de Cola & Tolerancia a Fallos
|
||||
</h6>
|
||||
<p class="small text-muted mb-3">
|
||||
Las creaciones de usuarios, matriculaciones y asignaciones no bloquean a Edu-Space. Se encolan y reintentan con <strong>Exponential Backoff</strong>. Si superan los intentos máximos, van a la <strong>Dead Letter Queue (DLQ)</strong>.
|
||||
</p>
|
||||
|
||||
<div class="row g-2 mb-3 text-center">
|
||||
<div class="col-4">
|
||||
<div class="p-2 border rounded bg-body">
|
||||
<div class="fs-4 fw-bold text-primary" id="statPending">0</div>
|
||||
<small class="text-muted" style="font-size:11px;">Pendientes</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="p-2 border rounded bg-body">
|
||||
<div class="fs-4 fw-bold text-warning" id="statRetrying">0</div>
|
||||
<small class="text-muted" style="font-size:11px;">Reintentando</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="p-2 border rounded bg-body">
|
||||
<div class="fs-4 fw-bold text-danger" id="statDlq">0</div>
|
||||
<small class="text-muted" style="font-size:11px;">Fallidas (DLQ)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button class="btn btn-sm btn-primary flex-grow-1" onclick="processQueueNow()" id="btnProcessNow">
|
||||
<i class="bi bi-lightning-charge me-1"></i>Forzar Sincronización Ahora
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="retryAllDlq()" id="btnRetryDlq">
|
||||
<i class="bi bi-arrow-repeat me-1"></i>Reintentar Fallidas DLQ
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de Tareas Recientes / DLQ -->
|
||||
<div class="card border-0 shadow-sm rounded-3">
|
||||
<div class="card-header bg-transparent border-bottom d-flex justify-content-between align-items-center px-3 py-2">
|
||||
<span class="fw-semibold small"><i class="bi bi-list-task me-1"></i>Tareas Recientes de Sincronización</span>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select form-select-sm" id="filterStatus" onchange="loadQueueTasks(1)" style="width: 140px;">
|
||||
<option value="">Todas</option>
|
||||
<option value="FAILED">Fallidas (DLQ)</option>
|
||||
<option value="PENDING">Pendientes</option>
|
||||
<option value="RETRYING">Reintentando</option>
|
||||
<option value="COMPLETED">Completadas</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm align-middle mb-0" style="font-size: 12.5px;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Acción</th>
|
||||
<th>Entidad</th>
|
||||
<th>Estado</th>
|
||||
<th>Intentos</th>
|
||||
<th>Detalle / Error</th>
|
||||
<th>Fecha</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="queueTasksTableBody">
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-3 text-muted">Cargando tareas de sincronización...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Prueba Conexión SMTP -->
|
||||
<div class="modal fade" id="testSmtpModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title fw-bold"><i class="bi bi-send-check text-primary me-2"></i>Prueba de Conexión SMTP</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="small text-muted mb-3">Se verificará el handshake con el servidor SMTP y opcionalmente se enviará un correo real de prueba con diseño institucional.</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Correo de Destino para Prueba</label>
|
||||
<input type="email" class="form-control form-control-sm" id="test_smtp_recipient" placeholder="su-email@unicaba.edu.ar">
|
||||
</div>
|
||||
<div id="testSmtpFeedback"></div>
|
||||
</div>
|
||||
<div class="modal-footer py-2">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="runSmtpTest()" id="btnRunSmtpTest">
|
||||
<i class="bi bi-play-fill me-1"></i>Ejecutar Prueba
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showAlert(message, type = 'success') {
|
||||
const container = document.getElementById('settingsAlertContainer');
|
||||
container.innerHTML = `
|
||||
<div class="alert alert-${type} alert-dismissible fade show py-2 px-3 small" role="alert">
|
||||
<i class="bi bi-${type === 'success' ? 'check-circle' : 'exclamation-triangle'} me-2"></i>
|
||||
${message}
|
||||
<button type="button" class="btn-close py-2" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function togglePassVisibility(inputId) {
|
||||
const input = document.getElementById(inputId);
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Guardar Configuración SMTP
|
||||
// -------------------------------------------------------------
|
||||
async function saveSmtpConfig(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnSaveSmtp');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Guardando...';
|
||||
|
||||
const payload = {
|
||||
host: document.getElementById('smtp_host').value.trim(),
|
||||
port: document.getElementById('smtp_port').value.trim(),
|
||||
user: document.getElementById('smtp_user').value.trim(),
|
||||
password: document.getElementById('smtp_password').value.trim(),
|
||||
security: document.getElementById('smtp_security').value,
|
||||
from_email: document.getElementById('smtp_from_email').value.trim(),
|
||||
from_name: document.getElementById('smtp_from_name').value.trim()
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/v1/admin/settings/smtp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert(data.message || 'Configuración SMTP guardada exitosamente.', 'success');
|
||||
} else {
|
||||
showAlert(data.message || 'Error al guardar configuración SMTP.', 'danger');
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Error de comunicación con el servidor: ' + err.message, 'danger');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>Guardar Parámetros SMTP';
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Probar Conexión SMTP en Vivo
|
||||
// -------------------------------------------------------------
|
||||
async function runSmtpTest() {
|
||||
const btn = document.getElementById('btnRunSmtpTest');
|
||||
const fb = document.getElementById('testSmtpFeedback');
|
||||
const recipient = document.getElementById('test_smtp_recipient').value.trim();
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Probando...';
|
||||
fb.innerHTML = '<div class="text-muted small py-2"><span class="spinner-border spinner-border-sm me-1"></span>Conectando al servidor SMTP...</div>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/v1/admin/settings/smtp/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ test_recipient: recipient })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
fb.innerHTML = `<div class="alert alert-success py-2 px-3 small mt-2 mb-0"><i class="bi bi-check-circle me-1"></i>${data.message}</div>`;
|
||||
} else {
|
||||
fb.innerHTML = `<div class="alert alert-danger py-2 px-3 small mt-2 mb-0"><i class="bi bi-x-circle me-1"></i>${data.message}</div>`;
|
||||
}
|
||||
} catch (err) {
|
||||
fb.innerHTML = `<div class="alert alert-danger py-2 px-3 small mt-2 mb-0">Error: ${err.message}</div>`;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-play-fill me-1"></i>Ejecutar Prueba';
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Guardar Métodos de Autenticación
|
||||
// -------------------------------------------------------------
|
||||
async function saveAuthProviders(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnSaveProviders');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Guardando...';
|
||||
|
||||
const payload = {
|
||||
local_enabled: document.getElementById('auth_local_enabled').checked,
|
||||
google_enabled: document.getElementById('auth_google_enabled').checked,
|
||||
moodle_enabled: document.getElementById('auth_moodle_enabled').checked
|
||||
};
|
||||
|
||||
try {
|
||||
const 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');
|
||||
} else {
|
||||
showAlert(data.message || 'Error al actualizar métodos de acceso.', 'danger');
|
||||
}
|
||||
} catch (err) {
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Guardar Google OAuth 2.0
|
||||
// -------------------------------------------------------------
|
||||
async function saveGoogleOAuth(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnSaveGoogle');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Guardando...';
|
||||
|
||||
const payload = {
|
||||
client_id: document.getElementById('google_client_id').value.trim(),
|
||||
client_secret: document.getElementById('google_client_secret').value.trim(),
|
||||
allowed_domains: document.getElementById('google_allowed_domains').value.trim(),
|
||||
callback_url: document.getElementById('google_callback_url').value.trim()
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/v1/admin/settings/google-oauth', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert(data.message || 'Credenciales de Google OAuth guardadas.', 'success');
|
||||
} else {
|
||||
showAlert(data.message || 'Error al guardar Google OAuth.', 'danger');
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Error: ' + err.message, 'danger');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-check-circle me-1"></i>Guardar Credenciales Google';
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Guardar Configuración Moodle
|
||||
// -------------------------------------------------------------
|
||||
async function saveMoodleConfig(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnSaveMoodle');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Guardando...';
|
||||
|
||||
const payload = {
|
||||
server_url: document.getElementById('moodle_server_url').value.trim(),
|
||||
ws_token: document.getElementById('moodle_ws_token').value.trim(),
|
||||
timeout: document.getElementById('moodle_timeout').value.trim(),
|
||||
auto_sync_enabled: document.getElementById('moodle_auto_sync').checked,
|
||||
sync_interval_minutes: document.getElementById('moodle_sync_interval').value.trim()
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/v1/admin/settings/moodle', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert(data.message || 'Configuración Moodle guardada.', 'success');
|
||||
} else {
|
||||
showAlert(data.message || 'Error al guardar Moodle.', 'danger');
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Error: ' + err.message, 'danger');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-save me-1"></i>Guardar Parámetros';
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Probar Conexión Moodle
|
||||
// -------------------------------------------------------------
|
||||
async function testMoodleConn() {
|
||||
const btn = document.getElementById('btnTestMoodle');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Probando...';
|
||||
|
||||
const payload = {
|
||||
server_url: document.getElementById('moodle_server_url').value.trim(),
|
||||
ws_token: document.getElementById('moodle_ws_token').value.trim()
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/v1/admin/settings/moodle/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert(`Conexión exitosa con Moodle: Sitio '${data.sitename}' (Versión Moodle ${data.version || '4.1'})`, 'success');
|
||||
} else {
|
||||
showAlert(`Fallo de conexión Moodle: ${data.message || 'Verifique URL y Token'}`, 'danger');
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Error de conexión con Moodle: ' + err.message, 'danger');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-broadcast me-1"></i>Probar Conexión';
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Monitor de Cola & DLQ
|
||||
// -------------------------------------------------------------
|
||||
async function refreshQueueStats() {
|
||||
try {
|
||||
const res = await fetch('/api/v1/admin/moodle/queue/stats');
|
||||
const data = await res.json();
|
||||
if (res.ok && data.data) {
|
||||
const s = data.data.summary || {};
|
||||
document.getElementById('statPending').innerText = (s.PENDING || 0) + (s.PROCESSING || 0);
|
||||
document.getElementById('statRetrying').innerText = s.RETRYING || 0;
|
||||
document.getElementById('statDlq').innerText = s.FAILED || 0;
|
||||
}
|
||||
await loadQueueTasks(1);
|
||||
} catch (err) {
|
||||
console.warn('Queue stats refresh notice:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadQueueTasks(page = 1) {
|
||||
const tbody = document.getElementById('queueTasksTableBody');
|
||||
const status = document.getElementById('filterStatus').value;
|
||||
try {
|
||||
const res = await fetch(`/api/v1/admin/moodle/queue/tasks?page=${page}&status=${status}`);
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.tasks || data.tasks.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-3 text-muted">No hay tareas de sincronización registradas.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.tasks.map(t => {
|
||||
let statusBadge = '<span class="badge bg-secondary">PENDIENTE</span>';
|
||||
if (t.status === 'COMPLETED') statusBadge = '<span class="badge bg-success">COMPLETADA</span>';
|
||||
else if (t.status === 'PROCESSING') statusBadge = '<span class="badge bg-info text-dark">PROCESANDO</span>';
|
||||
else if (t.status === 'RETRYING') statusBadge = '<span class="badge bg-warning text-dark">REINTENTANDO</span>';
|
||||
else if (t.status === 'FAILED') statusBadge = '<span class="badge bg-danger">FALLIDA (DLQ)</span>';
|
||||
|
||||
const errText = t.error_message ? `<span class="text-danger" title="${escapeHtml(t.error_message)}">${escapeHtml(t.error_message.slice(0, 45))}...</span>` : '<span class="text-muted">-</span>';
|
||||
const actionBtn = (t.status === 'FAILED' || t.status === 'RETRYING')
|
||||
? `<button class="btn btn-xs btn-outline-primary" onclick="retrySingleTask(${t.id})" title="Reintentar"><i class="bi bi-arrow-repeat"></i></button>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td><strong>#${t.id}</strong></td>
|
||||
<td><code>${escapeHtml(t.action)}</code></td>
|
||||
<td>${escapeHtml(t.entity_type)}:${escapeHtml(t.entity_id || '')}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>${t.attempts}/${t.max_attempts}</td>
|
||||
<td>${errText}</td>
|
||||
<td><small class="text-muted">${t.created_at ? t.created_at.slice(0, 16) : '-'}</small></td>
|
||||
<td class="text-end">${actionBtn}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
} catch (err) {
|
||||
tbody.innerHTML = `<tr><td colspan="8" class="text-center py-3 text-danger">Error al cargar tareas: ${err.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function processQueueNow() {
|
||||
const btn = document.getElementById('btnProcessNow');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Procesando...';
|
||||
try {
|
||||
const res = await fetch('/api/v1/admin/moodle/queue/process-now', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
showAlert(data.message || 'Sincronización procesada.', res.ok ? 'success' : 'danger');
|
||||
await refreshQueueStats();
|
||||
} catch (err) {
|
||||
showAlert('Error al disparar sincronización: ' + err.message, 'danger');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-lightning-charge me-1"></i>Forzar Sincronización Ahora';
|
||||
}
|
||||
}
|
||||
|
||||
async function retryAllDlq() {
|
||||
if (!confirm('¿Confirma que desea reintentar todas las tareas fallidas de la Dead Letter Queue?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/v1/admin/moodle/queue/retry-all', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
showAlert(data.message || 'Tareas reiniciadas.', 'info');
|
||||
await refreshQueueStats();
|
||||
} catch (err) {
|
||||
showAlert('Error: ' + err.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async function retrySingleTask(id) {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/admin/moodle/queue/tasks/${id}/retry`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
showAlert(data.message || `Tarea #${id} reiniciada.`, 'info');
|
||||
await refreshQueueStats();
|
||||
} catch (err) {
|
||||
showAlert('Error: ' + err.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/[&<>"']/g, function(m) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m];
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar estadísticas y tareas al iniciar
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
refreshQueueStats();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user