feat(web): account page with disconnect + delete-account
GET /account shows the registered display name, the signed-in email, the
user's connected video accounts (status + when), a Connect-YouTube link
when none is connected, and the disconnect / delete controls. Linked from
the header nav.
POST /account/disconnect/{provider}: deletes the OAuth token from the
SecretStore (resolved from the connection's own token_ref, provider-
agnostic) and the connection row. Does NOT delete the account.
POST /account/delete: confirm-before-destroy (a <details> disclosure gates
the destructive submit — works without JS). Captures token refs, calls
store.DeleteUser (cascades all rows), purges every secret, then routes to
/auth/logout to clear the session. Tapir-side only — Dex is left untouched
(decision 2026-06-03).
Account handlers depend on a narrow SecretRemover (Delete) and the extended
Store port; cmd/tapir serve shares one file-backed SecretStore between the
connect flow and account management.
Tests: account page renders connections + name + Connect link; disconnect
removes token (fake records Delete) + row and keeps the account; delete
wipes users/summaries/connections/identities and purges the token, then
redirects to logout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -198,6 +198,70 @@ templ RegisterPage(email, errMsg string) {
|
||||
}
|
||||
}
|
||||
|
||||
// AccountPage is the account-management view: the registered display name and
|
||||
// signed-in email, the user's connected video accounts (each with a Disconnect
|
||||
// control), a Connect-YouTube link when none is connected, and the delete-account
|
||||
// danger zone. flash surfaces a one-shot notification (disconnect/connect).
|
||||
templ AccountPage(displayName, email string, conns []store.Connection, flash string) {
|
||||
@Layout("Tapir — Account") {
|
||||
@flashBanner(flash)
|
||||
<article class="account">
|
||||
<h1>Account</h1>
|
||||
<dl class="account-meta">
|
||||
<dt>Display name</dt>
|
||||
<dd>{ displayNameOr(displayName) }</dd>
|
||||
if email != "" {
|
||||
<dt>Signed in as</dt>
|
||||
<dd>{ email }</dd>
|
||||
}
|
||||
</dl>
|
||||
<section>
|
||||
<h2>Connected accounts</h2>
|
||||
if len(conns) == 0 {
|
||||
<p class="muted">No connected video accounts yet.</p>
|
||||
} else {
|
||||
<ul class="conn-list">
|
||||
for _, c := range conns {
|
||||
<li class="conn">
|
||||
<div class="conn-main">
|
||||
<span class="conn-provider">{ providerLabel(c.Provider) }</span>
|
||||
if c.ProviderAccount != "" {
|
||||
<span class="muted">{ c.ProviderAccount }</span>
|
||||
}
|
||||
<span class="chip">{ c.Status }</span>
|
||||
</div>
|
||||
<div class="conn-meta muted">connected { c.ConnectedAt.Format("2006-01-02") }</div>
|
||||
<form method="post" action={ disconnectURL(c.Provider) }>
|
||||
<button type="submit" class="btn-secondary">Disconnect</button>
|
||||
</form>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
if !hasYouTube(conns) {
|
||||
<p><a class="btn" href="/oauth/youtube/connect">Connect YouTube</a></p>
|
||||
}
|
||||
</section>
|
||||
<section class="danger-zone">
|
||||
<h2>Delete account</h2>
|
||||
<p class="muted">
|
||||
Permanently remove your Tapir account and all of its data — summaries,
|
||||
watch/skip/save actions, and connected accounts. This cannot be undone.
|
||||
</p>
|
||||
<details class="confirm-delete">
|
||||
<summary class="btn-danger">Delete account…</summary>
|
||||
<div class="confirm-body">
|
||||
<p>This permanently deletes your account and all data. Are you sure?</p>
|
||||
<form method="post" action="/account/delete">
|
||||
<button type="submit" class="btn-danger">Yes, permanently delete my account</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
</article>
|
||||
}
|
||||
}
|
||||
|
||||
// ActionButtons is the toggle group fragment returned by POST /v/{id}/action.
|
||||
// Each button submits its verb; HTMX swaps this element in place (outerHTML),
|
||||
// and without JS the form POSTs and the handler redirects back to the detail
|
||||
|
||||
Reference in New Issue
Block a user