feat: register T1 (pending, verify)
This commit is contained in:
1 parent
3c3f7c6194
commit
4e41d60142
29 files changed
+2162
-42
No files matched your search
@@ -0,0 +1,197 @@
|
||||
Feature: Verify account screen
|
||||
|
||||
As a user who just clicked the verification link in their email
|
||||
I want /register/validate?c=CODE to submit that code automatically, land me on my dashboard once
|
||||
it's accepted, tell me plainly when it's wrong or expired, and let me log in inline if I opened
|
||||
the link somewhere I'm not logged in
|
||||
So that verifying my account is a single click from my inbox, whatever device or session state I
|
||||
open the link in
|
||||
|
||||
Background:
|
||||
Given /register/validate is a "public" route (meta.auth "public"), rendered as VerifyPage
|
||||
inside AuthShell, per docs/design/routes.md; the router's beforeEach never runs session.check
|
||||
for a public route, so VerifyPage starts with no assumption about the session
|
||||
And VerifyPage only calls POST /v1/auth/verify, DELETE /v1/auth/register, and POST
|
||||
/v1/auth/logout (and, inline, POST /v1/auth/login on a 401)
|
||||
And on mount, VerifyPage reads the "c" query parameter once: if it is absent or an empty
|
||||
string, no request is ever made and the Missing code state renders immediately; otherwise the
|
||||
Verifying state renders and POST /v1/auth/verify is called immediately with { code: c }
|
||||
And every call to POST /v1/auth/verify (the initial one and the post-login retry) passes
|
||||
{ onUnauthorized: 'passthrough', onForbidden: 'passthrough' } (the same onForbidden
|
||||
RequestOptions field added to useApi by features/pending_screen.feature, plus the existing
|
||||
onUnauthorized field used the same way getMe() already uses it), so a 401 renders the inline
|
||||
log-in state instead of the global redirect to /login, and a 403 renders the Invalid-or-
|
||||
expired state instead of the global "unverified session" redirect
|
||||
And Verifying and No-session-retrying both render the same full-card spinner: a centered
|
||||
"i-lucide-loader-2" icon with a spin animation and the text "Verifying your account…",
|
||||
role="status", replacing AuthShell's whole card body with no title header shown
|
||||
And on a 200, VerifyPage shows a ResultPanel with tone "success", icon "i-lucide-shield-check",
|
||||
title "Your account is verified", text "Taking you to your dashboard…", and no actions, while
|
||||
concurrently awaiting session.retry() (needed so /home's route guard sees the now-verified
|
||||
session instead of whatever stale state this tab last cached) and an 800 ms timer; once both
|
||||
settle, the app navigates to /home and a toast (color "success", icon "i-lucide-circle-check",
|
||||
title "Your account is verified", description "Welcome to RenewCMS.") is shown
|
||||
And on a 403 (user.ErrInvalidVerificationCode or user.ErrVerificationCodeExpired), VerifyPage
|
||||
shows a ResultPanel with tone "error", icon "i-lucide-link-2-off", title "This verification
|
||||
link doesn't work", text "It may have expired (links last 2 hours) or already been used.",
|
||||
and a note (rendered via the default slot) "Cancel your registration and start over to get a
|
||||
new link.", with a primary action "Cancel registration and start over" and a secondary action
|
||||
"Log out"; unlike Pending's cancel flow, this button opens no ConfirmDialog (verify.md's state
|
||||
list has no confirm step for it) and calls DELETE /v1/auth/register directly, passing
|
||||
{ onForbidden: 'passthrough' } for the same reason Pending's own DELETE call does
|
||||
And "Cancel registration and start over" resolving 200 navigates to /register and shows the
|
||||
same toast Pending's own Cancelled state shows (color "success", icon "i-lucide-circle-check",
|
||||
title "Registration cancelled", description "You can create the owner account again."),
|
||||
reusing that copy since it is the same underlying event
|
||||
And "Cancel registration and start over" failing — with a 403 (already verified elsewhere, so
|
||||
there is nothing left to cancel), a 500, or a request that never reaches the server — shows an
|
||||
alert on the same ResultPanel screen via AuthShell's #notice slot, reusing Register's existing
|
||||
server-error pattern: color "error", icon "i-lucide-circle-x", title "Couldn't cancel
|
||||
registration", description "Something went wrong on the server. Try again in a moment.",
|
||||
variant "soft", not closable, role "alert"; the ResultPanel and its two actions stay exactly
|
||||
as they were so the user can try again or choose "Log out" instead; no redirect happens for
|
||||
the 403 case here, unlike Pending's own already-verified handling
|
||||
And "Log out" (on the Invalid-or-expired state) behaves exactly as it does on Pending: it calls
|
||||
POST /v1/auth/logout, which per loginHandler.Logout always responds 200 and clears the jwt
|
||||
cookie, then VerifyPage calls session.clear() and navigates to /login, disabling both actions
|
||||
while the call is in flight
|
||||
And on a 401, VerifyPage replaces the spinner with an inline log-in form: AuthShell title "Log
|
||||
in to finish verifying", description "You opened this link somewhere you're not logged in.
|
||||
Log in here and we'll verify your account right after.", a Username field (placeholder "Your
|
||||
username") and a Password field (password input with a show/hide toggle), and a submit button
|
||||
"Log in and verify"; there is no "Forgot password?" hint and no registration-open footer,
|
||||
unlike LoginView
|
||||
And the inline form's submit is disabled until both fields are non-empty; on submit it calls
|
||||
POST /v1/auth/login with { username, password }
|
||||
And a 200 from that login call transitions to the No-session-retrying spinner and immediately
|
||||
retries POST /v1/auth/verify with the same code and the same passthrough options, which then
|
||||
follows the 200/403 handling above
|
||||
And a 403 (wrong credentials), 429, or 500/network failure from that inline login call reuses
|
||||
LoginView's existing alert copy for the same statuses (wrongCredentialsAlert, with LoginView's
|
||||
exact title/description; tooManyAttemptsAlert with the same retryAfter countdown behavior as
|
||||
LoginView; serverErrorAlert), shown via AuthShell's #notice slot, since verify.md specifies no
|
||||
separate copy for this embedded form and states.md's HTTP-status table assigns this copy
|
||||
generically to "Log in" contexts
|
||||
And while that inline login is submitting, both fields are disabled and the submit button reads
|
||||
"Logging in…" with a loading indicator, mirroring LoginView's submitting state
|
||||
|
||||
Scenario: Verifying — the code submits automatically on load
|
||||
When the user navigates to "/register/validate?c=abc123"
|
||||
Then the full-card spinner is shown with the text "Verifying your account…"
|
||||
And POST /v1/auth/verify is called once, immediately, with body {"code": "abc123"}
|
||||
|
||||
Scenario: Success — 200 refreshes the session and navigates to /home with a toast
|
||||
Given the user is on "/register/validate?c=abc123" in the Verifying state
|
||||
When POST /v1/auth/verify resolves 200
|
||||
Then a ResultPanel is shown with tone "success", icon "i-lucide-shield-check", title "Your
|
||||
account is verified", text "Taking you to your dashboard…", and no actions
|
||||
And session.retry() is awaited
|
||||
And after 800 ms and once session.retry() has resolved, the app navigates to /home
|
||||
And a toast is shown with color "success", icon "i-lucide-circle-check", title "Your account is
|
||||
verified", description "Welcome to RenewCMS."
|
||||
|
||||
Scenario: Invalid or expired — 403 offers a way to start over
|
||||
Given the user is on "/register/validate?c=abc123" in the Verifying state
|
||||
When POST /v1/auth/verify resolves 403
|
||||
Then a ResultPanel is shown with tone "error", icon "i-lucide-link-2-off", title "This
|
||||
verification link doesn't work", text "It may have expired (links last 2 hours) or already
|
||||
been used.", and note "Cancel your registration and start over to get a new link."
|
||||
And its actions are a primary "Cancel registration and start over" and a secondary "Log out"
|
||||
|
||||
Scenario: Cancel registration and start over — success
|
||||
Given the ResultPanel from "Invalid or expired" is shown
|
||||
When the user activates "Cancel registration and start over"
|
||||
Then DELETE /v1/auth/register is sent directly, without any confirmation dialog, with
|
||||
{ onForbidden: 'passthrough' }
|
||||
When it resolves 200
|
||||
Then the app navigates to /register
|
||||
And a toast is shown with color "success", icon "i-lucide-circle-check", title "Registration
|
||||
cancelled", description "You can create the owner account again."
|
||||
|
||||
Scenario Outline: Cancel registration and start over — failure shows an alert, not a redirect
|
||||
Given the ResultPanel from "Invalid or expired" is shown
|
||||
When the user activates "Cancel registration and start over"
|
||||
And DELETE /v1/auth/register <failure>
|
||||
Then an alert is shown with color "error", icon "i-lucide-circle-x", title "Couldn't cancel
|
||||
registration", description "Something went wrong on the server. Try again in a moment."
|
||||
And the ResultPanel and its two actions ("Cancel registration and start over", "Log out") are
|
||||
still shown, unchanged, so the user can retry or log out instead
|
||||
And no navigation happens
|
||||
|
||||
Examples:
|
||||
| failure |
|
||||
| resolves 403 with the apperr.AlreadyVerifiedErrorMessage body |
|
||||
| fails with a 500 response |
|
||||
| never reaches the server |
|
||||
|
||||
Scenario: Log out from the Invalid-or-expired state
|
||||
Given the ResultPanel from "Invalid or expired" is shown
|
||||
When the user activates "Log out"
|
||||
Then POST /v1/auth/logout is sent
|
||||
When it resolves 200
|
||||
Then the session store is cleared
|
||||
And the app navigates to /login
|
||||
|
||||
Scenario: No session — the inline log-in form replaces the spinner
|
||||
Given the user is on "/register/validate?c=abc123" in the Verifying state
|
||||
When POST /v1/auth/verify resolves 401
|
||||
Then the spinner is replaced by a form titled "Log in to finish verifying" with description
|
||||
"You opened this link somewhere you're not logged in. Log in here and we'll verify your
|
||||
account right after."
|
||||
And it has a Username field (placeholder "Your username") and a Password field with a show/hide
|
||||
toggle, and a submit button "Log in and verify"
|
||||
And there is no "Forgot password?" link and no registration-open footer
|
||||
|
||||
Scenario: No session — submit is disabled until both fields are filled
|
||||
Given the inline log-in form from "No session" is shown
|
||||
Then the submit button is disabled
|
||||
When the user types "marta" into Username and "limewash2026" into Password
|
||||
Then the submit button is enabled
|
||||
|
||||
Scenario: No session — successful login retries verification with the same code
|
||||
Given the inline log-in form from "No session" is shown on "/register/validate?c=abc123"
|
||||
When the user submits Username "marta" and Password "limewash2026"
|
||||
And POST /v1/auth/login resolves 200
|
||||
Then the full-card spinner is shown again with the text "Verifying your account…"
|
||||
And POST /v1/auth/verify is sent again, with body {"code": "abc123"} and the same passthrough
|
||||
options as the first attempt
|
||||
|
||||
Scenario: No session — wrong credentials during the inline login
|
||||
Given the inline log-in form from "No session" is shown
|
||||
When the user submits Username "marta" and Password "wrong-password"
|
||||
And POST /v1/auth/login resolves 403
|
||||
Then an error alert is shown with title "That username and password don't match" and
|
||||
description "Check both and try again. Passwords are case-sensitive."
|
||||
And the Password field is cleared and refocused
|
||||
And POST /v1/auth/verify is not retried
|
||||
|
||||
Scenario: No session — too many login attempts
|
||||
Given the inline log-in form from "No session" is shown
|
||||
When the user submits Username "marta" and Password "limewash2026"
|
||||
And POST /v1/auth/login resolves 429 with Retry-After 30
|
||||
Then an info alert is shown with title "Too many attempts" and description "For your security,
|
||||
wait a moment before trying again."
|
||||
And the submit button reads "Try again in 0:30" and is disabled until the countdown ends
|
||||
|
||||
Scenario Outline: No session — server or network error during the inline login
|
||||
Given the inline log-in form from "No session" is shown
|
||||
When the user submits Username "marta" and Password "limewash2026"
|
||||
And POST /v1/auth/login <failure>
|
||||
Then an error alert is shown with title "Couldn't reach RenewCMS" and description "Check your
|
||||
connection and try again."
|
||||
And the fields keep their values and are re-enabled
|
||||
|
||||
Examples:
|
||||
| failure |
|
||||
| fails with a 500 response |
|
||||
| never reaches the server |
|
||||
|
||||
Scenario: Missing code — no request is made
|
||||
When the user navigates to "/register/validate" with no "c" query parameter
|
||||
Then no request is made
|
||||
And a ResultPanel is shown with tone "error", icon "i-lucide-link-2-off", title "This link is
|
||||
incomplete", text "The verification code is missing. Open the link from your email again, or
|
||||
copy the full address into your browser."
|
||||
And its single action is a secondary "Go to log in"
|
||||
When the user activates "Go to log in"
|
||||
Then the app navigates to /login
|
||||
Reference in new issue
Block a user