Initial commit — Donna portal v1
Setup completo del portal interno donna.aal-law.com: * Landing público con OAuth login + grid de apps autenticado * Flujo OAuth2 contra Mailcow (mail.aal-law.com) * Sesiones SSO compartidas en el dominio (cookie donna_session, 8h) * Whitelist de dominios aal-law.com / aalnlaw.com * App "Control de Escrituras" migrada desde ZH-Consultores/control-escrituras (mismo schema, mismo frontend, ahora con shared Auth/Db/Config libs) * Migraciones SQL para users, escrituras_tramites, escrituras_historial * Caddy on :80 con rewrite extless → .php * Backup diario MariaDB (systemd timer + script) con retención 14d * Documentación de arquitectura en README + infra files en docs/infra
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class Apps {
|
||||
public static function catalog(): array {
|
||||
return [
|
||||
[
|
||||
'slug' => 'escrituras',
|
||||
'title' => 'Control de Escrituras',
|
||||
'description' => 'Seguimiento de trámites notariales: estado, abogado, notaría, historial y auditoría.',
|
||||
'icon' => 'document',
|
||||
'href' => '/escrituras/',
|
||||
'status' => 'live',
|
||||
],
|
||||
[
|
||||
'slug' => 'casos',
|
||||
'title' => 'Gestión de Casos',
|
||||
'description' => 'Expedientes, plazos procesales y notas de cada asunto.',
|
||||
'icon' => 'briefcase',
|
||||
'href' => '#',
|
||||
'status' => 'coming-soon',
|
||||
],
|
||||
[
|
||||
'slug' => 'agenda',
|
||||
'title' => 'Agenda',
|
||||
'description' => 'Calendario compartido de audiencias, reuniones y vencimientos.',
|
||||
'icon' => 'calendar',
|
||||
'href' => '#',
|
||||
'status' => 'coming-soon',
|
||||
],
|
||||
[
|
||||
'slug' => 'clientes',
|
||||
'title' => 'Directorio de Clientes',
|
||||
'description' => 'Datos de contacto, sociedades vinculadas y matriz de KYC.',
|
||||
'icon' => 'users',
|
||||
'href' => '#',
|
||||
'status' => 'coming-soon',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/Config.php';
|
||||
require_once __DIR__ . '/Db.php';
|
||||
|
||||
final class Auth {
|
||||
public static function startSession(): void {
|
||||
if (session_status() === PHP_SESSION_ACTIVE) return;
|
||||
|
||||
$secure = ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'
|
||||
|| (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
|
||||
session_name('donna_session');
|
||||
session_set_cookie_params([
|
||||
'lifetime' => Config::int('SESSION_LIFETIME', 28800),
|
||||
'path' => '/',
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
session_start();
|
||||
}
|
||||
|
||||
public static function isAuthenticated(): bool {
|
||||
self::startSession();
|
||||
return !empty($_SESSION['authenticated'])
|
||||
&& !empty($_SESSION['auth_time'])
|
||||
&& (time() - (int)$_SESSION['auth_time']) < Config::int('SESSION_LIFETIME', 28800);
|
||||
}
|
||||
|
||||
public static function user(): array {
|
||||
self::startSession();
|
||||
return [
|
||||
'email' => $_SESSION['user_email'] ?? '',
|
||||
'name' => $_SESSION['user_name'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
public static function buildLoginUrl(?string $returnTo = null): string {
|
||||
self::startSession();
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$_SESSION['oauth_state'] = $state;
|
||||
$_SESSION['oauth_return_to'] = $returnTo ?: ($_SERVER['REQUEST_URI'] ?? '/');
|
||||
|
||||
$params = http_build_query([
|
||||
'response_type' => 'code',
|
||||
'client_id' => Config::get('OAUTH_CLIENT_ID'),
|
||||
'redirect_uri' => Config::get('OAUTH_REDIRECT_URI'),
|
||||
'scope' => Config::get('OAUTH_SCOPE', 'profile'),
|
||||
'state' => $state,
|
||||
]);
|
||||
return Config::get('OAUTH_AUTH_URL') . '?' . $params;
|
||||
}
|
||||
|
||||
public static function requireAuth(): void {
|
||||
if (self::isAuthenticated()) return;
|
||||
$loginUrl = self::buildLoginUrl();
|
||||
$accepts = $_SERVER['HTTP_ACCEPT'] ?? '';
|
||||
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH'])
|
||||
|| str_contains($accepts, 'application/json');
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
http_response_code(401);
|
||||
echo json_encode(['ok' => false, 'auth_required' => true, 'login_url' => $loginUrl]);
|
||||
exit;
|
||||
}
|
||||
header('Location: ' . $loginUrl);
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function emailDomainAllowed(string $email): bool {
|
||||
$allowed = Config::list('ALLOWED_EMAIL_DOMAINS');
|
||||
if (!$allowed) return true;
|
||||
$parts = explode('@', strtolower(trim($email)));
|
||||
if (count($parts) !== 2) return false;
|
||||
return in_array($parts[1], array_map('strtolower', $allowed), true);
|
||||
}
|
||||
|
||||
public static function completeLogin(string $email, string $name): void {
|
||||
self::startSession();
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['authenticated'] = true;
|
||||
$_SESSION['user_email'] = $email;
|
||||
$_SESSION['user_name'] = $name ?: $email;
|
||||
$_SESSION['auth_time'] = time();
|
||||
|
||||
try {
|
||||
$pdo = Db::pdo();
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO users (email, name, first_seen_at, last_login_at)
|
||||
VALUES (:email, :name, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE name = VALUES(name), last_login_at = NOW()'
|
||||
);
|
||||
$stmt->execute([':email' => $email, ':name' => $name]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('users upsert failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function logout(): void {
|
||||
self::startSession();
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$p = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'] ?? '', $p['secure'], $p['httponly']);
|
||||
}
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
public static function popReturnTo(): string {
|
||||
self::startSession();
|
||||
$to = $_SESSION['oauth_return_to'] ?? '/';
|
||||
unset($_SESSION['oauth_return_to']);
|
||||
return $to;
|
||||
}
|
||||
|
||||
public static function checkState(string $state): bool {
|
||||
self::startSession();
|
||||
$expected = $_SESSION['oauth_state'] ?? '';
|
||||
unset($_SESSION['oauth_state']);
|
||||
return $state !== '' && hash_equals((string)$expected, $state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class Config {
|
||||
private static ?array $env = null;
|
||||
|
||||
public static function load(string $path = '/etc/donna/donna.env'): void {
|
||||
if (self::$env !== null) return;
|
||||
if (!is_readable($path)) {
|
||||
http_response_code(500);
|
||||
exit('Config no disponible.');
|
||||
}
|
||||
$vars = parse_ini_file($path, false, INI_SCANNER_RAW);
|
||||
if ($vars === false) {
|
||||
http_response_code(500);
|
||||
exit('Config inválida.');
|
||||
}
|
||||
self::$env = $vars;
|
||||
}
|
||||
|
||||
public static function get(string $key, ?string $default = null): string {
|
||||
self::load();
|
||||
return self::$env[$key] ?? $default ?? '';
|
||||
}
|
||||
|
||||
public static function int(string $key, int $default = 0): int {
|
||||
return (int) self::get($key, (string) $default);
|
||||
}
|
||||
|
||||
public static function list(string $key): array {
|
||||
$v = self::get($key);
|
||||
return $v === '' ? [] : array_map('trim', explode(',', $v));
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/Config.php';
|
||||
|
||||
final class Db {
|
||||
private static ?PDO $pdo = null;
|
||||
|
||||
public static function pdo(): PDO {
|
||||
if (self::$pdo !== null) return self::$pdo;
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',
|
||||
Config::get('DB_HOST', '127.0.0.1'),
|
||||
Config::get('DB_PORT', '3306'),
|
||||
Config::get('DB_NAME')
|
||||
);
|
||||
self::$pdo = new PDO($dsn, Config::get('DB_USER'), Config::get('DB_PASS'), [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
return self::$pdo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/Config.php';
|
||||
|
||||
final class Oauth {
|
||||
public static function exchangeCodeForToken(string $code): array {
|
||||
return self::httpPost(Config::get('OAUTH_TOKEN_URL'), [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code,
|
||||
'redirect_uri' => Config::get('OAUTH_REDIRECT_URI'),
|
||||
'client_id' => Config::get('OAUTH_CLIENT_ID'),
|
||||
'client_secret' => Config::get('OAUTH_CLIENT_SECRET'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function fetchProfile(string $accessToken): array {
|
||||
return self::httpGetBearer(Config::get('OAUTH_PROFILE_URL'), $accessToken);
|
||||
}
|
||||
|
||||
private static function httpPost(string $url, array $form): array {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($form),
|
||||
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
if ($err || $body === false) {
|
||||
error_log('oauth POST failed: ' . $err);
|
||||
return [];
|
||||
}
|
||||
return json_decode((string)$body, true) ?? [];
|
||||
}
|
||||
|
||||
private static function httpGetBearer(string $url, string $token): array {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $token,
|
||||
'Accept: application/json',
|
||||
],
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
if ($err || $body === false) {
|
||||
error_log('oauth GET failed: ' . $err);
|
||||
return [];
|
||||
}
|
||||
return json_decode((string)$body, true) ?? [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user