ESQUEMA (migraciones 005 + 006)
* users: agrega role ENUM('socio','abogado','usuario'), apellido, activo, orden
* Seed: cabrego@aal-law.com como socio + 15 placeholders por apellido para
poblar el dropdown "Abogado Solicitante" hasta que los reales se logueen.
* tramites_involucrados: tabla N:M (tramite_id, user_id) con CASCADE delete.
AUTH (src/Auth.php)
* userRow() cachea el row del usuario actual por request.
* role(), isSocio(), isAbogado(), canCreate(), canDelete(), requireSocio().
* completeLogin() retorna bool: false si user.activo=0 → callback muestra
pantalla "cuenta deshabilitada" en lugar de loguear.
BOOTSTRAP (src/Bootstrap.php + src/Lookups.php)
* Bootstrap::inject(view) reemplaza readfile en los wrappers; inyecta
<script>window.DONNA={me,name,role,isSocio,isAbogado,canCreate,canDelete,
abogados,users}</script> antes de </head> para que el frontend tenga
contexto sin requests extra.
TRAMITES — visibilidad + involucrados
* api.php: list filtra por (socio | creador | abogado solicitante mapped via
users.apellido | involucrado) excepto socios que ven todo. Todas las
mutaciones validan canView(). create/delete validan rol. Persiste
involucrados vía syncInvolucrados().
* tramites.html: nueva columna "Involucrados" después de "Abogado", chip
picker multi-select en el modal, gear icon (oculto a non-socios), oculta
botones "+ Nuevo" y ✕ para usuarios sin permisos. Auto-incluye al
creador como involucrado por defecto.
* Calendario y demás apps consumen el API filtrado → heredan la
visibilidad sin código nuevo.
PANEL DE CONTROL (/admin/)
* index.php: requireSocio() + Bootstrap injection.
* api.php: list_users, create_user, update_user. Salvaguardas: el socio
no puede demoverse ni desactivarse a sí mismo.
* admin.html: tabla unificada de usuarios con filtros, modal CRUD,
resaltado de fila propia, placeholders identificados visualmente.
GEAR ICON
* Agregado en escrituras.html, tramites.html, calendario.html con
display:none por defecto; JS lo revela si window.DONNA.isSocio.
* Link a /admin/.
72 lines
2.8 KiB
PHP
72 lines
2.8 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
require_once __DIR__ . '/../../src/Auth.php';
|
|
require_once __DIR__ . '/../../src/Oauth.php';
|
|
|
|
function deny(string $msg, int $code = 401): void {
|
|
http_response_code($code);
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
$safe = htmlspecialchars($msg, ENT_QUOTES, 'UTF-8');
|
|
echo <<<HTML
|
|
<!DOCTYPE html>
|
|
<html lang="es"><head><meta charset="UTF-8"><title>Acceso denegado — Donna</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
|
|
<style>
|
|
body{font-family:'DM Sans',sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#F7F6F3;}
|
|
.box{background:#fff;border:1px solid #eee;border-radius:14px;padding:44px 52px;text-align:center;max-width:440px;box-shadow:0 8px 32px rgba(0,0,0,0.06);}
|
|
.logo{font-family:'DM Serif Display',serif;font-size:24px;color:#1B3A6B;margin-bottom:6px;}
|
|
.tag{font-size:11px;color:#C9A84C;letter-spacing:.18em;text-transform:uppercase;margin-bottom:24px;font-weight:600;}
|
|
h2{color:#1B3A6B;margin:0 0 12px;font-family:'DM Serif Display',serif;font-weight:400;}
|
|
p{color:#666;font-size:14px;margin:0 0 24px;line-height:1.55;}
|
|
a{display:inline-block;padding:10px 24px;background:#1B3A6B;color:#fff;border-radius:8px;text-decoration:none;font-size:13px;font-weight:500;}
|
|
a:hover{background:#112446;}
|
|
</style></head><body>
|
|
<div class="box">
|
|
<div class="logo">Donna</div>
|
|
<div class="tag">AALN · Portal interno</div>
|
|
<h2>Acceso denegado</h2>
|
|
<p>{$safe}</p>
|
|
<a href="/">Volver al inicio</a>
|
|
</div></body></html>
|
|
HTML;
|
|
exit;
|
|
}
|
|
|
|
$state = $_GET['state'] ?? '';
|
|
if (!is_string($state) || !Auth::checkState($state)) {
|
|
deny('Token de seguridad inválido o expirado. Intenta iniciar sesión de nuevo.');
|
|
}
|
|
|
|
$code = $_GET['code'] ?? '';
|
|
if (!is_string($code) || $code === '') {
|
|
$err = $_GET['error_description'] ?? $_GET['error'] ?? 'Autorización cancelada.';
|
|
deny((string)$err);
|
|
}
|
|
|
|
$tokenResp = Oauth::exchangeCodeForToken($code);
|
|
if (empty($tokenResp['access_token'])) {
|
|
deny('No se pudo obtener el token de acceso. Reintenta más tarde.');
|
|
}
|
|
|
|
$profile = Oauth::fetchProfile($tokenResp['access_token']);
|
|
$email = $profile['email'] ?? $profile['username'] ?? '';
|
|
$name = $profile['full_name'] ?? $profile['name'] ?? $email;
|
|
|
|
if ($email === '') {
|
|
deny('No se pudo leer el perfil del usuario desde el servidor de correo.');
|
|
}
|
|
|
|
if (!Auth::emailDomainAllowed($email)) {
|
|
deny('Esta cuenta no está autorizada para Donna. Solo dominios corporativos de AALN.', 403);
|
|
}
|
|
|
|
if (!Auth::completeLogin($email, $name)) {
|
|
deny('Tu cuenta de Donna está desactivada. Contacta al administrador para reactivarla.', 403);
|
|
}
|
|
|
|
$returnTo = Auth::popReturnTo();
|
|
if (!str_starts_with($returnTo, '/')) $returnTo = '/';
|
|
|
|
header('Location: ' . $returnTo);
|
|
exit;
|