109 lines
4.5 KiB
Django/Jinja
109 lines
4.5 KiB
Django/Jinja
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="robots" content="noindex">
|
|
<title>{{ title }}</title>
|
|
<style>
|
|
body { font-family: system-ui, sans-serif; background: #f4f5f7; color: #1f2937; margin: 0; }
|
|
main { max-width: 22rem; margin: 4rem auto; background: #fff; padding: 2rem; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); }
|
|
h1 { font-size: 1.25rem; margin: 0 0 0.25rem; }
|
|
p { margin: 0 0 1rem; color: #4b5563; }
|
|
label { display: block; font-size: 0.875rem; margin: 0.75rem 0 0.25rem; }
|
|
input { box-sizing: border-box; width: 100%; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.25rem; font-size: 1rem; }
|
|
button { width: 100%; margin-top: 1.25rem; padding: 0.6rem; border: 0; border-radius: 0.25rem; font-size: 1rem; cursor: pointer; background: #1d4ed8; color: #fff; }
|
|
button.secondary { background: #fff; color: #1d4ed8; border: 1px solid #1d4ed8; margin-top: 0.75rem; }
|
|
.error { background: #fee2e2; color: #991b1b; padding: 0.5rem 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem; }
|
|
ul { margin: 0 0 1rem; padding-left: 1.25rem; color: #4b5563; }
|
|
#user_code { text-transform: uppercase; letter-spacing: 0.15em; font-family: ui-monospace, monospace; }
|
|
.issuer { font-size: 0.75rem; color: #6b7280; margin: 1.5rem 0 0; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>{{ title }}</h1>
|
|
{%- if message %}
|
|
<p>{{ message }}</p>
|
|
{%- endif %}
|
|
{%- if error %}
|
|
<div class="error" role="alert">{{ error }}</div>
|
|
{%- endif %}
|
|
{%- if items %}
|
|
<ul>
|
|
{%- for item in items %}
|
|
<li>{{ item }}</li>
|
|
{%- endfor %}
|
|
</ul>
|
|
{%- endif %}
|
|
{%- if decide %}
|
|
<form method="post" action="/oidc/device/decide">
|
|
<input type="hidden" name="user_code" value="{{ user_code }}">
|
|
<button type="submit" name="decision" value="allow">Allow</button>
|
|
<button class="secondary" type="submit" name="decision" value="deny">Deny</button>
|
|
</form>
|
|
{%- endif %}
|
|
{%- if form or device %}
|
|
<form id="sign-in" method="post" action="{{ action }}">
|
|
{%- if device %}
|
|
<label for="user_code">Code shown on the device</label>
|
|
<input id="user_code" name="user_code" value="{{ user_code }}" autocapitalize="characters" autocomplete="off" spellcheck="false" required{% if not form %} autofocus{% endif %}>
|
|
{%- else %}
|
|
<input type="hidden" name="request" value="{{ request }}">
|
|
{%- endif %}
|
|
{%- if form %}
|
|
<label for="name">Name</label>
|
|
<input id="name" name="name" value="{{ name }}" autocomplete="username webauthn" autocapitalize="none" spellcheck="false" required{% if not device %} autofocus{% endif %}>
|
|
<label for="password">Password</label>
|
|
<input id="password" name="password" type="password" autocomplete="current-password" required>
|
|
<label for="code">Code from the authenticator, when you have one</label>
|
|
<input id="code" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code">
|
|
<button type="submit">Sign in</button>
|
|
{%- else %}
|
|
<button type="submit">Continue</button>
|
|
{%- endif %}
|
|
</form>
|
|
{%- endif %}
|
|
{%- if form %}
|
|
<form id="passkey-form" method="post" action="{{ action }}" hidden>
|
|
<input type="hidden" name="request" value="{{ request }}">
|
|
<input type="hidden" name="user_code">
|
|
<input type="hidden" name="passkey_id">
|
|
<input type="hidden" name="credential">
|
|
</form>
|
|
<button id="passkey" class="secondary" type="button" hidden>Sign in with a passkey</button>
|
|
<script>
|
|
// A passkey sign-in begins at /oidc/passkey and posts the browser's answer to the form's action with the authorization
|
|
// request, or with the user code the sign-in form holds.
|
|
(function () {
|
|
var button = document.getElementById('passkey');
|
|
if (!window.PublicKeyCredential || !PublicKeyCredential.parseRequestOptionsFromJSON) {
|
|
return;
|
|
}
|
|
button.hidden = false;
|
|
button.addEventListener('click', async function () {
|
|
var code = document.getElementById('user_code');
|
|
if (code && !code.reportValidity()) {
|
|
return;
|
|
}
|
|
button.disabled = true;
|
|
try {
|
|
var begun = await (await fetch('/oidc/passkey', { method: 'POST' })).json();
|
|
var credential = await navigator.credentials.get({ publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(begun.options.publicKey) });
|
|
var form = document.getElementById('passkey-form');
|
|
form.elements.user_code.value = code ? code.value : '';
|
|
form.elements.passkey_id.value = begun.id;
|
|
form.elements.credential.value = JSON.stringify(credential.toJSON());
|
|
form.submit();
|
|
} catch (error) {
|
|
button.disabled = false;
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
{%- endif %}
|
|
<p class="issuer">{{ issuer }}</p>
|
|
</main>
|
|
</body>
|
|
</html>
|