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:
@@ -80,6 +80,72 @@ describe("water-log/auth — module surface", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Module guard for the Tuxedo admin-PIN entry flow ───────────────────
|
||||
//
|
||||
// The `/water/admin/login` page is Tuxedo-brand-specific and PIN-only.
|
||||
// Users must be able to enter the admin PIN WITHOUT being signed into
|
||||
// the platform. If a future refactor re-adds a Neon Auth dependency
|
||||
// (getSession / getAdminUser) inside `verifyWaterAdminPin`, the PIN
|
||||
// submission 500s for unauthenticated field staff.
|
||||
//
|
||||
// Note: other functions in settings.ts (getWaterAdminSettings,
|
||||
// saveWaterAdminSettings, regenerateAdminPin) are called from the
|
||||
// platform-admin-gated `/admin/water-log/settings/*` UI and correctly
|
||||
// DO depend on `getAdminUser()`. Only `verifyWaterAdminPin` is the
|
||||
// PIN-only entry point.
|
||||
|
||||
describe("water-log/settings — verifyWaterAdminPin surface", () => {
|
||||
/**
|
||||
* Extract just the body of `verifyWaterAdminPin` from settings.ts so
|
||||
* the regression check is scoped to that function (and not falsely
|
||||
* tripped by sibling functions that legitimately use getAdminUser).
|
||||
*
|
||||
* The function is the LAST export in the file, so we read from
|
||||
* `export async function verifyWaterAdminPin` to EOF.
|
||||
*/
|
||||
async function getVerifyWaterAdminPinSource(): Promise<string> {
|
||||
const fs = await import("node:fs/promises");
|
||||
const path = await import("node:path");
|
||||
const raw = await fs.readFile(
|
||||
path.resolve(
|
||||
process.cwd(),
|
||||
"src/actions/water-log/settings.ts",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const idx = raw.indexOf("export async function verifyWaterAdminPin");
|
||||
if (idx === -1) throw new Error("verifyWaterAdminPin not found");
|
||||
return raw.slice(idx);
|
||||
}
|
||||
|
||||
function stripComments(src: string): string {
|
||||
return src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/^\s*\/\/.*$/gm, "")
|
||||
.replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
it("does not call getAdminUser inside verifyWaterAdminPin", async () => {
|
||||
const stripped = stripComments(await getVerifyWaterAdminPinSource());
|
||||
expect(stripped).not.toMatch(/\bgetAdminUser\s*\(/);
|
||||
});
|
||||
|
||||
it("does not call getSession inside verifyWaterAdminPin", async () => {
|
||||
const stripped = stripComments(await getVerifyWaterAdminPinSource());
|
||||
expect(stripped).not.toMatch(/\bgetSession\s*\(/);
|
||||
});
|
||||
|
||||
it("always inserts the zero-UUID fallback adminUserId", async () => {
|
||||
const stripped = stripComments(await getVerifyWaterAdminPinSource());
|
||||
// Confirms the session row is inserted with the placeholder UUID
|
||||
// — i.e. the function no longer conditions adminUserId on a
|
||||
// successful platform-admin lookup.
|
||||
expect(stripped).toMatch(
|
||||
/00000000-0000-0000-0000-000000000000/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── requireFieldSession ─────────────────────────────────────────────────
|
||||
|
||||
describe("requireFieldSession()", () => {
|
||||
|
||||
Reference in New Issue
Block a user