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:
@@ -59,7 +59,7 @@ templ WelcomePage(user User, loggedIn bool) {
|
||||
<div class="welcome-cta">
|
||||
<a class="btn btn-lg" href="/auth/login">Get Started</a>
|
||||
</div>
|
||||
<p class="welcome-sub">New to Tapir? Just sign in — you'll complete a quick setup right after. Already have an account? You'll go straight through.</p>
|
||||
<p class="welcome-sub">Access is by invitation. If you have an invite link, it will set up your account automatically. Returning users with credentials can log in above.</p>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
@@ -307,6 +307,68 @@ templ RegisterPage(email, errMsg string) {
|
||||
}
|
||||
}
|
||||
|
||||
// InvitePage is the public set-password form an invited user reaches via their
|
||||
// emailed /invite/{token} link. The email is shown read-only (it is fixed by the
|
||||
// invite, not chosen here); the visitor sets a password to create their account.
|
||||
// errMsg, when set, reports a validation problem on the prior submit. No auth
|
||||
// chrome (header nav) is appropriate — the visitor has no session yet — but the
|
||||
// shared Layout keeps the look consistent.
|
||||
templ InvitePage(email, token, errMsg string) {
|
||||
@Layout("Tapir — Set your password") {
|
||||
<article class="register">
|
||||
<h1>Set up your Tapir account</h1>
|
||||
<p class="meta">Invitation for { email }.</p>
|
||||
<p>Choose a password to finish creating your account. You'll then log in with this email and password.</p>
|
||||
if errMsg != "" {
|
||||
<p class="error" role="alert">{ errMsg }</p>
|
||||
}
|
||||
<form method="post" action={ inviteURL(token) } class="register-form">
|
||||
<label>
|
||||
Email
|
||||
<input type="email" name="email" value={ email } readonly/>
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input type="password" name="password" minlength="8" required autofocus autocomplete="new-password"/>
|
||||
</label>
|
||||
<label>
|
||||
Confirm password
|
||||
<input type="password" name="password_confirm" minlength="8" required autocomplete="new-password"/>
|
||||
</label>
|
||||
<button type="submit" class="btn">Create my account</button>
|
||||
</form>
|
||||
</article>
|
||||
}
|
||||
}
|
||||
|
||||
// InviteInvalidPage is shown when an invite token is missing, expired, or already
|
||||
// used — a dead-end with no form, so a stale or replayed link reads clearly.
|
||||
templ InviteInvalidPage() {
|
||||
@Layout("Tapir — Invitation") {
|
||||
<article class="register">
|
||||
<h1>This invite link is no longer valid</h1>
|
||||
<p>This invitation has expired or has already been used. Ask for a fresh invite link, or log in if you already have an account.</p>
|
||||
<p><a class="btn" href="/auth/login">Log in</a></p>
|
||||
</article>
|
||||
}
|
||||
}
|
||||
|
||||
// InviteNoticePage is a terminal message after a submit that neither succeeded nor
|
||||
// is a retryable validation error (account already exists, RBAC missing, or the
|
||||
// dev "deployed-only" degrade). showLogin adds a log-in CTA where that is the
|
||||
// natural next step.
|
||||
templ InviteNoticePage(message string, showLogin bool) {
|
||||
@Layout("Tapir — Invitation") {
|
||||
<article class="register">
|
||||
<h1>Invitation</h1>
|
||||
<p>{ message }</p>
|
||||
if showLogin {
|
||||
<p><a class="btn" href="/auth/login">Log in</a></p>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user