Files
Axia4/public_html/_incl/db.php
2026-03-07 19:37:23 +00:00

857 lines
29 KiB
PHP

<?php
/**
* Axia4 Database Layer
*
* Provides a PDO SQLite connection and a lightweight migration runner.
* All application data previously stored as JSON files under /DATA is now
* persisted in /DATA/axia4.sqlite.
*
* Usage: db() → returns the shared PDO instance (auto-migrates on first call).
*/
define('DB_PATH', '/DATA/axia4.sqlite');
define('MIGRATIONS_DIR', __DIR__ . '/migrations');
// ── Connection ────────────────────────────────────────────────────────────────
function db(): PDO
{
static $pdo = null;
if ($pdo !== null) {
return $pdo;
}
if (!is_dir('/DATA')) {
mkdir('/DATA', 0755, true);
}
$pdo = new PDO('sqlite:' . DB_PATH);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('PRAGMA journal_mode = WAL');
$pdo->exec('PRAGMA foreign_keys = ON');
$pdo->exec('PRAGMA synchronous = NORMAL');
db_migrate($pdo);
return $pdo;
}
// ── Migration runner ──────────────────────────────────────────────────────────
function db_migrate(PDO $pdo): void
{
$pdo->exec(
'CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime(\'now\'))
)'
);
$applied = $pdo->query('SELECT version FROM schema_migrations ORDER BY version')
->fetchAll(PDO::FETCH_COLUMN);
$files = glob(MIGRATIONS_DIR . '/*.{sql,php}', GLOB_BRACE) ?: [];
sort($files);
foreach ($files as $file) {
if (!preg_match('/^(\d+)/', basename($file), $m)) {
continue;
}
$version = (int) $m[1];
if (in_array($version, $applied, true)) {
continue;
}
if (str_ends_with($file, '.sql')) {
$pdo->exec((string) file_get_contents($file));
} elseif (str_ends_with($file, '.php')) {
// PHP migration receives the connection as $db
$db = $pdo;
require $file;
}
$pdo->prepare('INSERT INTO schema_migrations (version) VALUES (?)')->execute([$version]);
}
}
// ── Config helpers ────────────────────────────────────────────────────────────
function db_get_config(string $key, $default = null)
{
$stmt = db()->prepare('SELECT value FROM config WHERE key = ?');
$stmt->execute([$key]);
$row = $stmt->fetch();
if ($row === false) {
return $default;
}
$decoded = json_decode($row['value'], true);
return $decoded !== null ? $decoded : $row['value'];
}
function db_set_config(string $key, $value): void
{
db()->prepare('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)')
->execute([$key, is_string($value) ? $value : json_encode($value)]);
}
function db_get_all_config(): array
{
$rows = db()->query('SELECT key, value FROM config')->fetchAll();
$result = [];
foreach ($rows as $row) {
$decoded = json_decode($row['value'], true);
$result[$row['key']] = ($decoded !== null) ? $decoded : $row['value'];
}
return $result;
}
// ── User helpers ──────────────────────────────────────────────────────────────
/** Find a user by username or email (always lower-cased). Returns DB row or null. */
function db_get_user(string $username): ?array
{
if (str_contains($username, "@")) {
$stmt = db()->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([strtolower($username)]);
$row = $stmt->fetch();
return $row !== false ? $row : null;
}
$stmt = db()->prepare('SELECT * FROM users WHERE username = ?');
$stmt->execute([strtolower($username)]);
$row = $stmt->fetch();
return $row !== false ? $row : null;
}
/** Return all user rows ordered by username. */
function db_get_all_users(): array
{
return db()->query('SELECT * FROM users ORDER BY username')->fetchAll();
}
/**
* Build the auth_data session array from a DB user row.
* Preserves the same format existing code expects:
* auth_data.permissions, auth_data.active_organizations auth_data.organizations.
*/
function db_build_auth_data(array $row): array
{
$permissions = json_decode($row['permissions'] ?? '[]', true) ?: [];
$meta = json_decode($row['meta'] ?? '{}', true) ?: [];
$ea = [
'organization' => '',
'organizations' => [],
'role' => '',
'aulas' => [],
'organizations_data' => [],
];
// Fetch all organization assignments for this user
$stmt = db()->prepare(
'SELECT org_id, role, ea_aulas
FROM user_orgs
WHERE user_id = ?
ORDER BY org_id'
);
$stmt->execute([$row['id']]);
$org_rows = $stmt->fetchAll();
$orgs = [];
if (!empty($org_rows)) {
$first = $org_rows[0];
foreach ($org_rows as $r) {
$orgs[] = $r['org_id'];
}
$ea['organization'] = $first['org_id'];
$ea['role'] = $first['role'];
$ea['aulas'] = json_decode($first['ea_aulas'] ?? '[]', true) ?: [];
$ea['organizations'] = $orgs;
$ea['organizations_data'] = $org_rows;
}
$active_org = $ea['organization'] ?? '';
$aulatek = [
'organizacion' => $active_org,
'organizaciones' => $orgs,
'organization' => $active_org,
'organizations' => $orgs,
'centro' => $active_org,
'centros' => $orgs,
'role' => $ea['role'] ?? '',
'aulas' => $ea['aulas'] ?? [],
];
return array_merge($meta, [
'display_name' => $row['display_name'],
'email' => $row['email'],
'password_hash' => $row['password_hash'],
'permissions' => $permissions,
'orgs' => $orgs,
'organizations' => $orgs,
'active_organization' => $active_org,
'active_organizations' => $ea,
'aulatek' => $aulatek,
'entreaulas' => $aulatek,
'google_auth' => (bool) $row['google_auth'],
]);
}
/**
* Create or update a user.
* $data keys: username, display_name, email, password_hash, permissions[],
* google_auth, entreaulas{organizacion,organizaciones[],role,aulas[]}, + any extra meta.
* Returns the user ID.
*/
function db_upsert_user(array $data): int
{
$pdo = db();
$username = strtolower((string) ($data['username'] ?? ''));
$existing = $pdo->prepare('SELECT id FROM users WHERE username = ?');
$existing->execute([$username]);
$existing_row = $existing->fetch();
$permissions = json_encode($data['permissions'] ?? []);
$meta_skip = ['username', 'display_name', 'email', 'password_hash',
'permissions', 'entreaulas', 'google_auth',
'orgs', 'organizations', 'organization', 'organizacion',
'role', 'aulas'];
$meta = [];
foreach ($data as $k => $v) {
if (!in_array($k, $meta_skip, true)) {
$meta[$k] = $v;
}
}
if ($existing_row) {
$user_id = (int) $existing_row['id'];
$upd = $pdo->prepare(
"UPDATE users SET
display_name = ?,
email = ?,
permissions = ?,
google_auth = ?,
meta = ?,
updated_at = datetime('now')
WHERE id = ?"
);
$upd->execute([
$data['display_name'] ?? '',
$data['email'] ?? '',
$permissions,
(int) ($data['google_auth'] ?? 0),
json_encode($meta),
$user_id,
]);
if (!empty($data['password_hash'])) {
$pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ?')
->execute([$data['password_hash'], $user_id]);
}
} else {
$pdo->prepare(
'INSERT INTO users (username, display_name, email, password_hash, permissions, google_auth, meta)
VALUES (?, ?, ?, ?, ?, ?, ?)'
)->execute([
$username,
$data['display_name'] ?? '',
$data['email'] ?? '',
$data['password_hash'] ?? '',
$permissions,
(int) ($data['google_auth'] ?? 0),
json_encode($meta),
]);
$user_id = (int) $pdo->lastInsertId();
}
// Update organization assignments if tenant data is provided.
$has_org_payload = array_key_exists('entreaulas', $data)
|| array_key_exists('orgs', $data)
|| array_key_exists('organizations', $data)
|| array_key_exists('organization', $data)
|| array_key_exists('organizacion', $data);
if ($has_org_payload) {
$ea = $data['entreaulas'] ?? [];
$organizations = [];
$candidate_lists = [
$data['organizations'] ?? null,
$data['orgs'] ?? null,
$ea['organizaciones'] ?? null,
$ea['organizations'] ?? null,
$ea['centros'] ?? null,
];
foreach ($candidate_lists as $list) {
if (is_array($list) && !empty($list)) {
$organizations = $list;
break;
}
}
if (empty($organizations)) {
foreach ([
$data['organization'] ?? null,
$data['organizacion'] ?? null,
$ea['organizacion'] ?? null,
$ea['organization'] ?? null,
$ea['centro'] ?? null,
] as $single) {
if (!empty($single)) {
$organizations = [$single];
break;
}
}
}
$organizations = array_values(array_unique(array_filter(array_map(
static function ($value): string {
return preg_replace('/[^a-zA-Z0-9._-]/', '', (string) $value);
},
$organizations
))));
$role = (string) ($data['role'] ?? $ea['role'] ?? '');
$aulas_payload = $data['aulas'] ?? $ea['aulas'] ?? [];
if (!is_array($aulas_payload)) {
$aulas_payload = [];
}
$aulas = json_encode($aulas_payload, JSON_UNESCAPED_UNICODE);
$pdo->prepare('DELETE FROM user_orgs WHERE user_id = ?')->execute([$user_id]);
$ins_org = $pdo->prepare('INSERT OR IGNORE INTO organizaciones (org_id, org_name) VALUES (?, ?)');
$ins_uo = $pdo->prepare(
'INSERT OR REPLACE INTO user_orgs (user_id, org_id, role, ea_aulas) VALUES (?, ?, ?, ?)'
);
foreach ($organizations as $org_id) {
if ($org_id === '') {
continue;
}
$ins_org->execute([$org_id, $org_id]);
$ins_uo->execute([$user_id, $org_id, $role, $aulas]);
}
}
return $user_id;
}
/** Delete a user and their organization assignments. */
function db_delete_user(string $username): void
{
db()->prepare('DELETE FROM users WHERE username = ?')->execute([strtolower($username)]);
}
// ── Organization helpers ─────────────────────────────────────────────────────
function db_get_organizations(): array
{
return db()->query('SELECT org_id, org_name FROM organizaciones ORDER BY org_id')->fetchAll();
}
function db_get_organization_ids(): array
{
return db()->query('SELECT org_id FROM organizaciones ORDER BY org_id')->fetchAll(PDO::FETCH_COLUMN);
}
function db_get_organizaciones(): array
{
return db_get_organizations();
}
function get_organizations(): array
{
return db_get_organizations();
}
function db_get_centros(): array
{
$rows = db_get_organizations();
return array_map(static function (array $row): array {
return [
'centro_id' => $row['org_id'],
'name' => $row['org_name'],
];
}, $rows);
}
function db_get_centro_ids(): array
{
return db_get_organization_ids();
}
// ── Aulario helpers ───────────────────────────────────────────────────────────
/** Get a single aulario config. Returns merged array (name, icon, + extra fields) or null. */
function db_get_aulario(string $centro_id, string $aulario_id): ?array
{
$stmt = db()->prepare(
'SELECT name, icon, extra FROM aularios WHERE org_id = ? AND aulario_id = ?'
);
$stmt->execute([$centro_id, $aulario_id]);
$row = $stmt->fetch();
if ($row === false) {
return null;
}
$extra = json_decode($row['extra'] ?? '{}', true) ?: [];
return array_merge($extra, ['name' => $row['name'], 'icon' => $row['icon']]);
}
/** Get all aularios for a centro as aulario_id → config array. */
function db_get_aularios(string $centro_id): array
{
$stmt = db()->prepare(
'SELECT aulario_id, name, icon, extra FROM aularios WHERE org_id = ? ORDER BY aulario_id'
);
$stmt->execute([$centro_id]);
$result = [];
foreach ($stmt->fetchAll() as $row) {
$extra = json_decode($row['extra'] ?? '{}', true) ?: [];
$result[$row['aulario_id']] = array_merge($extra, [
'name' => $row['name'],
'icon' => $row['icon'],
]);
}
return $result;
}
// ── SuperCafe helpers ─────────────────────────────────────────────────────────
function db_get_supercafe_menu(string $centro_id): array
{
$stmt = db()->prepare('SELECT data FROM supercafe_menu WHERE org_id = ?');
$stmt->execute([$centro_id]);
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_supercafe_menu(string $centro_id, array $menu): void
{
db()->prepare('INSERT OR REPLACE INTO supercafe_menu (org_id, data, updated_at) VALUES (?, ?, datetime(\'now\'))')
->execute([$centro_id, json_encode($menu, JSON_UNESCAPED_UNICODE)]);
}
/** Return all SC orders for a centro as an array of rows. */
function db_get_supercafe_orders(string $centro_id): array
{
$stmt = db()->prepare(
'SELECT * FROM supercafe_orders WHERE org_id = ? ORDER BY created_at DESC'
);
$stmt->execute([$centro_id]);
return $stmt->fetchAll();
}
/** Return a single SC order by ref, or null. */
function db_get_supercafe_order(string $centro_id, string $order_ref): ?array
{
$stmt = db()->prepare(
'SELECT * FROM supercafe_orders WHERE org_id = ? AND order_ref = ?'
);
$stmt->execute([$centro_id, $order_ref]);
$row = $stmt->fetch();
return $row !== false ? $row : null;
}
/** Create or update an SC order. */
function db_upsert_supercafe_order(
string $centro_id,
string $order_ref,
string $fecha,
string $persona,
string $comanda,
string $notas,
string $estado
): void {
db()->prepare(
'INSERT INTO supercafe_orders (org_id, order_ref, fecha, persona, comanda, notas, estado)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(org_id, order_ref) DO UPDATE SET
fecha = excluded.fecha,
persona = excluded.persona,
comanda = excluded.comanda,
notas = excluded.notas,
estado = excluded.estado'
)->execute([$centro_id, $order_ref, $fecha, $persona, $comanda, $notas, $estado]);
}
/** Generate the next order_ref for a centro (sc001, sc002, …). */
function db_next_supercafe_ref(string $centro_id): string
{
$stmt = db()->prepare(
"SELECT order_ref FROM supercafe_orders WHERE org_id = ? ORDER BY id DESC LIMIT 1"
);
$stmt->execute([$centro_id]);
$last = $stmt->fetchColumn();
$n = 0;
if ($last && preg_match('/^sc(\d+)$/', $last, $m)) {
$n = (int) $m[1];
}
return 'sc' . str_pad($n + 1, 3, '0', STR_PAD_LEFT);
}
/** Count 'Deuda' orders for a persona in a centro. */
function db_supercafe_count_debts(string $centro_id, string $persona_key): int
{
$stmt = db()->prepare(
"SELECT COUNT(*) FROM supercafe_orders WHERE org_id = ? AND persona = ? AND estado = 'Deuda'"
);
$stmt->execute([$centro_id, $persona_key]);
return (int) $stmt->fetchColumn();
}
// ── Comedor helpers ───────────────────────────────────────────────────────────
function db_get_comedor_menu_types(string $centro_id, string $aulario_id): array
{
$stmt = db()->prepare(
'SELECT data FROM comedor_menu_types WHERE org_id = ? AND aulario_id = ?'
);
$stmt->execute([$centro_id, $aulario_id]);
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_comedor_menu_types(string $centro_id, string $aulario_id, array $types): void
{
db()->prepare(
'INSERT OR REPLACE INTO comedor_menu_types (org_id, aulario_id, data) VALUES (?, ?, ?)'
)->execute([$centro_id, $aulario_id, json_encode($types, JSON_UNESCAPED_UNICODE)]);
}
function db_get_comedor_entry(string $centro_id, string $aulario_id, string $ym, string $day): array
{
$stmt = db()->prepare(
'SELECT data FROM comedor_entries WHERE org_id = ? AND aulario_id = ? AND year_month = ? AND day = ?'
);
$stmt->execute([$centro_id, $aulario_id, $ym, $day]);
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_comedor_entry(string $centro_id, string $aulario_id, string $ym, string $day, array $data): void
{
db()->prepare(
'INSERT OR REPLACE INTO comedor_entries (org_id, aulario_id, year_month, day, data) VALUES (?, ?, ?, ?, ?)'
)->execute([$centro_id, $aulario_id, $ym, $day, json_encode($data, JSON_UNESCAPED_UNICODE)]);
}
// ── Diario helpers ────────────────────────────────────────────────────────────
function db_get_diario_entry(string $centro_id, string $aulario_id, string $entry_date): array
{
$stmt = db()->prepare(
'SELECT data FROM diario_entries WHERE org_id = ? AND aulario_id = ? AND entry_date = ?'
);
$stmt->execute([$centro_id, $aulario_id, $entry_date]);
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_diario_entry(string $centro_id, string $aulario_id, string $entry_date, array $data): void
{
db()->prepare(
'INSERT OR REPLACE INTO diario_entries (org_id, aulario_id, entry_date, data) VALUES (?, ?, ?, ?)'
)->execute([$centro_id, $aulario_id, $entry_date, json_encode($data, JSON_UNESCAPED_UNICODE)]);
}
// ── Panel alumno helpers ──────────────────────────────────────────────────────
function db_get_panel_alumno(string $centro_id, string $aulario_id, string $alumno): array
{
$stmt = db()->prepare(
'SELECT data FROM panel_alumno WHERE org_id = ? AND aulario_id = ? AND alumno = ?'
);
$stmt->execute([$centro_id, $aulario_id, $alumno]);
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_panel_alumno(string $centro_id, string $aulario_id, string $alumno, array $data): void
{
db()->prepare(
'INSERT OR REPLACE INTO panel_alumno (org_id, aulario_id, alumno, data) VALUES (?, ?, ?, ?)'
)->execute([$centro_id, $aulario_id, $alumno, json_encode($data, JSON_UNESCAPED_UNICODE)]);
}
// ── Invitation helpers ────────────────────────────────────────────────────────
function db_get_all_invitations(): array
{
return db()->query('SELECT * FROM invitations ORDER BY code')->fetchAll();
}
function db_get_invitation(string $code): ?array
{
$stmt = db()->prepare('SELECT * FROM invitations WHERE code = ?');
$stmt->execute([strtoupper($code)]);
$row = $stmt->fetch();
return $row !== false ? $row : null;
}
function db_upsert_invitation(string $code, bool $active, bool $single_use): void
{
db()->prepare(
'INSERT OR REPLACE INTO invitations (code, active, single_use) VALUES (?, ?, ?)'
)->execute([strtoupper($code), (int) $active, (int) $single_use]);
}
function db_deactivate_invitation(string $code): void
{
db()->prepare('UPDATE invitations SET active = 0 WHERE code = ?')->execute([strtoupper($code)]);
}
function db_delete_invitation(string $code): void
{
db()->prepare('DELETE FROM invitations WHERE code = ?')->execute([strtoupper($code)]);
}
// ── Club helpers ──────────────────────────────────────────────────────────────
function db_get_club_config(): array
{
$stmt = db()->query('SELECT data FROM club_config WHERE id = 1');
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_club_config(array $config): void
{
db()->prepare('INSERT OR REPLACE INTO club_config (id, data) VALUES (1, ?)')
->execute([json_encode($config, JSON_UNESCAPED_UNICODE)]);
}
function db_get_all_club_events(): array
{
return db()->query('SELECT date_ref, data FROM club_events ORDER BY date_ref DESC')->fetchAll();
}
function db_get_club_event(string $date_ref): array
{
$stmt = db()->prepare('SELECT data FROM club_events WHERE date_ref = ?');
$stmt->execute([$date_ref]);
$row = $stmt->fetch();
if ($row === false) {
return [];
}
return json_decode($row['data'], true) ?: [];
}
function db_set_club_event(string $date_ref, array $data): void
{
db()->prepare('INSERT OR REPLACE INTO club_events (date_ref, data) VALUES (?, ?)')
->execute([$date_ref, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)]);
}
// ── Multi-tenant helpers ──────────────────────────────────────────────────────
/** Return all organization IDs the authenticated user belongs to. */
function get_user_organizations(?array $auth_data = null): array
{
$data = $auth_data ?? $_SESSION['auth_data'] ?? [];
$orgs = $data['organizations']
?? $data['orgs']
?? $data['aulatek']['organizaciones']
?? $data['aulatek']['organizations']
?? $data['aulatek']['centros']
?? $data['entreaulas']['organizaciones']
?? $data['entreaulas']['organizations']
?? $data['entreaulas']['centros']
?? [];
if (!empty($orgs) && is_array($orgs)) {
return array_values(array_unique(array_filter($orgs, static function ($value): bool {
return is_string($value) && $value !== '';
})));
}
if (!empty($orgs)) {
return [(string) $orgs];
}
foreach ([
$data['active_organization'] ?? null,
$data['aulatek']['organizacion'] ?? null,
$data['aulatek']['organization'] ?? null,
$data['aulatek']['centro'] ?? null,
$data['entreaulas']['organizacion'] ?? null,
$data['entreaulas']['organization'] ?? null,
$data['entreaulas']['centro'] ?? null,
] as $single) {
if (is_string($single) && $single !== '') {
return [$single];
}
}
return [];
}
/** Spanish alias used by pre-body.php menu rendering. */
function get_user_organizaciones(?array $auth_data = null): array
{
$org_ids = get_user_organizations($auth_data);
if (empty($org_ids)) {
return [];
}
$name_by_id = [];
foreach (db_get_organizations() as $org_row) {
$name_by_id[$org_row['org_id']] = $org_row['org_name'];
}
$result = [];
foreach ($org_ids as $org_id) {
$result[$org_id] = $name_by_id[$org_id] ?? $org_id;
}
return $result;
}
function get_user_centros(?array $auth_data = null): array
{
return get_user_organizations($auth_data);
}
/** Ensure active organization session keys are set and mirrored for legacy code. */
function init_active_org(?array $auth_data = null): void
{
$organizations = get_user_organizations($auth_data);
if (empty($organizations)) {
$_SESSION['active_organization'] = null;
$_SESSION['active_organizacion'] = null;
$_SESSION['active_centro'] = null;
return;
}
$current = $_SESSION['active_organization']
?? $_SESSION['active_organizacion']
?? $_SESSION['active_centro']
?? null;
if (!is_string($current) || !in_array($current, $organizations, true)) {
$current = $organizations[0];
}
$_SESSION['active_organization'] = $current;
$_SESSION['active_organizacion'] = $current;
$_SESSION['active_centro'] = $current;
if (!isset($_SESSION['auth_data']) || !is_array($_SESSION['auth_data'])) {
$_SESSION['auth_data'] = [];
}
$_SESSION['auth_data']['active_organization'] = $current;
if (!isset($_SESSION['auth_data']['aulatek']) || !is_array($_SESSION['auth_data']['aulatek'])) {
$_SESSION['auth_data']['aulatek'] = [];
}
$_SESSION['auth_data']['aulatek']['organizacion'] = $current;
$_SESSION['auth_data']['aulatek']['organization'] = $current;
$_SESSION['auth_data']['aulatek']['centro'] = $current;
if (!isset($_SESSION['auth_data']['entreaulas']) || !is_array($_SESSION['auth_data']['entreaulas'])) {
$_SESSION['auth_data']['entreaulas'] = [];
}
$_SESSION['auth_data']['entreaulas']['organizacion'] = $current;
$_SESSION['auth_data']['entreaulas']['organization'] = $current;
$_SESSION['auth_data']['entreaulas']['centro'] = $current;
}
function init_active_centro(?array $auth_data = null): void
{
init_active_org($auth_data);
}
// ── User session helpers (Dispositivos conectados) ────────────────────────────
/**
* Register or refresh a session record in user_sessions.
* Stores a SHA-256 hash of the PHP session_id so the raw token never reaches the DB.
*/
function db_register_session(string $username): void
{
$token = hash('sha256', session_id());
$ip = $_SERVER['HTTP_X_FORWARDED_FOR']
?? $_SERVER['REMOTE_ADDR']
?? '';
// Only keep the first IP if X-Forwarded-For contains a list
$ip = trim(explode(',', $ip)[0]);
$ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512);
db()->prepare(
"INSERT INTO user_sessions (session_token, username, ip_address, user_agent)
VALUES (?, ?, ?, ?)
ON CONFLICT(session_token) DO UPDATE SET
last_active = datetime('now'),
username = excluded.username"
)->execute([$token, strtolower($username), $ip, $ua]);
}
/** Update last_active for the current session. */
function db_touch_session(): void
{
$token = hash('sha256', session_id());
db()->prepare(
"UPDATE user_sessions SET last_active = datetime('now') WHERE session_token = ?"
)->execute([$token]);
}
/** Delete the current session record from the DB. */
function db_delete_session(): void
{
$token = hash('sha256', session_id());
db()->prepare('DELETE FROM user_sessions WHERE session_token = ?')->execute([$token]);
}
/**
* Delete a specific session by token for a given user.
* Enforces that the session belongs to the requesting user.
*/
function db_revoke_session(string $token, string $username): void
{
db()->prepare('DELETE FROM user_sessions WHERE session_token = ? AND username = ?')
->execute([$token, strtolower($username)]);
}
/** Delete all session records for a user (e.g. on password change or account wipe). */
function db_delete_user_sessions(string $username): void
{
db()->prepare('DELETE FROM user_sessions WHERE username = ?')
->execute([strtolower($username)]);
}
/** Return all session rows for a user, newest first. */
function db_get_user_sessions(string $username): array
{
$stmt = db()->prepare(
'SELECT session_token, ip_address, user_agent, created_at, last_active
FROM user_sessions
WHERE username = ?
ORDER BY last_active DESC'
);
$stmt->execute([strtolower($username)]);
return $stmt->fetchAll();
}
/**
* Check whether the current PHP session has a valid record in user_sessions.
* Returns false if the session was revoked or was never registered.
*/
function db_session_is_valid(string $username): bool
{
$token = hash('sha256', session_id());
$stmt = db()->prepare(
'SELECT 1 FROM user_sessions WHERE session_token = ? AND username = ? LIMIT 1'
);
$stmt->execute([$token, strtolower($username)]);
return $stmt->fetchColumn() !== false;
}