mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-30 04:37:06 +00:00
274 lines
12 KiB
HTML
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 }}
|