| 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 |
|