fix(water-log): make verifyWaterAdminPin fully PIN-only
Deploy to route.crispygoat.com / deploy (push) Successful in 4m3s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m3s
The previous PR wrapped the getAdminUser() call in try/catch as a
best-effort audit hook. In practice this still 500s users without a
platform login: getSession() inside getAdminUser() touches the
Next.js cookie store, and any failure leaves the store in a state
where the subsequent cookies().set('wl_admin_session', ...) throws.
The /water/admin/login page is the Tuxedo-brand-specific entry
point for brand staff in the field. It must work PIN-only, with no
Neon Auth dependency.
Fix:
- Drop the getAdminUser() call inside verifyWaterAdminPin entirely.
- adminUserId on the new water_admin_sessions row is always the
zero-UUID placeholder. Audit attribution is intentionally deferred.
- Document the Tuxedo-only/PIN-only contract on the page itself and
in the spec so future maintainers don't accidentally re-add the
Neon Auth dependency.
Guards (TDD, red → green):
- tests/unit/water-log-auth.test.ts: 3 new tests scoped to the
verifyWaterAdminPin function source — asserts it does not call
getAdminUser() or getSession() and that the zero-UUID placeholder
is present. These fail if a future refactor re-introduces the
dependency.
This commit is contained in:
@@ -88,12 +88,13 @@ The site-admin helper (`requireWaterAdminPermission`) keeps using
|
||||
`/admin/water-log/*` (which sits behind Neon Auth in the middleware
|
||||
and on the server).
|
||||
|
||||
> Note: the action `verifyWaterAdminPin` (in `settings.ts`) does make a
|
||||
> best-effort `getAdminUser()` call *after* a successful PIN check, but
|
||||
> only to attach a platform-admin `adminUserId` to the new
|
||||
> `water_admin_sessions` row for audit. It is wrapped in try/catch and
|
||||
> falls back to a zero-UUID placeholder. The `wl_admin_session` is
|
||||
> valid either way — that helper call is not an auth gate.
|
||||
> Note: the action `verifyWaterAdminPin` (in `settings.ts`) is now a
|
||||
> fully PIN-only flow — it does NOT call `getAdminUser()` or
|
||||
> `getSession()` at all. The `adminUserId` on the new
|
||||
> `water_admin_sessions` row is always the zero-UUID placeholder. This
|
||||
> is because `/water/admin/login` is the Tuxedo-brand-specific entry
|
||||
> point and must work for users who are not signed into the platform.
|
||||
> Audit attribution is intentionally deferred to a future ticket.
|
||||
|
||||
### 4.2 New file: `src/actions/water-log/auth.ts`
|
||||
|
||||
@@ -174,23 +175,25 @@ moves.
|
||||
keep their existing `getAdminUser()` gate (site-admin path).
|
||||
- `verifyWaterAdminPin` becomes a pure PIN-only flow: validate format,
|
||||
look up `water_admin_settings` for the brand, verify PIN, mint
|
||||
`wl_admin_session` cookie. **No `getSession()` call.** `getAdminUser()`
|
||||
is still called inside the success path to attach an `adminUserId`
|
||||
to the new session row for audit — wrapped in try/catch, falls back
|
||||
to a zero-UUID placeholder if no admin user is signed in. The
|
||||
session itself is valid either way.
|
||||
`wl_admin_session` cookie. **No `getSession()` call. No
|
||||
`getAdminUser()` call.** `adminUserId` is always the zero-UUID
|
||||
placeholder. The page is the Tuxedo-brand-specific entry point and
|
||||
must work without a platform login.
|
||||
|
||||
### 4.4 Why `verifyWaterAdminPin` is a special case
|
||||
|
||||
`verifyWaterAdminPin` is the most-affected function in this PR. Today:
|
||||
|
||||
1. It calls `await getSession()` (Neon Auth) — drop.
|
||||
2. It calls `getAdminUser()` inside the success path to attach the
|
||||
1. It called `await getSession()` (Neon Auth) — dropped.
|
||||
2. It called `getAdminUser()` inside the success path to attach the
|
||||
caller's platform-admin user to the new `wl_admin_session` row for
|
||||
audit. This is correct and stays, but the call is now **wrapped in
|
||||
a try/catch that swallows any error and falls back to a zero-UUID
|
||||
`adminUserId`**. The `wl_admin_session` is still valid in either
|
||||
case — the cookie holds the brand + session id, not the admin user.
|
||||
audit. That call was wrapped in a try/catch that fell back to a
|
||||
zero-UUID `adminUserId`, but the underlying `getSession()` call
|
||||
inside `getAdminUser()` could still corrupt the Next.js cookie
|
||||
store and cause the subsequent `cookies().set("wl_admin_session",
|
||||
...)` to throw — surfacing as a 500 to users without a platform
|
||||
login. The call is now **fully removed**; `adminUserId` is always
|
||||
the zero-UUID placeholder. Audit attribution is a follow-up.
|
||||
|
||||
This is the right shape because the water-admin PIN flow is for ditch
|
||||
riders / brand water admins, who often aren't platform admins. Tying
|
||||
|
||||
Reference in New Issue
Block a user