server auth

This commit is contained in:
2026-05-28 08:35:50 -04:00
parent 3d44a392f1
commit fc63f9c44a
37 changed files with 3877 additions and 74 deletions

View File

@@ -0,0 +1,72 @@
{{ define "account.gohtml" }}{{ template "base" . }}{{ end }}
{{ define "account_content" }}
<section class="account-shell">
<header class="account-header">
<div>
<p class="eyebrow">Account</p>
<h1>{{ .DisplayName }}</h1>
<p>{{ .Email }} &middot; {{ .Role }}</p>
</div>
<button type="button" data-auth-logout>Sign out</button>
</header>
<div class="account-grid">
<section class="account-section">
<h2>API Tokens</h2>
<form class="auth-form" data-token-create>
<label>
Name
<input type="text" name="name" placeholder="CLI on laptop" required />
</label>
<fieldset class="scope-grid">
<legend>Scopes</legend>
<label><input type="checkbox" name="scope" value="docs:read" checked /> Docs read</label>
<label><input type="checkbox" name="scope" value="docs:write" /> Docs write</label>
<label><input type="checkbox" name="scope" value="sync:read" checked /> Sync read</label>
<label><input type="checkbox" name="scope" value="sync:write" /> Sync write</label>
<label><input type="checkbox" name="scope" value="admin" /> Admin</label>
</fieldset>
<button type="submit">Create token</button>
<p class="form-status" data-token-status></p>
<pre class="token-output" data-token-output hidden></pre>
</form>
<ul class="token-list" data-token-list></ul>
</section>
<section class="account-section">
<h2>Password</h2>
<form class="auth-form" data-password-change>
<label>
Current password
<input type="password" name="currentPassword" autocomplete="current-password" />
</label>
<label>
New password
<input type="password" name="newPassword" autocomplete="new-password" minlength="12" required />
</label>
<button type="submit">Change password</button>
<p class="form-status" data-password-status></p>
</form>
</section>
<section class="account-section" data-admin-users-section hidden>
<h2>Users</h2>
<div class="user-list" data-admin-users></div>
<p class="form-status" data-admin-users-status></p>
</section>
<section class="account-section account-section--danger">
<h2>Delete Account</h2>
<form class="auth-form" data-account-delete>
<label>
Current password
<input type="password" name="currentPassword" autocomplete="current-password" />
</label>
<button type="submit">Delete account</button>
<p class="form-status" data-delete-status></p>
</form>
</section>
</div>
</section>
{{ end }}

View File

@@ -19,7 +19,7 @@
<header class="site-header">
<div class="site-header__inner">
<a class="site-brand" href="/" aria-label="Cairnquire home">
<img src="/static/cairnquire%20logo%402x.webp" alt="" width="160" height="40" decoding="async" />
<img src="/static/favicon.png" alt="" width="64" height="64" decoding="async" />
<span class="sr-only">Cairnquire</span>
</a>
<form class="site-search" action="/" method="get">
@@ -28,6 +28,7 @@
</form>
<nav class="site-nav">
<a href="/">Docs</a>
<a href="/account">Account</a>
{{ if .WebEnabled }}<a href="/app/">Admin</a>{{ end }}
</nav>
</div>
@@ -41,6 +42,12 @@
{{ template "document_edit_content" .Data }}
{{ else if eq .BodyTemplate "search_content" }}
{{ template "search_content" .Data }}
{{ else if eq .BodyTemplate "login_content" }}
{{ template "login_content" .Data }}
{{ else if eq .BodyTemplate "account_content" }}
{{ template "account_content" .Data }}
{{ else if eq .BodyTemplate "device_verify_content" }}
{{ template "device_verify_content" .Data }}
{{ else if eq .BodyTemplate "error_content" }}
{{ template "error_content" .Data }}
{{ end }}
@@ -53,6 +60,16 @@
<span aria-hidden="true" class="cached-icon"></span>
<p>Viewing cached offline content.</p>
</div>
<section class="sync-queue" data-sync-queue hidden aria-live="polite">
<div class="sync-queue__header">
<div>
<strong data-sync-queue-title>Pending changes</strong>
<p data-sync-queue-summary>Waiting to sync.</p>
</div>
<button type="button" data-sync-now>Sync now</button>
</div>
<ul data-sync-queue-list></ul>
</section>
<div class="version-notice" data-version-notice hidden>
<p>A newer version is available.</p>
<button type="button" data-version-reload>Reload</button>
@@ -61,6 +78,7 @@
<script src="/static/sync.js" defer></script>
<script src="/static/realtime.js" defer></script>
<script src="/static/editor.js" defer></script>
<script src="/static/auth.js" defer></script>
<script src="/static/render.js" defer></script>
</body>

View File

@@ -0,0 +1,21 @@
{{ define "device_verify.gohtml" }}{{ template "base" . }}{{ end }}
{{ define "device_verify_content" }}
<section class="auth-shell">
<div class="auth-panel">
<div class="auth-panel__header">
<p class="eyebrow">Device authorization</p>
<h1>Approve device</h1>
</div>
<form class="auth-form" data-device-approve>
<label>
User code
<input type="text" name="userCode" value="{{ .UserCode }}" autocomplete="one-time-code" required />
</label>
<button type="submit">Approve</button>
<p class="form-status" data-device-status></p>
</form>
<p class="auth-note">You must be signed in with a browser session before approving a device.</p>
</div>
</section>
{{ end }}

View File

@@ -0,0 +1,76 @@
{{ define "login.gohtml" }}{{ template "base" . }}{{ end }}
{{ define "login_content" }}
<section class="auth-shell">
<input type="hidden" data-auth-next value="{{ .Next }}" />
<div class="auth-panel">
<div class="auth-panel__header">
<p class="eyebrow">Cairnquire</p>
<h1>Sign in</h1>
</div>
<div class="auth-grid">
<form class="auth-form" data-auth-login>
<h2>Password</h2>
<label>
Email
<input type="email" name="email" autocomplete="email" required />
</label>
<label>
Password
<input type="password" name="password" autocomplete="current-password" required />
</label>
<button type="submit">Sign in</button>
<p class="form-status" data-login-status></p>
</form>
<form class="auth-form" data-passkey-login>
<h2>Passkey</h2>
<label>
Email
<input type="email" name="email" autocomplete="username webauthn" required />
</label>
<button type="submit">Use passkey</button>
<p class="form-status" data-passkey-login-status></p>
</form>
</div>
<details class="auth-details">
<summary>Create an account</summary>
<div class="auth-grid">
<form class="auth-form" data-auth-register>
<h2>Password account</h2>
<label>
Email
<input type="email" name="email" autocomplete="email" required />
</label>
<label>
Display name
<input type="text" name="displayName" autocomplete="name" />
</label>
<label>
Password
<input type="password" name="password" autocomplete="new-password" minlength="12" required />
</label>
<button type="submit">Register</button>
<p class="form-status" data-register-status></p>
</form>
<form class="auth-form" data-passkey-register>
<h2>Passkey account</h2>
<label>
Email
<input type="email" name="email" autocomplete="email" required />
</label>
<label>
Display name
<input type="text" name="displayName" autocomplete="name" />
</label>
<button type="submit">Create passkey</button>
<p class="form-status" data-passkey-register-status></p>
</form>
</div>
</details>
</div>
</section>
{{ end }}