97 lines · 3.2 KB
Raw Download
1
<?php
2
require_once __DIR__ . '/inc/webauthn.php';
3
require_once __DIR__ . '/inc/layout.php';
4
5
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
6
header('Pragma: no-cache');
7
header('Expires: 0');
8
9
// Logout runs before the cache-buster redirect so it takes effect on the
10
// first request rather than after a bounce, and so the stamped URL is never
11
// the one carrying ?logout=1. date('His') repeated within a second, so the
12
// destination is stamped with random bytes instead.
13
if (isset($_GET['logout'])) {
14
    log_out();
15
    header('Location: index.php?loggedout=' . bin2hex(random_bytes(4)));
16
    exit;
17
}
18
19
// Same stale-cache defense as the other pages; the login POST passes through.
20
cache_buster_redirect();
21
22
if (is_logged_in()) {
23
    header('Location: index.php');
24
    exit;
25
}
26
27
$firstRun = !password_is_set();
28
$error = '';
29
30
// Offer the passkey button only when there is one registered for this host
31
// that the browser could actually use — otherwise it can only fail.
32
$hasPasskeys = false;
33
if (!$firstRun && webauthn_available()) {
34
    $st = db()->prepare('SELECT COUNT(*) FROM passkeys WHERE rp_id = ?');
35
    $st->execute([webauthn_rp_id()]);
36
    $hasPasskeys = (int)$st->fetchColumn() > 0;
37
}
38
39
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
40
    check_csrf();
41
    $pass = (string)($_POST['password'] ?? '');
42
    if ($firstRun) {
43
        if (strlen($pass) < 8) {
44
            $error = 'Password must be at least 8 characters.';
45
        } elseif ($pass !== (string)($_POST['confirm'] ?? '')) {
46
            $error = 'Passwords do not match.';
47
        } else {
48
            save_password($pass);
49
            log_in();
50
            remember_login();
51
            flash('Password set. Welcome!');
52
            header('Location: index.php');
53
            exit;
54
        }
55
    } elseif (verify_password($pass)) {
56
        log_in();
57
        remember_login();
58
        header('Location: index.php');
59
        exit;
60
    } else {
61
        sleep(1);
62
        $error = 'Wrong password.';
63
    }
64
}
65
66
page_header($firstRun ? 'Set password' : 'Login');
67
68
?>
69
<h1><?= $firstRun ? 'Set a password' : 'Login' ?></h1>
70
<?php if ($firstRun): ?>
71
<p class="muted">First run: choose the admin password for this site. It will be required for adding, editing, and using filament.</p>
72
<?php endif; ?>
73
<?php if ($error): ?>
74
<p class="error"><?= e($error) ?></p>
75
<?php endif; ?>
76
<form method="post">
77
  <?= csrf_field() ?>
78
  <label for="password">Password</label>
79
  <input type="password" id="password" name="password" required autofocus autocomplete="<?= $firstRun ? 'new-password' : 'current-password' ?>">
80
<?php if ($firstRun): ?>
81
  <label for="confirm">Confirm password</label>
82
  <input type="password" id="confirm" name="confirm" required autocomplete="new-password">
83
<?php endif; ?>
84
  <div class="actions">
85
    <button><?= $firstRun ? 'Set password' : 'Login' ?></button>
86
  </div>
87
</form>
88
<?php if ($hasPasskeys): ?>
89
<script type="application/json" id="pkCfg"><?= json_encode(['csrf' => csrf_token()], JSON_HEX_TAG | JSON_HEX_AMP) ?></script>
90
<div class="actions">
91
  <button type="button" class="secondary" id="pkLogin">Sign in with a passkey</button>
92
</div>
93
<p class="muted" id="pkMsg"></p>
94
<?php endif; ?>
95
<?php
96
page_footer($hasPasskeys ? ['assets/passkey.js'] : []);
97