Files
hyprcrd/relay/inc/auth.php
T
thandClaude Opus 4.6 d50f9fe789 relay UI overhaul, favicon, settings, admin last-seen, fixes
Relay:
  - Inbox/shared/sent split into own view-inbox (nav button + poll).
    Compose is now single-column, centered.
  - Header logo links to compose. Inbox badge stays fresh on all
    views via a quiet loadInbox on every view switch.
  - Mobile menu: full-height sheet below header, larger buttons
    (15px font, 14px padding), full width.
  - Login form wrapped in <form> with name attributes + for/id on
    labels so password managers can autofill.
  - Admin user list shows "last seen <relative time>" per user.
    last_seen_at column on tenants, updated on every authenticated
    request (session + client API).
  - Cache-busting on style.css + app.js via filemtime.
  - View persistence in localStorage (survives reload).
  - Shared-bucket cards show title only (preview line removed).

Favicon:
  - favicon.svg added to project root + copied to public/, relay/,
    website/. Linked via <link rel="icon" type="image/svg+xml"> in
    all three <head> sections.

Settings:
  - New toggle: "Show shopping list items in task collector"
    (default off). collectTasks skips #einkauf/#shopping cards when
    disabled.

Fixes:
  - card-spinner gets display:inline-block so it rotates outside
    flex containers (shopping save spinner).
  - Tim tab test opens the plugin before checking active state.
  - Compose view centered with margin:auto on desktop.

Bumps to 1.9.29.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 13:16:23 +02:00

75 lines
2.9 KiB
PHP

<?php
// Auth helpers: rate limiting, sessions (UI), client API auth (header).
if (!defined('RELAY_BOOTSTRAPPED')) { http_response_code(404); exit; }
function isRateLimited(PDO $db, string $endpoint, int $max = 10): bool {
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$count = $db->prepare("SELECT COUNT(*) FROM rate_limits WHERE ip = :ip AND endpoint = :ep AND attempted_at > datetime('now', '-1 minutes')");
$count->execute(['ip' => $ip, 'ep' => $endpoint]);
if ((int)$count->fetchColumn() >= $max) return true;
$db->prepare("INSERT INTO rate_limits (ip, endpoint) VALUES (:ip, :ep)")->execute(['ip' => $ip, 'ep' => $endpoint]);
return false;
}
function makeSession(PDO $db, int $tenantId, int $hours = 24): string {
$token = bin2hex(random_bytes(32));
$db->prepare("INSERT INTO sessions (id, tenant_id, expires_at) VALUES (:t, :u, datetime('now', '+{$hours} hours'))")
->execute(['t' => $token, 'u' => $tenantId]);
return $token;
}
function getSessionTenant(PDO $db): ?array {
$token = $_COOKIE['relay_session'] ?? '';
if (!$token) return null;
$stmt = $db->prepare("SELECT t.* FROM sessions s JOIN tenants t ON t.id = s.tenant_id WHERE s.id = :tok AND s.expires_at > datetime('now')");
$stmt->execute(['tok' => $token]);
$tenant = $stmt->fetch() ?: null;
if ($tenant) touchLastSeen($db, (int)$tenant['id']);
return $tenant;
}
function destroySession(PDO $db): void {
$token = $_COOKIE['relay_session'] ?? '';
if ($token) {
$db->prepare('DELETE FROM sessions WHERE id = :t')->execute(['t' => $token]);
}
setcookie('relay_session', '', [
'expires' => time() - 3600,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
'secure' => !empty($_SERVER['HTTPS']) || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https',
]);
}
function setSessionCookie(string $token): void {
setcookie('relay_session', $token, [
'expires' => time() + 86400,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
'secure' => !empty($_SERVER['HTTPS']) || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https',
]);
}
/**
* Authenticate a tenant via the client API headers (X-Relay-User + X-Relay-Key).
* Returns the tenant row on success, null on failure.
*/
function authClient(PDO $db): ?array {
$username = $_SERVER['HTTP_X_RELAY_USER'] ?? '';
$key = $_SERVER['HTTP_X_RELAY_KEY'] ?? '';
if (!$username || !$key) return null;
$tenant = getTenantByUsername($db, $username);
if (!$tenant) return null;
if (!password_verify($key, $tenant['secret_hash'])) return null;
touchLastSeen($db, (int)$tenant['id']);
return $tenant;
}
function touchLastSeen(PDO $db, int $tenantId): void {
$db->prepare("UPDATE tenants SET last_seen_at = datetime('now') WHERE id = :id")
->execute(['id' => $tenantId]);
}