fix(water-log): remove nested Promise.all in field PIN login
Deploy to route.crispygoat.com / deploy (push) Successful in 5m3s

Replace nested Promise.all([insert, update]) + outer
Promise.all([withBrand, cookies()]) with sequential awaits in
verifyWaterPin, mirroring the admin-portal pattern from commit
ca79896 and src/lib/water-admin-pin-auth.ts.

Symptom: field PIN works once, fails after logout until reset.
Root cause is the same family flagged in ca79896: cookies()
corrupts Next.js's request-scoped AsyncLocalStorage when called
from inside a pg transaction, and nested Promise.all inside
the transaction makes the failure non-deterministic.

Also adds scripts/diag-water-pin.mjs for one-shot prod DB
inspection if the symptom persists post-deploy.
This commit is contained in:
Tyler
2026-07-02 10:32:20 -06:00
parent ce62b17898
commit 69d8f829f1
2 changed files with 192 additions and 26 deletions
+34 -26
View File
@@ -144,32 +144,40 @@ export async function verifyWaterPin(
}
// Create a session in the user's brand context. The session insert
// and the last_used_at bump are independent writes — fire them in
// parallel and wait on both before returning.
const [sessionId, cookieStore] = await Promise.all([
withBrand(match.brandId, async (db) => {
const expiresAt = new Date(
Date.now() + FIELD_SESSION_HOURS * 60 * 60 * 1000,
);
const [insertResult] = await Promise.all([
db
.insert(waterSessions)
.values({
irrigatorId: match.id,
expiresAt,
})
.returning({ id: waterSessions.id }),
db
.update(waterIrrigators)
.set({ lastUsedAt: new Date() })
.where(eq(waterIrrigators.id, match.id)),
]);
const row = insertResult[0];
if (!row) throw new Error("Failed to create session");
return row.id;
}),
cookies(),
]);
// and the last_used_at bump run *sequentially* on the same pg
// connection inside the `withBrand` transaction — never concurrently.
//
// The previous shape used a nested `Promise.all([insert, update])`
// *and* an outer `Promise.all([withBrand, cookies()])`. That was
// known-fragile per the codebase's own commit `ca79896`: "cookies()
// corrupts Next.js's request-scoped AsyncLocalStorage if called from
// inside a pg transaction." The admin portal has been on the
// sequential shape (see `src/lib/water-admin-pin-auth.ts`) since
// that commit; this brings the field portal in line.
const sessionId = await withBrand(match.brandId, async (db) => {
const expiresAt = new Date(
Date.now() + FIELD_SESSION_HOURS * 60 * 60 * 1000,
);
const [inserted] = await db
.insert(waterSessions)
.values({
irrigatorId: match.id,
expiresAt,
})
.returning({ id: waterSessions.id });
if (!inserted) throw new Error("Failed to create session");
await db
.update(waterIrrigators)
.set({ lastUsedAt: new Date() })
.where(eq(waterIrrigators.id, match.id));
return inserted.id;
});
// Cookie set is fully outside the DB transaction. Same rule as
// `verifyAdminPin` (see comment in `src/lib/water-admin-pin-auth.ts`).
const cookieStore = await cookies();
cookieStore.set("wl_session", sessionId, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",