verifyWaterAdminPin previously called cookies().set(...) from inside
the withBrand(...) pg transaction. The pg transaction's
AsyncLocalStorage context masks Next.js's request-scoped cookie
store, so for users without a platform login the cookie set threw
and the route handler returned 500.
Move the cookie write to the outer function frame, after withBrand
resolves. The DB session row is still inserted inside the
transaction (so it's atomic with the PIN verification); only the
cookie write is hoisted out.
The prior fix removed getAdminUser() but left the cookie set inside
the transaction, so valid PINs still 500'd. This is the missing
half.
Adds a static regression test that asserts const cookieStore
is declared AFTER the matching close-paren of withBrand(...).
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.
Field users at /water were blocked because every server action called
getSession() (Neon Auth) even though the field path is PIN-authenticated.
A missing or expired platform session hung or rejected PIN submissions,
so users without a platform login could never reach the water log entry
screen.
Centralize water-log auth in src/actions/water-log/auth.ts with three
helpers — requireFieldSession, requireWaterAdminSession, and
requireWaterAdminPermission — and migrate all server actions off the
stray getSession() calls. Auth.ts deliberately does NOT import
getSession; a static-source unit test enforces that.
Changes:
- src/actions/water-log/auth.ts (new): dedicated auth layer, no Neon Auth
- src/actions/water-log/field.ts: -10 await getSession(), use helpers
- src/actions/water-log/admin.ts: -18 await getSession(), use helpers
- src/actions/water-log/settings.ts: -4 await getSession(), resilient
to missing platform admin
- 3x admin pages: update getWaterAdminSession import path
- tests/unit/water-log-auth.test.ts (new): 17 tests incl. regression
guard that auth.ts never imports getSession
- tests/water-log.spec.ts: 3 E2E regression tests (no platform login)
- vitest.config.ts: alias for deep @/db/schema/water-log path
Rollback = revert this commit. No DB migration; no schema change.
Existing rows/cookies unchanged.