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:
@@ -68,6 +68,14 @@ type App struct {
|
||||
// Processing tracks in-flight immediate summarizations so the status endpoint
|
||||
// shows the animation until the summary lands. The zero value is ready to use.
|
||||
Processing ProcessingSet
|
||||
// Invitations validates and consumes email-invite tokens for the public
|
||||
// /invite/{token} flow. Nil = the invite routes report "invalid" (the flow is
|
||||
// effectively off). *store.Store satisfies it.
|
||||
Invitations InvitationStore
|
||||
// Dex creates the Dex local-password account when an invite is claimed. Nil =
|
||||
// not in-cluster (dev): the submit handler degrades to a clear "deployed-only"
|
||||
// message instead of creating an account. *dex.PasswordClient satisfies it.
|
||||
Dex DexPasswordCreator
|
||||
}
|
||||
|
||||
func (a *App) logger() *slog.Logger {
|
||||
@@ -87,6 +95,11 @@ func (a *App) Router() http.Handler {
|
||||
root.Handle("GET /static/", staticHandler())
|
||||
root.Handle("/auth/", a.Auth.Routes())
|
||||
|
||||
// Email invitation claim (public — the visitor has no Dex session yet, so this
|
||||
// sits OUTSIDE Auth.Middleware). The token in the path is the capability.
|
||||
root.HandleFunc("GET /invite/{token}", a.handleInviteForm)
|
||||
root.HandleFunc("POST /invite/{token}", a.handleInviteSubmit)
|
||||
|
||||
app := http.NewServeMux()
|
||||
app.HandleFunc("GET /{$}", a.handleList)
|
||||
app.HandleFunc("GET /v/{videoId}", a.handleDetail)
|
||||
|
||||
Reference in New Issue
Block a user