Enables password managers (1Password, iCloud Keychain) to offer passkey suggestions via autofill on the login page, matching behavior of other passkey-enabled websites.
119 lines
3.7 KiB
HTML
119 lines
3.7 KiB
HTML
{% extends "base.html" %} {% import "partials/icons.html" as icons %}
|
|
{% block title %}Brewlog · Login{% endblock %}
|
|
{% block head %}
|
|
<script
|
|
defer
|
|
src="/static/js/webauthn.js?v={{ version_info.commit }}"
|
|
></script>
|
|
{% endblock %}
|
|
{% block content %}
|
|
<div class="mx-auto max-w-md">
|
|
<div class="rounded-lg border bg-surface p-6">
|
|
<h1 class="text-2xl font-semibold text-accent">Login</h1>
|
|
<p class="mt-2 text-sm text-text-secondary">
|
|
Sign in with a passkey to access the coffee log.
|
|
</p>
|
|
|
|
<div
|
|
id="login-error"
|
|
class="mt-4 hidden rounded-md bg-error-bg border border-error-border p-3 text-sm text-error-text"
|
|
role="alert"
|
|
></div>
|
|
|
|
<div
|
|
id="login-unsupported"
|
|
class="mt-4 hidden rounded-md bg-warning-bg border border-warning-border p-3 text-sm text-warning-text"
|
|
>
|
|
This browser does not support passkeys. Please use a modern browser
|
|
(Chrome, Firefox, Safari, or Edge).
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
<input
|
|
type="text"
|
|
autocomplete="username webauthn"
|
|
id="conditional-ui-input"
|
|
class="hidden"
|
|
aria-hidden="true"
|
|
tabindex="-1"
|
|
/>
|
|
|
|
<button
|
|
id="login-button"
|
|
type="button"
|
|
onclick="handleLogin()"
|
|
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-3 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{{ icons::key("h-4 w-4") }} Sign in with Passkey
|
|
</button>
|
|
|
|
<div
|
|
id="login-loading"
|
|
class="mt-4 hidden text-center text-sm text-text-muted"
|
|
>
|
|
<p>Waiting for passkey...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let conditionalAbort = null;
|
|
|
|
const handleLogin = async () => {
|
|
if (conditionalAbort) {
|
|
conditionalAbort.abort();
|
|
conditionalAbort = null;
|
|
}
|
|
|
|
const button = document.getElementById("login-button");
|
|
const errorDiv = document.getElementById("login-error");
|
|
const loadingDiv = document.getElementById("login-loading");
|
|
|
|
errorDiv.classList.add("hidden");
|
|
loadingDiv.classList.remove("hidden");
|
|
button.disabled = true;
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
let queryString = "";
|
|
if (params.has("cli_callback")) {
|
|
queryString = `?${params.toString()}`;
|
|
}
|
|
|
|
try {
|
|
const result = await startPasskeyAuthentication(queryString);
|
|
window.location.href = result.redirect || "/";
|
|
} catch (err) {
|
|
errorDiv.textContent = err.message;
|
|
errorDiv.classList.remove("hidden");
|
|
loadingDiv.classList.add("hidden");
|
|
button.disabled = false;
|
|
}
|
|
};
|
|
|
|
const initConditionalUI = async () => {
|
|
if (!window.PublicKeyCredential?.isConditionalMediationAvailable) return;
|
|
const available =
|
|
await PublicKeyCredential.isConditionalMediationAvailable();
|
|
if (!available) return;
|
|
|
|
conditionalAbort = new AbortController();
|
|
try {
|
|
const result = await startConditionalUIAuthentication(
|
|
conditionalAbort.signal,
|
|
);
|
|
window.location.href = result.redirect || "/";
|
|
} catch {
|
|
// Silently ignored — user may use the button instead, or the
|
|
// abort controller was triggered by handleLogin().
|
|
}
|
|
};
|
|
|
|
if (!window.PublicKeyCredential) {
|
|
document.getElementById("login-button").disabled = true;
|
|
document.getElementById("login-unsupported").classList.remove("hidden");
|
|
} else {
|
|
initConditionalUI();
|
|
}
|
|
</script>
|
|
{% endblock %}
|