fix(water-log): make verifyWaterAdminPin fully PIN-only
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:
Tyler
2026-07-01 17:45:30 -06:00
parent 658f6a5b8b
commit 98fc1d3bdd
4 changed files with 107 additions and 29 deletions
+8 -12
View File
@@ -226,17 +226,13 @@ const formatError = validatePin(pin);
return { success: false, error: "Invalid PIN" };
}
// Tie the session to the calling site admin (best-effort).
// The `wl_admin_session` is valid whether or not a platform admin
// is signed in — this call only attaches an `adminUserId` for
// audit. If `getAdminUser()` fails or returns null, we fall back
// to a zero-UUID placeholder.
let adminUser: Awaited<ReturnType<typeof getAdminUser>> = null;
try {
adminUser = await getAdminUser();
} catch {
// ignore — the session is valid regardless
}
// The `/water/admin/login` page is Tuxedo-brand-specific and PIN-only.
// There is no platform-admin requirement here — `wl_admin_session` is
// valid whether or not a Neon Auth session exists. We deliberately do
// NOT call `getAdminUser()` (it would touch cookies via Neon Auth and
// 500 for users without a platform login). `adminUserId` is therefore
// always the zero-UUID placeholder; audit attribution happens elsewhere.
const PLACEHOLDER_ADMIN_USER_ID = "00000000-0000-0000-0000-000000000000";
const expiresAt = new Date(
Date.now() + settings.sessionDurationHours * 60 * 60 * 1000,
@@ -245,7 +241,7 @@ const formatError = validatePin(pin);
.insert(waterAdminSessions)
.values({
brandId,
adminUserId: adminUser?.user_id ?? adminUser?.id ?? "00000000-0000-0000-0000-000000000000",
adminUserId: PLACEHOLDER_ADMIN_USER_ID,
pinHashUsed: settings.pinHash,
expiresAt,
})
+13
View File
@@ -3,6 +3,19 @@ import WaterAdminPinClient from "./WaterAdminPinClient";
export const dynamic = "force-dynamic";
/**
* Tuxedo-brand-specific water admin login.
*
* The 4-digit admin PIN gates `/water/admin/*` for the Tuxedo brand.
* Users must be able to enter the PIN WITHOUT being signed into the
* platform — this is a PIN-only entry point for brand staff in the
* field. The corresponding `verifyWaterAdminPin()` action deliberately
* does not call Neon Auth (no `getSession()` / no `getAdminUser()`).
*
* If you need a similar portal for another brand, duplicate this
* page under a different slug (e.g. `/water/admin-tuxedo/login`) and
* swap in that brand's UUID.
*/
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
export default async function WaterAdminLoginPage() {