Files

274 lines
12 KiB
HTML

{{/*
Device authorization templates.
Three states share a layout shell:
- device-approve : pending — show identity, device, code, actions
- device-approved : success
- device-error : invalid / expired
The visual idea on the approval page is to make the *pairing* obvious:
the signed-in account on one side, the requesting device on the other.
If the user is staring at someone else's avatar, the page should make
that mismatch impossible to miss before the Approve button is clicked.
*/}}
{{ define "device-approve" }}
<!DOCTYPE html>
<html lang="en">
<head>
{{ template "head" . }}
{{ template "meta" .Meta }}
</head>
<body>
{{ template "nav-simple" . }}
<main id="main-content" class="px-4 py-12 sm:py-16">
<div class="mx-auto w-full max-w-xl">
{{/* Eyebrow — small-caps bearing label */}}
<div class="flex items-center gap-2 text-xs font-medium uppercase tracking-[0.18em] text-base-content/50 mb-3">
{{ icon "compass" "size-4 text-primary" }}
<span>Device authorization</span>
</div>
<h1 class="font-display text-3xl sm:text-4xl font-semibold tracking-tight">Confirm the pairing</h1>
<p class="mt-3 text-base-content/70 leading-relaxed max-w-prose">
A device is requesting credentials for {{ .ClientShortName }}.
The pairing below will let it push and pull on your behalf, so make sure
both halves are correct before approving.
</p>
{{/* The pairing — identity on one side, device on the other.
On md+ they sit side-by-side with a connector between; on mobile
they stack with the connector rotated. */}}
<section aria-label="Pairing" class="mt-10 flex flex-col gap-3">
{{/* Identity card */}}
<article class="relative bg-base-200 rounded-lg p-6 sm:p-7
shadow-[inset_0_0_0_1px_oklch(100%_0_0/0.06)]
flex flex-col items-center text-center">
<span class="block text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-base-content/55 mb-4">
Account
</span>
<div class="device-mark shrink-0">
{{ if .User.Avatar }}
<img src="{{ resizeImage .User.Avatar 192 }}"
alt="" aria-hidden="true"
width="80" height="80"
class="block w-full h-full rounded-full object-cover" />
{{ else }}
<div class="w-full h-full rounded-full bg-secondary text-secondary-content
flex items-center justify-center font-display font-semibold text-2xl uppercase">
{{ firstChar .User.Handle }}
</div>
{{ end }}
</div>
<div class="font-display text-lg font-semibold leading-tight mt-4 max-w-full truncate" title="@{{ .User.Handle }}">
@{{ .User.Handle }}
</div>
{{ if .ProfileDisplayName }}
<div class="text-sm text-base-content/70 mt-0.5 max-w-full truncate" title="{{ .ProfileDisplayName }}">
{{ .ProfileDisplayName }}
</div>
{{ end }}
<div class="font-mono text-[0.7rem] text-base-content/45 mt-2 max-w-full truncate" title="{{ .User.DID }}">
{{ .UserDIDShort }}
</div>
</article>
{{/* Connector — horizontal divider through the verb */}}
<div class="flex items-center gap-3 px-2" aria-hidden="true">
<span class="bearing-line bearing-line-h"></span>
<span class="font-mono text-[0.65rem] tracking-[0.18em] uppercase text-base-content/55 whitespace-nowrap">
authorizes
</span>
<span class="bearing-line bearing-line-h"></span>
</div>
{{/* Device card */}}
<article class="relative bg-base-200 rounded-lg p-6 sm:p-7
shadow-[inset_0_0_0_1px_oklch(100%_0_0/0.06)]
flex flex-col items-center text-center">
<span class="block text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-base-content/55 mb-4">
Device
</span>
<div class="device-mark device-mark-square shrink-0
bg-base-300 text-primary
flex items-center justify-center">
{{ icon "terminal" "size-8" }}
</div>
<div class="font-display text-lg font-semibold leading-tight mt-4 max-w-full truncate" title="{{ .Pending.DeviceName }}">
{{ .Pending.DeviceName }}
</div>
<div class="font-mono text-sm text-base-content/70 mt-0.5 max-w-full truncate" title="{{ .Pending.IPAddress }}">
{{ .Pending.IPAddress }}
</div>
{{ if .UserAgentShort }}
<div class="text-[0.7rem] text-base-content/45 mt-2 max-w-full truncate" title="{{ .Pending.UserAgent }}">
{{ .UserAgentShort }}
</div>
{{ end }}
</article>
</section>
{{/* Verification code — instrument readout */}}
<section aria-label="Verification code" class="mt-10">
<div class="flex items-baseline justify-between gap-3 mb-2">
<span class="text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-base-content/55">
Verification code
</span>
<span class="text-xs text-base-content/55">
Should match your terminal
</span>
</div>
<div class="instrument-readout">
<span class="instrument-tick" aria-hidden="true"></span>
<code class="font-mono font-semibold text-2xl sm:text-3xl tracking-[0.4em] text-base-content tabular-nums">{{ .Pending.UserCode }}</code>
<span class="instrument-tick" aria-hidden="true"></span>
</div>
</section>
{{/* Actions */}}
<form id="device-approve-form" class="mt-10 flex flex-col-reverse sm:flex-row sm:justify-center gap-3">
<button type="button" class="btn btn-ghost order-2 sm:order-1"
data-action="deny" aria-label="Cancel and deny this device">
Cancel
</button>
<button type="submit" class="btn btn-primary order-1 sm:order-2"
data-action="approve" aria-label="Approve this device for {{ .User.Handle }}">
{{ icon "check" "size-4" }}
<span>Approve as @{{ .User.Handle }}</span>
</button>
</form>
{{/* Switch-account affordance — bearing-line separator above it */}}
<div class="mt-12 pt-6 bearing-line bearing-line-h-full text-sm text-base-content/65">
Not @{{ .User.Handle }}?
<a href="/auth/logout?return_to=/device?user_code={{ urlquery .Pending.UserCode }}"
class="link link-primary font-medium ml-1">
Switch accounts to authorize as someone else
</a>
</div>
<div id="device-error-banner" role="alert" class="alert alert-error mt-6 hidden">
{{ icon "circle-x" "size-5" }}
<span data-bind="message">Something went wrong. Please try again.</span>
</div>
</div>
</main>
{{ template "footer" . }}
<script>
(function() {
var form = document.getElementById('device-approve-form');
if (!form) return;
var banner = document.getElementById('device-error-banner');
var bannerMsg = banner ? banner.querySelector('[data-bind="message"]') : null;
var userCode = {{ .Pending.UserCode }};
function showError(msg) {
if (!banner) return;
if (bannerMsg && msg) bannerMsg.textContent = msg;
banner.classList.remove('hidden');
}
function setBusy(busy) {
var buttons = form.querySelectorAll('button');
buttons.forEach(function(b) { b.disabled = busy; });
}
function submit(approve) {
setBusy(true);
if (banner) banner.classList.add('hidden');
fetch('/device/approve', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ user_code: userCode, approve: approve })
}).then(function(resp) {
if (!resp.ok) throw new Error('approval_failed');
if (approve) {
window.location.href = '/device?user_code=' + encodeURIComponent(userCode);
} else {
window.location.href = '/';
}
}).catch(function() {
setBusy(false);
showError("We couldn't process that request. Please try again.");
});
}
form.addEventListener('submit', function(e) {
e.preventDefault();
submit(true);
});
var deny = form.querySelector('[data-action="deny"]');
if (deny) deny.addEventListener('click', function() { submit(false); });
})();
</script>
</body>
</html>
{{ end }}
{{ define "device-approved" }}
<!DOCTYPE html>
<html lang="en">
<head>
{{ template "head" . }}
{{ template "meta" .Meta }}
</head>
<body>
{{ template "nav-simple" . }}
<main id="main-content" class="px-4 py-16 sm:py-24">
<div class="mx-auto w-full max-w-xl text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/15 text-primary mb-6">
{{ icon "check-circle" "size-8" }}
</div>
<h1 class="font-display text-3xl sm:text-4xl font-semibold tracking-tight">Device authorized</h1>
<p class="mt-3 text-base-content/70 leading-relaxed">
<span class="font-mono text-base-content">{{ .DeviceName }}</span>
is now authorized. You can return to your terminal and close this tab.
</p>
<div class="mt-8 flex flex-wrap justify-center gap-3">
<a href="/settings/devices" class="btn btn-primary">Manage devices</a>
<a href="/" class="btn btn-ghost">Back to home</a>
</div>
</div>
</main>
{{ template "footer" . }}
</body>
</html>
{{ end }}
{{ define "device-error" }}
<!DOCTYPE html>
<html lang="en">
<head>
{{ template "head" . }}
{{ template "meta" .Meta }}
</head>
<body>
{{ template "nav-simple" . }}
<main id="main-content" class="px-4 py-16 sm:py-24">
<div class="mx-auto w-full max-w-xl text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-error/15 text-error mb-6">
{{ icon "alert-triangle" "size-8" }}
</div>
<h1 class="font-display text-3xl sm:text-4xl font-semibold tracking-tight">Authorization couldn't be completed</h1>
<p class="mt-3 text-base-content/70 leading-relaxed">{{ .Message }}</p>
<div class="mt-8 flex flex-wrap justify-center gap-3">
<a href="/" class="btn btn-primary">Back to home</a>
</div>
</div>
</main>
{{ template "footer" . }}
</body>
</html>
{{ end }}