tenseleyflow/shithub / 134847d

Browse files

S10: settings shell — sidebar nav + two-column page layout

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
134847de50b72cd36ef6fb7ffe3bd4880350a29f
Parents
1b35f34
Tree
65873d5

10 changed files

StatusFile+-
M internal/web/handlers/auth/sshkeys.go 7 6
M internal/web/handlers/auth/tokens.go 1 0
M internal/web/handlers/auth/twofactor.go 19 14
M internal/web/static/css/shithub.css 96 0
A internal/web/templates/_settings_nav.html 48 0
M internal/web/templates/settings/2fa_disable.html 42 35
M internal/web/templates/settings/2fa_enable.html 25 20
M internal/web/templates/settings/2fa_recovery.html 30 23
M internal/web/templates/settings/keys.html 46 39
M internal/web/templates/settings/tokens.html 76 69
internal/web/handlers/auth/sshkeys.gomodified
@@ -34,12 +34,13 @@ func (h *Handlers) renderSSHKeysList(w http.ResponseWriter, r *http.Request, add
3434
 		return
3535
 	}
3636
 	h.renderPage(w, r, "settings/keys", map[string]any{
37
-		"Title":     "SSH keys",
38
-		"CSRFToken": middleware.CSRFTokenForRequest(r),
39
-		"Keys":      keys,
40
-		"AddError":  addError,
41
-		"AddTitle":  addTitle,
42
-		"AddBlob":   addBlob,
37
+		"Title":          "SSH keys",
38
+		"CSRFToken":      middleware.CSRFTokenForRequest(r),
39
+		"SettingsActive": "keys",
40
+		"Keys":           keys,
41
+		"AddError":       addError,
42
+		"AddTitle":       addTitle,
43
+		"AddBlob":        addBlob,
4344
 	})
4445
 }
4546
 
internal/web/handlers/auth/tokens.gomodified
@@ -42,6 +42,7 @@ func (h *Handlers) renderTokensList(
4242
 	h.renderPage(w, r, "settings/tokens", map[string]any{
4343
 		"Title":          "Personal access tokens",
4444
 		"CSRFToken":      middleware.CSRFTokenForRequest(r),
45
+		"SettingsActive": "tokens",
4546
 		"Tokens":         rows,
4647
 		"AllScopes":      pat.AllScopes,
4748
 		"CreateError":    createError,
internal/web/handlers/auth/twofactor.gomodified
@@ -181,10 +181,11 @@ func (h *Handlers) twoFactorEnableForm(w http.ResponseWriter, r *http.Request) {
181181
 	}
182182
 
183183
 	h.renderPage(w, r, "settings/2fa_enable", map[string]any{
184
-		"Title":     "Enable two-factor authentication",
185
-		"CSRFToken": middleware.CSRFTokenForRequest(r),
186
-		"QRSvg":     svg,
187
-		"Secret":    totp.EncodeBase32(secret), // displayed for manual entry; also high-entropy + redacted in logs
184
+		"Title":          "Enable two-factor authentication",
185
+		"CSRFToken":      middleware.CSRFTokenForRequest(r),
186
+		"SettingsActive": "2fa",
187
+		"QRSvg":          svg,
188
+		"Secret":         totp.EncodeBase32(secret), // displayed for manual entry; also high-entropy + redacted in logs
188189
 	})
189190
 }
190191
 
@@ -198,8 +199,9 @@ func (h *Handlers) twoFactorEnableSubmit(w http.ResponseWriter, r *http.Request)
198199
 
199200
 	render := func(msg string, recoveryCodes []string) {
200201
 		data := map[string]any{
201
-			"Title":     "Enable two-factor authentication",
202
-			"CSRFToken": middleware.CSRFTokenForRequest(r),
202
+			"Title":          "Enable two-factor authentication",
203
+			"CSRFToken":      middleware.CSRFTokenForRequest(r),
204
+			"SettingsActive": "2fa",
203205
 		}
204206
 		if msg != "" {
205207
 			data["Error"] = msg
@@ -304,8 +306,9 @@ func (h *Handlers) twoFactorEnableSubmit(w http.ResponseWriter, r *http.Request)
304306
 
305307
 func (h *Handlers) twoFactorDisableForm(w http.ResponseWriter, r *http.Request) {
306308
 	h.renderPage(w, r, "settings/2fa_disable", map[string]any{
307
-		"Title":     "Disable two-factor authentication",
308
-		"CSRFToken": middleware.CSRFTokenForRequest(r),
309
+		"Title":          "Disable two-factor authentication",
310
+		"CSRFToken":      middleware.CSRFTokenForRequest(r),
311
+		"SettingsActive": "2fa",
309312
 	})
310313
 }
311314
 
@@ -320,9 +323,10 @@ func (h *Handlers) twoFactorDisableSubmit(w http.ResponseWriter, r *http.Request
320323
 
321324
 	render := func(msg string) {
322325
 		h.renderPage(w, r, "settings/2fa_disable", map[string]any{
323
-			"Title":     "Disable two-factor authentication",
324
-			"CSRFToken": middleware.CSRFTokenForRequest(r),
325
-			"Error":     msg,
326
+			"Title":          "Disable two-factor authentication",
327
+			"CSRFToken":      middleware.CSRFTokenForRequest(r),
328
+			"SettingsActive": "2fa",
329
+			"Error":          msg,
326330
 		})
327331
 	}
328332
 
@@ -422,9 +426,10 @@ func (h *Handlers) twoFactorRegenerateSubmit(w http.ResponseWriter, r *http.Requ
422426
 	h.notifyUser(r.Context(), user.ID, "recovery_regenerated")
423427
 
424428
 	h.renderPage(w, r, "settings/2fa_recovery", map[string]any{
425
-		"Title":         "New recovery codes",
426
-		"CSRFToken":     middleware.CSRFTokenForRequest(r),
427
-		"RecoveryCodes": codes,
429
+		"Title":          "New recovery codes",
430
+		"CSRFToken":      middleware.CSRFTokenForRequest(r),
431
+		"SettingsActive": "2fa",
432
+		"RecoveryCodes":  codes,
428433
 	})
429434
 }
430435
 
internal/web/static/css/shithub.cssmodified
@@ -388,3 +388,99 @@ code {
388388
 .shithub-profile-pinned h2, .shithub-profile-contributions h2, .shithub-profile-readme h2 { font-size: 1.1rem; margin: 0 0 0.5rem; padding-bottom: 0.25rem; border-bottom: 1px solid var(--border-default); }
389389
 .shithub-empty { color: var(--fg-muted); font-style: italic; padding: 1rem; background: var(--canvas-subtle); border-radius: 6px; }
390390
 .shithub-profile-unavailable h1 { color: var(--fg-muted); }
391
+
392
+/* ----- settings shell (S10) ----- */
393
+.shithub-settings-page {
394
+  max-width: 64rem;
395
+  margin: 2rem auto;
396
+  padding: 0 1rem;
397
+  display: grid;
398
+  grid-template-columns: 220px 1fr;
399
+  gap: 2rem;
400
+  align-items: start;
401
+}
402
+.shithub-settings-side { font-size: 0.9rem; }
403
+.shithub-settings-side-title {
404
+  margin: 0 0 0.75rem;
405
+  font-size: 1.5rem;
406
+  font-weight: 400;
407
+  padding-bottom: 0.5rem;
408
+  border-bottom: 1px solid var(--border-default);
409
+}
410
+.shithub-settings-side nav ul {
411
+  list-style: none;
412
+  padding: 0;
413
+  margin: 0 0 1rem;
414
+}
415
+.shithub-settings-side nav li {
416
+  border-radius: 6px;
417
+}
418
+.shithub-settings-side nav li.active {
419
+  background: var(--canvas-subtle);
420
+  border-left: 2px solid var(--accent-emphasis);
421
+}
422
+.shithub-settings-side nav li a {
423
+  display: block;
424
+  padding: 0.4rem 0.75rem;
425
+  color: var(--fg-default);
426
+  text-decoration: none;
427
+  border-radius: 6px;
428
+}
429
+.shithub-settings-side nav li a:hover { background: var(--canvas-subtle); }
430
+.shithub-settings-side nav li.active a { font-weight: 500; }
431
+.shithub-settings-side-group {
432
+  margin: 1.25rem 0 0.5rem;
433
+  padding-top: 0.75rem;
434
+  border-top: 1px solid var(--border-default);
435
+  font-size: 0.75rem;
436
+  font-weight: 600;
437
+  text-transform: uppercase;
438
+  letter-spacing: 0.05em;
439
+  color: var(--fg-muted);
440
+}
441
+.shithub-settings-danger { color: #cf222e; }
442
+
443
+.shithub-settings-content { min-width: 0; }
444
+.shithub-settings-content > h1 {
445
+  margin: 0 0 1.5rem;
446
+  padding-bottom: 0.75rem;
447
+  border-bottom: 1px solid var(--border-default);
448
+  font-size: 1.5rem;
449
+  font-weight: 400;
450
+}
451
+.shithub-settings-section {
452
+  margin: 0 0 2rem;
453
+  padding-bottom: 1.5rem;
454
+  border-bottom: 1px solid var(--border-muted, var(--border-default));
455
+}
456
+.shithub-settings-section:last-child { border-bottom: none; }
457
+.shithub-settings-section h2 {
458
+  margin: 0 0 0.5rem;
459
+  font-size: 1rem;
460
+  font-weight: 600;
461
+}
462
+.shithub-settings-section p { margin: 0 0 1rem; color: var(--fg-muted); }
463
+.shithub-settings-section form { display: grid; gap: 0.85rem; max-width: 32rem; }
464
+.shithub-settings-section label { display: grid; gap: 0.25rem; font-weight: 500; font-size: 0.9rem; }
465
+.shithub-settings-section input[type=text],
466
+.shithub-settings-section input[type=email],
467
+.shithub-settings-section input[type=password],
468
+.shithub-settings-section input[type=url],
469
+.shithub-settings-section textarea,
470
+.shithub-settings-section select {
471
+  font: inherit;
472
+  padding: 0.5rem 0.75rem;
473
+  border: 1px solid var(--border-default);
474
+  border-radius: 6px;
475
+  background: var(--canvas-subtle);
476
+}
477
+.shithub-settings-section textarea { min-height: 4rem; resize: vertical; }
478
+.shithub-settings-section .shithub-button { justify-self: start; }
479
+
480
+.shithub-settings-danger-zone {
481
+  border: 1px solid rgba(207, 34, 46, 0.4);
482
+  border-radius: 6px;
483
+  padding: 1rem 1.25rem;
484
+  background: rgba(207, 34, 46, 0.04);
485
+}
486
+.shithub-settings-danger-zone h2 { color: #cf222e; }
internal/web/templates/_settings_nav.htmladded
@@ -0,0 +1,48 @@
1
+{{ define "settings-nav" -}}
2
+<aside class="shithub-settings-side" aria-label="Settings sections">
3
+  <h2 class="shithub-settings-side-title">Settings</h2>
4
+  <nav>
5
+    <ul>
6
+      <li{{ if eq .SettingsActive "profile" }} class="active"{{ end }}>
7
+        <a href="/settings/profile">Public profile</a>
8
+      </li>
9
+      <li{{ if eq .SettingsActive "account" }} class="active"{{ end }}>
10
+        <a href="/settings/account">Account</a>
11
+      </li>
12
+      <li{{ if eq .SettingsActive "appearance" }} class="active"{{ end }}>
13
+        <a href="/settings/appearance">Appearance</a>
14
+      </li>
15
+      <li{{ if eq .SettingsActive "notifications" }} class="active"{{ end }}>
16
+        <a href="/settings/notifications">Notifications</a>
17
+      </li>
18
+    </ul>
19
+    <h3 class="shithub-settings-side-group">Access</h3>
20
+    <ul>
21
+      <li{{ if eq .SettingsActive "emails" }} class="active"{{ end }}>
22
+        <a href="/settings/emails">Emails</a>
23
+      </li>
24
+      <li{{ if eq .SettingsActive "password" }} class="active"{{ end }}>
25
+        <a href="/settings/password">Password</a>
26
+      </li>
27
+      <li{{ if eq .SettingsActive "2fa" }} class="active"{{ end }}>
28
+        <a href="/settings/security/2fa/enable">Two-factor auth</a>
29
+      </li>
30
+      <li{{ if eq .SettingsActive "keys" }} class="active"{{ end }}>
31
+        <a href="/settings/keys">SSH keys</a>
32
+      </li>
33
+      <li{{ if eq .SettingsActive "tokens" }} class="active"{{ end }}>
34
+        <a href="/settings/tokens">Personal access tokens</a>
35
+      </li>
36
+      <li{{ if eq .SettingsActive "sessions" }} class="active"{{ end }}>
37
+        <a href="/settings/sessions">Sessions</a>
38
+      </li>
39
+    </ul>
40
+    <h3 class="shithub-settings-side-group">Danger</h3>
41
+    <ul>
42
+      <li{{ if eq .SettingsActive "danger" }} class="active"{{ end }}>
43
+        <a href="/settings/danger" class="shithub-settings-danger">Delete account</a>
44
+      </li>
45
+    </ul>
46
+  </nav>
47
+</aside>
48
+{{- end }}
internal/web/templates/settings/2fa_disable.htmlmodified
@@ -1,37 +1,44 @@
11
 {{ define "page" -}}
2
-<section class="shithub-auth">
3
-  <h1>Disable two-factor authentication</h1>
4
-  <p>To disable 2FA, confirm your password and a current authenticator code. This protects your account if your session is hijacked.</p>
5
-  {{ with .Error }}<p class="shithub-flash shithub-flash-error" role="alert">{{ . }}</p>{{ end }}
6
-  <form method="POST" action="/settings/security/2fa/disable" novalidate>
7
-    <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
8
-    <label>
9
-      <span>Password</span>
10
-      <input type="password" name="password" required autocomplete="current-password">
11
-    </label>
12
-    <label>
13
-      <span>Authenticator code</span>
14
-      <input type="text" name="code" required inputmode="numeric"
15
-             pattern="[0-9]{6}" minlength="6" maxlength="6" autocomplete="one-time-code">
16
-    </label>
17
-    <button type="submit" class="shithub-button shithub-button-danger">Disable 2FA</button>
18
-  </form>
19
-  <hr>
20
-  <details>
21
-    <summary>Regenerate recovery codes instead</summary>
22
-    <form method="POST" action="/settings/security/2fa/regenerate" novalidate>
23
-      <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
24
-      <label>
25
-        <span>Password</span>
26
-        <input type="password" name="password" required autocomplete="current-password">
27
-      </label>
28
-      <label>
29
-        <span>Authenticator code</span>
30
-        <input type="text" name="code" required inputmode="numeric"
31
-               pattern="[0-9]{6}" minlength="6" maxlength="6" autocomplete="one-time-code">
32
-      </label>
33
-      <button type="submit" class="shithub-button">Regenerate recovery codes</button>
34
-    </form>
35
-  </details>
36
-</section>
2
+<div class="shithub-settings-page">
3
+  {{ template "settings-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>Two-factor authentication</h1>
6
+    <section class="shithub-settings-section">
7
+      <h2>Disable 2FA</h2>
8
+      <p>To disable 2FA, confirm your password and a current authenticator code. This protects your account if your session is hijacked.</p>
9
+      {{ with .Error }}<p class="shithub-flash shithub-flash-error" role="alert">{{ . }}</p>{{ end }}
10
+      <form method="POST" action="/settings/security/2fa/disable" novalidate>
11
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
12
+        <label>
13
+          <span>Password</span>
14
+          <input type="password" name="password" required autocomplete="current-password">
15
+        </label>
16
+        <label>
17
+          <span>Authenticator code</span>
18
+          <input type="text" name="code" required inputmode="numeric"
19
+                 pattern="[0-9]{6}" minlength="6" maxlength="6" autocomplete="one-time-code">
20
+        </label>
21
+        <button type="submit" class="shithub-button shithub-button-danger">Disable 2FA</button>
22
+      </form>
23
+    </section>
24
+
25
+    <section class="shithub-settings-section">
26
+      <h2>Regenerate recovery codes</h2>
27
+      <p>Issue a fresh set of recovery codes. The old set stops working immediately.</p>
28
+      <form method="POST" action="/settings/security/2fa/regenerate" novalidate>
29
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
30
+        <label>
31
+          <span>Password</span>
32
+          <input type="password" name="password" required autocomplete="current-password">
33
+        </label>
34
+        <label>
35
+          <span>Authenticator code</span>
36
+          <input type="text" name="code" required inputmode="numeric"
37
+                 pattern="[0-9]{6}" minlength="6" maxlength="6" autocomplete="one-time-code">
38
+        </label>
39
+        <button type="submit" class="shithub-button">Regenerate recovery codes</button>
40
+      </form>
41
+    </section>
42
+  </div>
43
+</div>
3744
 {{- end }}
internal/web/templates/settings/2fa_enable.htmlmodified
@@ -1,23 +1,28 @@
11
 {{ define "page" -}}
2
-<section class="shithub-auth shithub-auth-wide">
3
-  <h1>Enable two-factor authentication</h1>
4
-  <ol class="shithub-2fa-steps">
5
-    <li>Install an authenticator app (Aegis, 1Password, Authy, Google Authenticator, …).</li>
6
-    <li>Scan this QR code, or paste the secret manually.</li>
7
-    <li>Enter the 6-digit code your app shows to confirm.</li>
8
-  </ol>
9
-  <div class="shithub-2fa-qr" aria-hidden="false">
10
-    {{ .QRSvg }}
2
+<div class="shithub-settings-page">
3
+  {{ template "settings-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>Enable two-factor authentication</h1>
6
+    <section class="shithub-settings-section">
7
+      <ol class="shithub-2fa-steps">
8
+        <li>Install an authenticator app (Aegis, 1Password, Authy, Google Authenticator, …).</li>
9
+        <li>Scan this QR code, or paste the secret manually.</li>
10
+        <li>Enter the 6-digit code your app shows to confirm.</li>
11
+      </ol>
12
+      <div class="shithub-2fa-qr" aria-hidden="false">
13
+        {{ .QRSvg }}
14
+      </div>
15
+      <p class="shithub-2fa-secret"><code>{{ .Secret }}</code></p>
16
+      <form method="POST" action="/settings/security/2fa/enable" novalidate>
17
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
18
+        <label>
19
+          <span>6-digit code from your app</span>
20
+          <input type="text" name="code" required autocomplete="one-time-code"
21
+                 inputmode="numeric" pattern="[0-9]{6}" minlength="6" maxlength="6">
22
+        </label>
23
+        <button type="submit" class="shithub-button shithub-button-primary">Confirm</button>
24
+      </form>
25
+    </section>
1126
   </div>
12
-  <p class="shithub-2fa-secret"><code>{{ .Secret }}</code></p>
13
-  <form method="POST" action="/settings/security/2fa/enable" novalidate>
14
-    <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
15
-    <label>
16
-      <span>6-digit code from your app</span>
17
-      <input type="text" name="code" required autocomplete="one-time-code"
18
-             inputmode="numeric" pattern="[0-9]{6}" minlength="6" maxlength="6">
19
-    </label>
20
-    <button type="submit" class="shithub-button shithub-button-primary">Confirm</button>
21
-  </form>
22
-</section>
27
+</div>
2328
 {{- end }}
internal/web/templates/settings/2fa_recovery.htmlmodified
@@ -1,25 +1,32 @@
11
 {{ define "page" -}}
2
-<section class="shithub-auth shithub-auth-wide">
3
-  {{ if .RecoveryCodes }}
4
-  <h1>Save your recovery codes</h1>
5
-  <p>These codes are shown <strong>once</strong>. Each works one time, in place of your authenticator code, if you lose access to your device. Store them in a password manager or print them.</p>
6
-  <ul class="shithub-recovery-codes" aria-label="Recovery codes">
7
-    {{ range .RecoveryCodes }}<li><code>{{ . }}</code></li>{{ end }}
8
-  </ul>
9
-  <p><a href="/settings/security/2fa/disable" class="shithub-button">Done — return to security settings</a></p>
10
-  {{ else }}
11
-  <h1>Two-factor authentication</h1>
12
-  {{ with .Error }}<p class="shithub-flash shithub-flash-error" role="alert">{{ . }}</p>{{ end }}
13
-  <p>Enter the 6-digit code from your authenticator app to finish enabling 2FA.</p>
14
-  <form method="POST" action="/settings/security/2fa/enable" novalidate>
15
-    <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
16
-    <label>
17
-      <span>Code</span>
18
-      <input type="text" name="code" required inputmode="numeric"
19
-             pattern="[0-9]{6}" minlength="6" maxlength="6">
20
-    </label>
21
-    <button type="submit" class="shithub-button shithub-button-primary">Confirm</button>
22
-  </form>
23
-  {{ end }}
24
-</section>
2
+<div class="shithub-settings-page">
3
+  {{ template "settings-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    {{ if .RecoveryCodes }}
6
+    <h1>Save your recovery codes</h1>
7
+    <section class="shithub-settings-section">
8
+      <p>These codes are shown <strong>once</strong>. Each works one time, in place of your authenticator code, if you lose access to your device. Store them in a password manager or print them.</p>
9
+      <ul class="shithub-recovery-codes" aria-label="Recovery codes">
10
+        {{ range .RecoveryCodes }}<li><code>{{ . }}</code></li>{{ end }}
11
+      </ul>
12
+      <p><a href="/settings/security/2fa/disable" class="shithub-button">Done — return to security settings</a></p>
13
+    </section>
14
+    {{ else }}
15
+    <h1>Two-factor authentication</h1>
16
+    <section class="shithub-settings-section">
17
+      {{ with .Error }}<p class="shithub-flash shithub-flash-error" role="alert">{{ . }}</p>{{ end }}
18
+      <p>Enter the 6-digit code from your authenticator app to finish enabling 2FA.</p>
19
+      <form method="POST" action="/settings/security/2fa/enable" novalidate>
20
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
21
+        <label>
22
+          <span>Code</span>
23
+          <input type="text" name="code" required inputmode="numeric"
24
+                 pattern="[0-9]{6}" minlength="6" maxlength="6">
25
+        </label>
26
+        <button type="submit" class="shithub-button shithub-button-primary">Confirm</button>
27
+      </form>
28
+    </section>
29
+    {{ end }}
30
+  </div>
31
+</div>
2532
 {{- end }}
internal/web/templates/settings/keys.htmlmodified
@@ -1,43 +1,50 @@
11
 {{ define "page" -}}
2
-<section class="shithub-auth shithub-auth-wide">
3
-  <h1>SSH keys</h1>
4
-  <p>SSH keys let you push and clone over SSH without typing a password. Generate one with <code>ssh-keygen -t ed25519</code> and paste the contents of the <code>.pub</code> file below.</p>
2
+<div class="shithub-settings-page">
3
+  {{ template "settings-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>SSH keys</h1>
6
+    <section class="shithub-settings-section">
7
+      <p>SSH keys let you push and clone over SSH without typing a password. Generate one with <code>ssh-keygen -t ed25519</code> and paste the contents of the <code>.pub</code> file below.</p>
58
 
6
-  {{ if .Keys }}
7
-  <ul class="shithub-key-list">
8
-    {{ range .Keys }}
9
-    <li class="shithub-key-row">
10
-      <div>
11
-        <strong>{{ .Title }}</strong>
12
-        <span class="shithub-key-meta">{{ .KeyType }}{{ if .KeyBits }} · {{ .KeyBits }} bits{{ end }}</span>
13
-        <code class="shithub-key-fp">{{ .FingerprintSha256 }}</code>
14
-        {{ if .LastUsedAt.Valid }}<span class="shithub-key-last">last used {{ .LastUsedAt.Time.Format "2006-01-02" }}</span>{{ end }}
15
-      </div>
16
-      <form method="POST" action="/settings/keys/{{ .ID }}/delete" novalidate>
17
-        <input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
18
-        <button type="submit" class="shithub-button shithub-button-danger">Delete</button>
19
-      </form>
20
-    </li>
21
-    {{ end }}
22
-  </ul>
23
-  {{ else }}
24
-  <p class="shithub-key-empty">No SSH keys yet.</p>
25
-  {{ end }}
9
+      {{ if .Keys }}
10
+      <ul class="shithub-key-list">
11
+        {{ range .Keys }}
12
+        <li class="shithub-key-row">
13
+          <div>
14
+            <strong>{{ .Title }}</strong>
15
+            <span class="shithub-key-meta">{{ .KeyType }}{{ if .KeyBits }} · {{ .KeyBits }} bits{{ end }}</span>
16
+            <code class="shithub-key-fp">{{ .FingerprintSha256 }}</code>
17
+            {{ if .LastUsedAt.Valid }}<span class="shithub-key-last">last used {{ .LastUsedAt.Time.Format "2006-01-02" }}</span>{{ end }}
18
+          </div>
19
+          <form method="POST" action="/settings/keys/{{ .ID }}/delete" novalidate>
20
+            <input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
21
+            <button type="submit" class="shithub-button shithub-button-danger">Delete</button>
22
+          </form>
23
+        </li>
24
+        {{ end }}
25
+      </ul>
26
+      {{ else }}
27
+      <p class="shithub-key-empty">No SSH keys yet.</p>
28
+      {{ end }}
29
+    </section>
2630
 
27
-  <h2>Add a key</h2>
28
-  {{ with .AddError }}<p class="shithub-flash shithub-flash-error" role="alert">{{ . }}</p>{{ end }}
29
-  <form method="POST" action="/settings/keys" novalidate>
30
-    <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
31
-    <label>
32
-      <span>Title</span>
33
-      <input type="text" name="title" maxlength="80" required value="{{ .AddTitle }}">
34
-    </label>
35
-    <label>
36
-      <span>Public key</span>
37
-      <textarea name="public_key" rows="4" required spellcheck="false"
38
-                placeholder="ssh-ed25519 AAAA...">{{ .AddBlob }}</textarea>
39
-    </label>
40
-    <button type="submit" class="shithub-button shithub-button-primary">Add SSH key</button>
41
-  </form>
42
-</section>
31
+    <section class="shithub-settings-section">
32
+      <h2>Add a key</h2>
33
+      {{ with .AddError }}<p class="shithub-flash shithub-flash-error" role="alert">{{ . }}</p>{{ end }}
34
+      <form method="POST" action="/settings/keys" novalidate>
35
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
36
+        <label>
37
+          <span>Title</span>
38
+          <input type="text" name="title" maxlength="80" required value="{{ .AddTitle }}">
39
+        </label>
40
+        <label>
41
+          <span>Public key</span>
42
+          <textarea name="public_key" rows="4" required spellcheck="false"
43
+                    placeholder="ssh-ed25519 AAAA...">{{ .AddBlob }}</textarea>
44
+        </label>
45
+        <button type="submit" class="shithub-button shithub-button-primary">Add SSH key</button>
46
+      </form>
47
+    </section>
48
+  </div>
49
+</div>
4350
 {{- end }}
internal/web/templates/settings/tokens.htmlmodified
@@ -1,76 +1,83 @@
11
 {{ define "page" -}}
2
-<section class="shithub-auth shithub-auth-wide">
3
-  <h1>Personal access tokens</h1>
4
-  <p>Tokens authenticate API calls and git-over-HTTPS pushes. Use them with <code>Authorization: token &lt;value&gt;</code> or as the password in HTTP Basic.</p>
2
+<div class="shithub-settings-page">
3
+  {{ template "settings-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>Personal access tokens</h1>
6
+    <section class="shithub-settings-section">
7
+      <p>Tokens authenticate API calls and git-over-HTTPS pushes. Use them with <code>Authorization: token &lt;value&gt;</code> or as the password in HTTP Basic.</p>
58
 
6
-  {{ if .JustCreatedRaw }}
7
-  <div class="shithub-token-created" role="alert">
8
-    <strong>Save this token now — it won't be shown again.</strong>
9
-    <pre><code>{{ .JustCreatedRaw }}</code></pre>
10
-  </div>
11
-  {{ end }}
12
-
13
-  {{ if .Tokens }}
14
-  <ul class="shithub-token-list">
15
-    {{ range .Tokens }}
16
-    <li class="shithub-token-row">
17
-      <div>
18
-        <strong>{{ .Name }}</strong>
19
-        <span class="shithub-key-meta">
20
-          {{ if .RevokedAt.Valid }}revoked{{ else if and .ExpiresAt.Valid (gt .ExpiresAt.Time.Unix 0) }}expires {{ .ExpiresAt.Time.Format "2006-01-02" }}{{ else }}no expiry{{ end }}
21
-        </span>
22
-        <code class="shithub-key-fp">{{ .TokenPrefix }}…</code>
23
-        <div class="shithub-token-scopes">{{ range .Scopes }}<span>{{ . }}</span>{{ end }}</div>
24
-        {{ if .LastUsedAt.Valid }}<span class="shithub-key-last">last used {{ .LastUsedAt.Time.Format "2006-01-02 15:04" }}</span>{{ end }}
9
+      {{ if .JustCreatedRaw }}
10
+      <div class="shithub-token-created" role="alert">
11
+        <strong>Save this token now — it won't be shown again.</strong>
12
+        <pre><code>{{ .JustCreatedRaw }}</code></pre>
2513
       </div>
26
-      {{ if not .RevokedAt.Valid }}
27
-      <form method="POST" action="/settings/tokens/{{ .ID }}/revoke" novalidate>
28
-        <input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
29
-        <button type="submit" class="shithub-button shithub-button-danger">Revoke</button>
30
-      </form>
3114
       {{ end }}
32
-    </li>
33
-    {{ end }}
34
-  </ul>
35
-  {{ else }}
36
-  <p class="shithub-key-empty">No tokens yet.</p>
37
-  {{ end }}
3815
 
39
-  <h2>Create a token</h2>
40
-  {{ if not .RecentAuthOK }}
41
-    <p class="shithub-flash shithub-flash-error" role="alert">
42
-      Confirm 2FA before creating a token. <a href="/login">Sign in again</a> with your authenticator code.
43
-    </p>
44
-  {{ end }}
45
-  {{ with .CreateError }}<p class="shithub-flash shithub-flash-error" role="alert">{{ . }}</p>{{ end }}
46
-  <form method="POST" action="/settings/tokens" novalidate>
47
-    <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
48
-    <label>
49
-      <span>Name</span>
50
-      <input type="text" name="name" maxlength="80" required value="{{ .CreateName }}" placeholder="ci runner, laptop, …">
51
-    </label>
52
-    <label>
53
-      <span>Expiry</span>
54
-      <select name="expires_in">
55
-        <option value="90">90 days (recommended)</option>
56
-        <option value="30">30 days</option>
57
-        <option value="365">1 year</option>
58
-        <option value="">No expiry</option>
59
-      </select>
60
-    </label>
61
-    <fieldset class="shithub-token-scopes-pick">
62
-      <legend>Scopes</legend>
63
-      {{ $picked := .CreateScopes }}
64
-      {{ range .AllScopes }}
65
-      {{ $s := . }}
66
-      <label>
67
-        <input type="checkbox" name="scopes" value="{{ . }}"
68
-          {{ range $picked }}{{ if eq . (printf "%s" $s) }}checked{{ end }}{{ end }}>
69
-        <code>{{ . }}</code>
70
-      </label>
16
+      {{ if .Tokens }}
17
+      <ul class="shithub-token-list">
18
+        {{ range .Tokens }}
19
+        <li class="shithub-token-row">
20
+          <div>
21
+            <strong>{{ .Name }}</strong>
22
+            <span class="shithub-key-meta">
23
+              {{ if .RevokedAt.Valid }}revoked{{ else if and .ExpiresAt.Valid (gt .ExpiresAt.Time.Unix 0) }}expires {{ .ExpiresAt.Time.Format "2006-01-02" }}{{ else }}no expiry{{ end }}
24
+            </span>
25
+            <code class="shithub-key-fp">{{ .TokenPrefix }}…</code>
26
+            <div class="shithub-token-scopes">{{ range .Scopes }}<span>{{ . }}</span>{{ end }}</div>
27
+            {{ if .LastUsedAt.Valid }}<span class="shithub-key-last">last used {{ .LastUsedAt.Time.Format "2006-01-02 15:04" }}</span>{{ end }}
28
+          </div>
29
+          {{ if not .RevokedAt.Valid }}
30
+          <form method="POST" action="/settings/tokens/{{ .ID }}/revoke" novalidate>
31
+            <input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
32
+            <button type="submit" class="shithub-button shithub-button-danger">Revoke</button>
33
+          </form>
34
+          {{ end }}
35
+        </li>
36
+        {{ end }}
37
+      </ul>
38
+      {{ else }}
39
+      <p class="shithub-key-empty">No tokens yet.</p>
7140
       {{ end }}
72
-    </fieldset>
73
-    <button type="submit" class="shithub-button shithub-button-primary">Create token</button>
74
-  </form>
75
-</section>
41
+    </section>
42
+
43
+    <section class="shithub-settings-section">
44
+      <h2>Create a token</h2>
45
+      {{ if not .RecentAuthOK }}
46
+        <p class="shithub-flash shithub-flash-error" role="alert">
47
+          Confirm 2FA before creating a token. <a href="/login">Sign in again</a> with your authenticator code.
48
+        </p>
49
+      {{ end }}
50
+      {{ with .CreateError }}<p class="shithub-flash shithub-flash-error" role="alert">{{ . }}</p>{{ end }}
51
+      <form method="POST" action="/settings/tokens" novalidate>
52
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
53
+        <label>
54
+          <span>Name</span>
55
+          <input type="text" name="name" maxlength="80" required value="{{ .CreateName }}" placeholder="ci runner, laptop, …">
56
+        </label>
57
+        <label>
58
+          <span>Expiry</span>
59
+          <select name="expires_in">
60
+            <option value="90">90 days (recommended)</option>
61
+            <option value="30">30 days</option>
62
+            <option value="365">1 year</option>
63
+            <option value="">No expiry</option>
64
+          </select>
65
+        </label>
66
+        <fieldset class="shithub-token-scopes-pick">
67
+          <legend>Scopes</legend>
68
+          {{ $picked := .CreateScopes }}
69
+          {{ range .AllScopes }}
70
+          {{ $s := . }}
71
+          <label>
72
+            <input type="checkbox" name="scopes" value="{{ . }}"
73
+              {{ range $picked }}{{ if eq . (printf "%s" $s) }}checked{{ end }}{{ end }}>
74
+            <code>{{ . }}</code>
75
+          </label>
76
+          {{ end }}
77
+        </fieldset>
78
+        <button type="submit" class="shithub-button shithub-button-primary">Create token</button>
79
+      </form>
80
+    </section>
81
+  </div>
82
+</div>
7683
 {{- end }}