feat(web): public /invite/{token} set-password + account-creation flow
The Stage-1 onboarding path: an invited user opens their emailed link, sets a password, and Tapir creates their Dex local-password account so they can log in. Mounted on root OUTSIDE Auth.Middleware — the visitor has no Dex session yet; the token in the path is the capability. handleInviteForm previews the token (no consume) and shows the form, or a clear "expired / already used" page. handleInviteSubmit validates the password BEFORE consuming the token (a typo is retryable), then claims the invite exactly once, bcrypt-hashes (cost 12), and creates the Dex account — mapping ErrPasswordExists -> "log in instead" and ErrForbidden -> "contact the administrator". Off-cluster (App.Dex nil) it degrades to a "deployed-only" message without burning the token. On success it sets an account_created flash and redirects to /auth/login. Welcome sub-text now states access is invite-only. Handlers depend on narrow ports (InvitationStore, DexPasswordCreator) so tests use fakes; cmdServe wires the store + an in-cluster dex.PasswordClient. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+11
-5
@@ -183,6 +183,11 @@ func statusURL(videoID string) templ.SafeURL {
|
||||
return templ.SafeURL("/v/" + videoID + "/status")
|
||||
}
|
||||
|
||||
// inviteURL builds the claim path (POST) for an invite token.
|
||||
func inviteURL(token string) templ.SafeURL {
|
||||
return templ.SafeURL("/invite/" + token)
|
||||
}
|
||||
|
||||
// Charmbracelet-inspired palette for the summarizing animation (TapirSpinner) —
|
||||
// a charm purple box, pink tapir, mint snout/eyes/progress. Kept as named consts
|
||||
// so the inline span colours and the CSS track/fill share one source of truth.
|
||||
@@ -333,11 +338,12 @@ type flashView struct {
|
||||
// flashMessages maps each flash code to its banner. An unknown code renders no
|
||||
// banner (flashFor returns ok=false), so a forged cookie value is inert.
|
||||
var flashMessages = map[string]flashView{
|
||||
flashConnected: {"success", "YouTube account connected."},
|
||||
flashConnectFailed: {"error", "Could not connect your YouTube account. Please try again."},
|
||||
flashDisconnected: {"success", "Account disconnected."},
|
||||
flashDeleted: {"success", "Your account and all its data were deleted."},
|
||||
flashRegistered: {"success", "Welcome to Tapir — your account is ready."},
|
||||
flashConnected: {"success", "YouTube account connected."},
|
||||
flashConnectFailed: {"error", "Could not connect your YouTube account. Please try again."},
|
||||
flashDisconnected: {"success", "Account disconnected."},
|
||||
flashDeleted: {"success", "Your account and all its data were deleted."},
|
||||
flashRegistered: {"success", "Welcome to Tapir — your account is ready."},
|
||||
flashAccountCreated: {"success", "Account created — log in with your email and password."},
|
||||
}
|
||||
|
||||
func flashFor(code string) (flashView, bool) {
|
||||
|
||||
Reference in New Issue
Block a user