diff options
Diffstat (limited to 'admin/login.php')
| -rw-r--r-- | admin/login.php | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/admin/login.php b/admin/login.php new file mode 100644 index 0000000..0fe0d1d --- /dev/null +++ b/admin/login.php @@ -0,0 +1,117 @@ +<?php +require_once __DIR__ . '/../partials/session.php'; +require_once __DIR__ . '/../partials/proxy_helpers.php'; + +// Post-login destination: honour a local ?ref= path (no open redirects), +// otherwise land on the unified admin panel. +$ref = $_GET['ref'] ?? ''; +$dest = (is_string($ref) && $ref !== '' && $ref[0] === '/' && substr($ref, 0, 2) !== '//') ? $ref : '/admin.php'; + +if (!empty($_SESSION['admin'])) { + header('Location: ' . $dest); + exit; +} + +if (empty($_SESSION['csrf'])) { + $_SESSION['csrf'] = bin2hex(random_bytes(32)); +} + +/** + * Load admin credentials from outside the document root. + * Override path with ADMIN_AUTH_FILE in the php-fpm environment. + * + * File returns: ['algo' => 'password_hash'|'sha256', 'hash' => string, 'salt' => string?] + */ +function admin_auth_config(): ?array { + $path = getenv('ADMIN_AUTH_FILE'); + if (!is_string($path) || $path === '') { + $path = '/var/lib/sillylaird/admin_auth.php'; + } + if (!is_file($path) || !is_readable($path)) { + return null; + } + $cfg = include $path; + return is_array($cfg) ? $cfg : null; +} + +function admin_password_ok(string $provided): bool { + $cfg = admin_auth_config(); + if ($cfg === null) { + return false; + } + $hash = (string)($cfg['hash'] ?? ''); + if ($hash === '') { + return false; + } + $algo = strtolower((string)($cfg['algo'] ?? 'password_hash')); + if ($algo === 'password_hash' || str_starts_with($hash, '$2y$') || str_starts_with($hash, '$2a$') || str_starts_with($hash, '$argon')) { + return password_verify($provided, $hash); + } + // Legacy: sha256(password + salt) + $salt = (string)($cfg['salt'] ?? ''); + $computed = hash('sha256', $provided . $salt); + return hash_equals($hash, $computed); +} + +$error = ''; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!proxy_rate_limit('admin_login', 5, 300)) { + http_response_code(429); + $error = 'Too many attempts. Try again in a few minutes.'; + } else { + $token = $_POST['csrf'] ?? ''; + if (!is_string($token) || !hash_equals($_SESSION['csrf'], $token)) { + $error = 'Bad request.'; + } else { + $provided = $_POST['password'] ?? ''; + if (is_string($provided) && admin_password_ok($provided)) { + session_regenerate_id(true); + $_SESSION['admin'] = true; + unset($_SESSION['csrf']); + header('Location: ' . $dest); + exit; + } + $error = 'Wrong password.'; + usleep(random_int(150000, 400000)); + } + } +} +?> +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <title>Admin Login — SillyLaird</title> + <link rel="icon" href="https://www.sillylaird.ca/assets/img/lain.png" /> + <link rel="stylesheet" href="/assets/css/fonts.css" /> + <link rel="stylesheet" href="/assets/css/site.css?v=1.5" /> + <link rel="stylesheet" href="/assets/css/skeleton.css" /> +</head> +<body> + <a class="skip-link" href="#main">Skip to content</a> + <?php include $_SERVER['DOCUMENT_ROOT'] . '/partials/header.php'; ?> + + <main id="main" class="wrap stack"> + <section aria-labelledby="login-title"> + <h1 id="login-title">Admin Login</h1> + + <?php if ($error): ?> + <p style="color: var(--accent); font-size: var(--fs-sm);"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></p> + <?php endif; ?> + + <form method="post" action="" style="max-width: 360px;"> + <input type="hidden" name="csrf" value="<?= htmlspecialchars($_SESSION['csrf'], ENT_QUOTES, 'UTF-8') ?>" /> + <div class="field-group"> + <label for="password">Password</label> + <input type="password" id="password" name="password" autofocus autocomplete="current-password" /> + </div> + <button type="submit">Enter</button> + </form> + </section> + </main> + + <?php include $_SERVER['DOCUMENT_ROOT'] . '/partials/footer.php'; ?> +</body> +</html> |
