| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- <?php
- /**
- * Admin authentication: file-based credentials, session login,
- * brute-force throttling, and online password change.
- */
- declare(strict_types=1);
- const AUTH_MAX_FAILURES = 5;
- const AUTH_LOCK_SECONDS = 900; // 15 minutes
- function credentials_load(): array
- {
- $file = CONFIG_DIR . '/credentials.php';
- if (!is_file($file)) {
- http_response_code(500);
- exit('Missing config/credentials.php — copy config/credentials.sample.php.');
- }
- return require $file;
- }
- function auth_check(): bool
- {
- session_boot();
- return !empty($_SESSION['admin']);
- }
- /** Gatekeeper at the top of every admin page. */
- function auth_require(): void
- {
- if (!auth_check()) {
- redirect('login.php');
- }
- }
- function auth_throttle_file(): string
- {
- return DATA_DIR . '/login-throttle.json';
- }
- /** Seconds until login is allowed again, 0 if not locked. */
- function auth_locked_for(): int
- {
- $t = json_read(auth_throttle_file(), ['failures' => 0, 'last' => 0]);
- if (($t['failures'] ?? 0) < AUTH_MAX_FAILURES) {
- return 0;
- }
- $remaining = ($t['last'] ?? 0) + AUTH_LOCK_SECONDS - time();
- return max(0, $remaining);
- }
- function auth_attempt(string $username, string $password): bool
- {
- session_boot();
- if (auth_locked_for() > 0) {
- return false;
- }
- $cred = credentials_load();
- $ok = hash_equals($cred['username'], $username)
- && password_verify($password, $cred['password_hash']);
- if ($ok) {
- if (is_file(auth_throttle_file())) {
- @unlink(auth_throttle_file());
- }
- session_regenerate_id(true);
- $_SESSION['admin'] = true;
- return true;
- }
- $t = json_read(auth_throttle_file(), ['failures' => 0, 'last' => 0]);
- // A stale lock window restarts the count.
- if (time() - ($t['last'] ?? 0) > AUTH_LOCK_SECONDS) {
- $t['failures'] = 0;
- }
- $t['failures'] = ($t['failures'] ?? 0) + 1;
- $t['last'] = time();
- json_write(auth_throttle_file(), $t);
- return false;
- }
- function auth_logout(): void
- {
- session_boot();
- $_SESSION = [];
- session_destroy();
- }
- /**
- * Change the admin password: verifies the current one, then atomically
- * rewrites config/credentials.php. Returns an error message or null on success.
- */
- function auth_change_password(string $current, string $new): ?string
- {
- $cred = credentials_load();
- if (!password_verify($current, $cred['password_hash'])) {
- return 'Current password is incorrect.';
- }
- if (strlen($new) < 8) {
- return 'New password must be at least 8 characters.';
- }
- $cred['password_hash'] = password_hash($new, PASSWORD_DEFAULT);
- $file = CONFIG_DIR . '/credentials.php';
- $php = "<?php\n// Rewritten by the admin settings page on " . date('c') . "\n"
- . "return " . var_export($cred, true) . ";\n";
- $tmp = $file . '.' . bin2hex(random_bytes(6)) . '.tmp';
- if (file_put_contents($tmp, $php, LOCK_EX) === false || !rename($tmp, $file)) {
- @unlink($tmp);
- return 'Could not write credentials file — check that config/ is writable.';
- }
- if (function_exists('opcache_invalidate')) {
- @opcache_invalidate($file, true);
- }
- return null;
- }
|