Replace username/password authentication with FIDO2/WebAuthn passkey-based auth using webauthn-rs. Sessions and bearer tokens are unchanged — only the way they are created changes. - Add webauthn-rs, uuid, open, url deps; remove argon2, rpassword - Add passkey_credentials and registration_tokens tables (migrations 17-18) - Add domain entities, typed IDs, and repository traits for passkeys/tokens - Add SQL repository implementations for passkeys and registration tokens - Add ChallengeStore for in-memory WebAuthn ceremony state - Add WebAuthn route handlers (register/auth start+finish ceremonies) - Add CLI browser handoff for token creation (opens browser, local callback) - Replace login form with "Sign in with Passkey" button - Add registration page for first-user bootstrap via one-time token - Replace BREWLOG_ADMIN_USERNAME/PASSWORD with BREWLOG_RP_ID/RP_ORIGIN - Change default BREWLOG_URL from 127.0.0.1 to localhost (WebAuthn requires it)
79 lines
2.6 KiB
HTML
79 lines
2.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Brewlog · Login{% endblock %}
|
|
{% block head %}
|
|
<script src="/webauthn.js"></script>
|
|
{% endblock %}
|
|
{% block content %}
|
|
<div class="mx-auto max-w-md">
|
|
<div class="rounded-lg border border-amber-300 bg-amber-100/80 p-6 shadow-sm">
|
|
<h1 class="text-2xl font-semibold text-amber-700">Login</h1>
|
|
<p class="mt-2 text-sm text-stone-600">
|
|
Sign in with your passkey to manage your coffee log.
|
|
</p>
|
|
|
|
<div id="login-error" class="mt-4 hidden rounded-md bg-red-100 border border-red-300 p-3 text-sm text-red-800"></div>
|
|
|
|
<div id="login-unsupported" class="mt-4 hidden rounded-md bg-yellow-100 border border-yellow-300 p-3 text-sm text-yellow-800">
|
|
Your browser does not support passkeys. Please use a modern browser (Chrome, Firefox, Safari, or Edge).
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
<button
|
|
id="login-button"
|
|
type="button"
|
|
class="w-full rounded-md bg-amber-600 px-4 py-3 text-sm font-semibold text-amber-50 transition hover:bg-amber-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Sign in with Passkey
|
|
</button>
|
|
|
|
<div id="login-loading" class="mt-4 hidden text-center text-sm text-stone-500">
|
|
<p>Waiting for passkey...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const button = document.getElementById("login-button");
|
|
const errorDiv = document.getElementById("login-error");
|
|
const loadingDiv = document.getElementById("login-loading");
|
|
const unsupportedDiv = document.getElementById("login-unsupported");
|
|
|
|
// Check WebAuthn support
|
|
if (!window.PublicKeyCredential) {
|
|
button.disabled = true;
|
|
unsupportedDiv.classList.remove("hidden");
|
|
return;
|
|
}
|
|
|
|
// Check for CLI callback query params
|
|
const params = new URLSearchParams(window.location.search);
|
|
let queryString = "";
|
|
if (params.has("cli_callback")) {
|
|
queryString = "?" + params.toString();
|
|
}
|
|
|
|
button.addEventListener("click", async function () {
|
|
errorDiv.classList.add("hidden");
|
|
loadingDiv.classList.remove("hidden");
|
|
button.disabled = true;
|
|
|
|
try {
|
|
const result = await startPasskeyAuthentication(queryString);
|
|
|
|
if (result.redirect) {
|
|
window.location.href = result.redirect;
|
|
} else {
|
|
window.location.href = "/";
|
|
}
|
|
} catch (err) {
|
|
errorDiv.textContent = err.message;
|
|
errorDiv.classList.remove("hidden");
|
|
loadingDiv.classList.add("hidden");
|
|
button.disabled = false;
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|