Commit 1888fca
authored
fix(stack): close codex follow-up — auth races, CORS gaps, missing email-link routes (#77)
* fix(stack): close codex follow-up — auth races, CORS gaps, missing email-link routes
Second Codex pass uncovered six gaps. This PR ships fixes for all six, each
with its own guardrail or sibling test.
## Atomicity in single-use token flows
* Email verification: token lookup moves INSIDE the transaction as an atomic
`DELETE … RETURNING`. The user row is then locked `FOR UPDATE`. Two parallel
verify calls used to both pass a pre-tx existence check and both call
`provisionAfterVerification` (which itself does select-then-insert against
no unique constraint) — the user ended up with two personal accounts. The
atomic claim collapses the race to one winner.
* Password reset: same shape — atomic `DELETE … RETURNING` inside the tx.
The bcrypt hash still runs outside the tx so it doesn't pin a connection,
but only the call that wins the DELETE proceeds to UPDATE the password.
* Ownership transfer decline: now transactional with the same `FOR UPDATE`
lock + repeated `acceptedAt/declinedAt IS NULL` predicates on the UPDATE
that accept() uses. Without it, an accept/decline race could leave BOTH
timestamps set.
## Billing customer creation
* Stripe `customers.create` now passes `idempotencyKey: account-customer:{id}`
so a double-click collapses to one customer record. The DB write-back is
conditional (`stripeCustomerId IS NULL OR ''`) so a concurrent caller
that beat us to the API doesn't get overwritten. Stripe holds idempotency
keys for 24h — well beyond any plausible double-submit window.
## CORS for cross-origin Sentry tracing
* Added `sentry-trace`, `baggage`, `traceparent` to `CORS_ALLOWED_HEADERS`.
The Sentry browser SDK writes these on every outbound /api/* fetch when
`browserTracingIntegration` is loaded; without the allowlist the cross-
origin preflight rejects every traced call.
* Added `exposeHeaders: ['x-request-id']` so the SPA's error toasts can
still surface the request id when the API runs on a different host.
* Sibling test in `security.constants.test.ts` locks both behaviours.
## Three missing email-link landing routes
* `/invitations/accept` — auto-fires the accept call (mirrors VerifyEmailPage
shape). ProtectedRoute-wrapped so anonymous clicks land on /login first
and come back with the `?token=` preserved.
* `/account/ownership-transfer/accept` — renders Accept and Decline buttons,
NEVER auto-fires. Accepting transfers a whole account — a stray click on
the email shouldn't perform it. The API also enforces recipient-JWT match.
* `/account/requests` — reviewer-side inbox for domain-join requests with
approve/deny actions. Lives inside AppShell. The API enforces role on
every mutation; we don't add a second gate.
Each new page ships with `.hooks.ts`, `.utils.ts`, `.types.ts`,
`.constants.ts`, `.stories.tsx`, `.test.tsx`, `.utils.test.ts`, plus
matching sibling `.hooks.test.tsx`. i18n keys added to both en and de
common.json bundles. Log event names registered in `logger.events.ts`.
## Out of scope (P2/P3 from the Codex pass)
* P2: OpenAPI fidelity — `success: boolean` → `t.Literal(true)`, dropping
the multipart/text-plain default. Touches every route in the API and is
pure schema hygiene; deferred to its own PR.
* P3: UI/API authorization rule duplication — would need a generated rule
matrix or parity test, neither of which fits a follow-up.
## Gates
* API: 1033/1035 (2 DB-only skipped — local pg unavailable)
* UI: 572/572
* Both apps: lint + lint-meta + typecheck + knip clean
* fix(auth): keep "Email already verified" branch after atomic claim
Codex follow-up tightened email verify to use atomic DELETE...RETURNING
on the token row, which collapsed the duplicate-account race. But it
also removed the explicit "already verified" check — a second click on
an older verification link (still a valid token row from a prior
resend) would now silently re-run the provisioning idempotency path
instead of telling the user the email is already verified.
Reinstates the check inside the transaction, after the user row is
locked FOR UPDATE. Both contracts hold: races are atomic, and a stale
verification email surfaces the user-friendly error string the
integration test still asserts.1 parent b2eb426 commit 1888fca
49 files changed
Lines changed: 2497 additions & 83 deletions
File tree
- apps
- api
- src
- api
- accounts
- auth/services
- billing
- config/security
- tests/config/security
- ui/src
- app/router
- features/accounts
- components
- InvitationAcceptPage
- JoinRequestsPage
- OwnershipTransferAcceptPage
- lib
- i18n/locales
- de
- en
- logger
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 52 additions & 34 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
261 | 261 | | |
262 | 262 | | |
263 | 263 | | |
264 | | - | |
265 | | - | |
266 | | - | |
267 | | - | |
268 | | - | |
269 | | - | |
270 | | - | |
271 | | - | |
272 | | - | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
273 | 283 | | |
274 | | - | |
275 | | - | |
| 284 | + | |
| 285 | + | |
276 | 286 | | |
277 | | - | |
278 | | - | |
279 | | - | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
280 | 290 | | |
281 | | - | |
282 | | - | |
283 | | - | |
284 | | - | |
285 | | - | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
286 | 296 | | |
287 | | - | |
288 | | - | |
289 | | - | |
290 | | - | |
291 | | - | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
292 | 309 | | |
293 | | - | |
294 | | - | |
295 | | - | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
296 | 313 | | |
297 | | - | |
298 | | - | |
299 | | - | |
300 | | - | |
301 | | - | |
302 | | - | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
303 | 320 | | |
304 | | - | |
| 321 | + | |
| 322 | + | |
305 | 323 | | |
306 | 324 | | |
307 | 325 | | |
| |||
Lines changed: 50 additions & 27 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
| 40 | + | |
50 | 41 | | |
51 | | - | |
52 | | - | |
53 | | - | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
54 | 66 | | |
55 | | - | |
56 | | - | |
57 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
58 | 73 | | |
59 | | - | |
60 | | - | |
61 | | - | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
62 | 77 | | |
63 | | - | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
64 | 89 | | |
65 | | - | |
66 | 90 | | |
67 | 91 | | |
68 | 92 | | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
| 93 | + | |
73 | 94 | | |
74 | | - | |
75 | | - | |
| 95 | + | |
| 96 | + | |
76 | 97 | | |
77 | 98 | | |
| 99 | + | |
| 100 | + | |
78 | 101 | | |
79 | 102 | | |
80 | 103 | | |
| |||
Lines changed: 25 additions & 15 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
| 139 | + | |
145 | 140 | | |
146 | | - | |
147 | | - | |
148 | | - | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
149 | 160 | | |
150 | | - | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
151 | 164 | | |
152 | | - | |
153 | 165 | | |
154 | 166 | | |
155 | 167 | | |
156 | 168 | | |
157 | 169 | | |
158 | | - | |
| 170 | + | |
159 | 171 | | |
160 | 172 | | |
161 | 173 | | |
| |||
165 | 177 | | |
166 | 178 | | |
167 | 179 | | |
168 | | - | |
169 | | - | |
170 | | - | |
| 180 | + | |
171 | 181 | | |
172 | 182 | | |
173 | 183 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| |||
169 | 169 | | |
170 | 170 | | |
171 | 171 | | |
172 | | - | |
173 | | - | |
174 | | - | |
175 | | - | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
176 | 189 | | |
177 | 190 | | |
178 | 191 | | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
179 | 200 | | |
180 | 201 | | |
181 | 202 | | |
182 | | - | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
183 | 212 | | |
184 | 213 | | |
185 | 214 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
24 | 34 | | |
25 | 35 | | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
26 | 45 | | |
27 | 46 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| 29 | + | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
| |||
Lines changed: 29 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
0 commit comments