diff --git a/admin.php b/admin.php index f24c5c0..306ede3 100644 --- a/admin.php +++ b/admin.php @@ -30,6 +30,10 @@ define('DB_PORT', $env['DB_PORT'] ?? '5432'); define('DB_NAME', $env['DB_NAME'] ?? 'freedoms4'); define('DB_USER', $env['DB_USER'] ?? 'freedoms4_user'); define('DB_PASS', $env['DB_PASS'] ?? ''); +define('PROSODY_DB_NAME', $env['PROSODY_DB_NAME'] ?? 'prosody'); +define('PROSODY_DB_USER', $env['PROSODY_DB_USER'] ?? 'prosody'); +define('PROSODY_DB_PASS', $env['PROSODY_DB_PASS'] ?? ''); +define('PROSODY_HOST', $env['PROSODY_HOST'] ?? 'freedoms4.org'); define('SESSION_NAME', 'f4_session'); define('SESSION_SECURE', true); @@ -83,6 +87,135 @@ function db_connect(): PDO { return $pdo; } +function prosody_db_connect(): ?PDO { + static $pdo = null; + static $failed = false; + if ($pdo !== null) return $pdo; + if ($failed) return null; + try { + $dsn = sprintf('pgsql:host=127.0.0.1;port=5432;dbname=%s', PROSODY_DB_NAME); + $pdo = new PDO($dsn, PROSODY_DB_USER, PROSODY_DB_PASS, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + return $pdo; + } catch (PDOException $e) { + error_log('admin.php Prosody DB error: ' . $e->getMessage()); + $failed = true; + return null; + } +} + +// Remove a user's XMPP account (all stores: accounts, roster, vcard, etc.) +// so it doesn't keep working indefinitely after the user is deleted. +function delete_xmpp_account(string $username): void { + $pdo = prosody_db_connect(); + if (!$pdo) return; // Prosody DB unreachable — already logged, don't block user deletion on it + try { + $pdo->prepare('DELETE FROM prosody WHERE host = :h AND "user" = :u') + ->execute([':h' => PROSODY_HOST, ':u' => strtolower($username)]); + } catch (Exception $e) { + error_log("delete_xmpp_account failed for {$username}: " . $e->getMessage()); + } +} + +function ensure_xmpp_backup_table(PDO $pdo): void { + $pdo->exec( + "CREATE TABLE IF NOT EXISTS xmpp_account_backups ( + username VARCHAR(32) PRIMARY KEY, + rows JSONB NOT NULL, + blocked_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + )" + ); +} + +// Back up a user's Prosody rows (all stores) into xmpp_account_backups, +// then remove them from the live prosody table — mirroring how +// email-block.sh moves the Dovecot passwd-file entry into a backup file. +function xmpp_backup_and_remove(string $username): void { + $prosody = prosody_db_connect(); + if (!$prosody) return; // logged already — don't block the block/delete action on it + + try { + $stmt = $prosody->prepare('SELECT host, "user", store, key, type, value FROM prosody WHERE host = :h AND "user" = :u'); + $stmt->execute([':h' => PROSODY_HOST, ':u' => strtolower($username)]); + $rows = $stmt->fetchAll(); + if (!$rows) return; // nothing to back up (e.g. never had an XMPP account) + + $pdo = db_connect(); + ensure_xmpp_backup_table($pdo); + $pdo->prepare( + 'INSERT INTO xmpp_account_backups (username, rows, blocked_at) + VALUES (:u, :rows, NOW()) + ON CONFLICT (username) DO UPDATE SET rows = :rows2, blocked_at = NOW()' + )->execute([ + ':u' => strtolower($username), + ':rows' => json_encode($rows), + ':rows2' => json_encode($rows), + ]); + + $prosody->prepare('DELETE FROM prosody WHERE host = :h AND "user" = :u') + ->execute([':h' => PROSODY_HOST, ':u' => strtolower($username)]); + } catch (Exception $e) { + error_log("xmpp_backup_and_remove failed for {$username}: " . $e->getMessage()); + } +} + +// Restore a previously backed-up XMPP account (on unblock) and remove the +// backup row once restored. +function xmpp_restore_from_backup(string $username): void { + try { + $pdo = db_connect(); + ensure_xmpp_backup_table($pdo); + $stmt = $pdo->prepare('SELECT rows FROM xmpp_account_backups WHERE username = :u LIMIT 1'); + $stmt->execute([':u' => strtolower($username)]); + $row = $stmt->fetch(); + if (!$row) return; // nothing backed up — nothing to restore + + $prosody = prosody_db_connect(); + if (!$prosody) return; // can't restore right now — backup row stays put, safe to retry later + + $entries = json_decode($row['rows'], true) ?: []; + $insert = $prosody->prepare( + 'INSERT INTO prosody (host, "user", store, key, type, value) VALUES (:host, :user, :store, :key, :type, :value)' + ); + foreach ($entries as $entry) { + try { + $insert->execute([ + ':host' => $entry['host'], + ':user' => $entry['user'], + ':store' => $entry['store'], + ':key' => $entry['key'], + ':type' => $entry['type'], + ':value' => $entry['value'], + ]); + } catch (Exception $e) { + // Row may already exist (e.g. partial prior restore) — skip and continue + error_log("xmpp_restore_from_backup: skipped one row for {$username}: " . $e->getMessage()); + } + } + + $pdo->prepare('DELETE FROM xmpp_account_backups WHERE username = :u') + ->execute([':u' => strtolower($username)]); + } catch (Exception $e) { + error_log("xmpp_restore_from_backup failed for {$username}: " . $e->getMessage()); + } +} + +// Remove any leftover XMPP backup row for a user (used on permanent +// deletion, in case they were blocked at some point before being deleted). +function xmpp_delete_backup(string $username): void { + try { + $pdo = db_connect(); + ensure_xmpp_backup_table($pdo); + $pdo->prepare('DELETE FROM xmpp_account_backups WHERE username = :u') + ->execute([':u' => strtolower($username)]); + } catch (Exception $e) { + error_log("xmpp_delete_backup failed for {$username}: " . $e->getMessage()); + } +} + // ── Session + admin check ── if (session_status() === PHP_SESSION_NONE) { session_name(SESSION_NAME); @@ -160,6 +293,10 @@ if ($action === 'block_user') { $safe_user = escapeshellarg($target['username']); shell_exec("sudo /usr/local/bin/email-block block {$safe_user} 2>&1"); + // Backup and remove their XMPP/Prosody account so it stops working + // while blocked, restorable on unblock. + xmpp_backup_and_remove($target['username']); + json_out(['success' => true]); } @@ -174,6 +311,9 @@ if ($action === 'unblock_user') { $safe_user = escapeshellarg($target['username']); shell_exec("sudo /usr/local/bin/email-block unblock {$safe_user} 2>&1"); + // Restore their XMPP/Prosody account from backup, if one exists. + xmpp_restore_from_backup($target['username']); + json_out(['success' => true]); } @@ -203,6 +343,13 @@ if ($action === 'delete_user') { $pdo->prepare('DELETE FROM users WHERE id = :id') ->execute([':id' => $user_id]); + // Remove the user's XMPP/Prosody account so it stops working once their + // freedoms4 account is gone, instead of remaining usable indefinitely. + delete_xmpp_account($target['username']); + // ...and remove any leftover backup row too, in case they were blocked + // at some point before being deleted outright. + xmpp_delete_backup($target['username']); + // Clear OTP history for this email so re-signing-up doesn't hit the // daily OTP request limit because of OTPs sent before deletion. $pdo->prepare('DELETE FROM email_otps WHERE email = :e')