{% extends "base.html" %}
{% block title %}Sign in{% endblock %}
{# No topbar on the sign-in page: every link in it needs a session. #}
{% block chrome %}{% endblock %}
{% block content %}
<form class="card" id="login-form">
<h1>Sign in</h1>
{# The OAuth callback redirects here with ?error=google on any failure. The
reason is in the log, not the query string - which of "no such account" and
"bad token" it was is not a stranger's business. #}
{% if request.args.get("error") == "google" %}
<p class="error">That Google sign-in did not work. Try again, or use a password.</p>
{% endif %}
<label>Email
<input type="email" name="email" autocomplete="username" required autofocus>
</label>
<label>Password
<input type="password" name="password" autocomplete="current-password" required>
</label>
<p class="error" id="login-error" hidden></p>
{# Shown only when the server says the address was never confirmed. Keyed off
the `unverified` flag rather than the wording of the message, so the two
cannot drift apart. #}
<p class="note" id="resend-row" hidden>
<button type="button" class="linkbtn" id="resend">Send the confirmation email again</button>
</p>
<button type="submit" id="login-submit">Sign in</button>
{% if google_enabled() %}
<div class="divider"><span>or</span></div>
{# A link, not a fetch: the OAuth flow is a browser redirect to Google. #}
<a class="button-secondary" href="/auth/google/start">Sign in with Google</a>
{% endif %}
<p class="muted">
<a href="/forgot">Forgot your password?</a> ·
<a href="/signup">Create an account</a>
</p>
</form>
{% endblock %}
{% block scripts %}
<script src="/static/js/webauthn.js"></script>
<script>
const form = document.getElementById("login-form");
const error = document.getElementById("login-error");
const button = document.getElementById("login-submit");
const resendRow = document.getElementById("resend-row");
const show = (message) => { error.textContent = message; error.hidden = false; };
form.addEventListener("submit", async (e) => {
e.preventDefault();
error.hidden = true;
resendRow.hidden = true;
button.disabled = true;
const data = new FormData(form);
try {
const res = await api.post("/api/login", {
email: data.get("email"),
password: data.get("password"),
});
// The password was right and the account has an authenticator. No session
// exists yet - the server parked it - so the assertion is what finishes
// the sign-in.
if (res.mfa_required) {
button.textContent = "Waiting for your passkey…";
await webauthn.assertLogin();
}
// A full navigation, not a fetch: the session cookie is set now, and the
// next page must be rendered by the server with it.
window.location.href = "/";
} catch (err) {
show(err.message);
// `unverified` is a flag on the error body, not a string match.
if (err.data && err.data.unverified) resendRow.hidden = false;
button.disabled = false;
button.textContent = "Sign in";
}
});
document.getElementById("resend").addEventListener("click", async () => {
const email = new FormData(form).get("email");
// Answers 200 whatever the address, so there is one thing to say.
await api.post("/api/signup/resend", { email });
resendRow.hidden = true;
show("If that address has an unconfirmed account, a new link is on its way.");
});
</script>
{% endblock %}