agilentics / boiler
{% 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> &middot;
    <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 %}