aboutsummaryrefslogtreecommitdiffstats
path: root/admin/login.php
diff options
context:
space:
mode:
authorsillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
committersillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
commit898b52edcb47bcb3e9d6106e74ca73e74ea01e70 (patch)
tree85c6ee5ad58b860144551184d4cf86b560c62b91 /admin/login.php
downloadwww-898b52edcb47bcb3e9d6106e74ca73e74ea01e70.tar.gz
www-898b52edcb47bcb3e9d6106e74ca73e74ea01e70.zip
import live www.sillylaird.ca webrootHEADmain
Diffstat (limited to 'admin/login.php')
-rw-r--r--admin/login.php117
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>