refactor(water-log): centralize brand + session primitives
Deploy to route.crispygoat.com / deploy (push) Successful in 4m19s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m19s
Phase 1 of the water-log auth refactor. No behavior change.
NEW:
- src/lib/water-log/brand.ts — single source of TUXEDO_BRAND_ID
- src/lib/water-log/session.ts — cookie name constants + shared types
(FieldSession, AdminSession, FieldSessionAuthed,
FieldSessionResult). Safe to import from client
components.
- src/lib/water-log/session-server.ts — server-only cookie I/O helpers
(setSessionCookie, clearSessionCookie,
readSessionCookie).
MODIFIED:
- src/lib/water-admin-pin-auth.ts — re-export TUXEDO_BRAND_ID, use helpers
- src/actions/water-log/auth.ts — use shared cookie constant + types
- src/actions/water-log/field.ts — drop local TUXEDO_BRAND_ID copy
- src/app/api/water-photo-upload/route.ts — use shared cookie names
- src/components/water/mobile/MobileWaterApp.tsx — drop local
TUXEDO_BRAND_ID copy;
use string ops over
regex for cookie peek.
FieldSession retains its union shape (back-compat) — FieldSessionAuthed
is the narrowed success branch.
This commit is contained in:
@@ -34,12 +34,19 @@ import {
|
|||||||
waterIrrigators,
|
waterIrrigators,
|
||||||
} from "@/db/schema/water-log";
|
} from "@/db/schema/water-log";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
|
import {
|
||||||
|
WL_SESSION_COOKIE,
|
||||||
|
type FieldSession,
|
||||||
|
type FieldSessionResult,
|
||||||
|
} from "@/lib/water-log/session-server";
|
||||||
|
|
||||||
// ── Result types ────────────────────────────────────────────────────────
|
// ── Result types ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export type FieldSession =
|
// `FieldSession` is now defined in `@/lib/water-log/session.ts` so the
|
||||||
| { ok: true; userId: string; brandId: string; role: "irrigator" | "water_admin" }
|
// `FieldSession | { ok: false, error }` shape is shared with
|
||||||
| { ok: false; error: string };
|
// `lib/water-log/pin-login.ts` (Phase 2) and the rest of the module
|
||||||
|
// surface. Re-export for back-compat with existing call sites.
|
||||||
|
export type { FieldSession };
|
||||||
|
|
||||||
export type SiteAdminPermission =
|
export type SiteAdminPermission =
|
||||||
| { ok: true; adminUser: NonNullable<Awaited<ReturnType<typeof getAdminUser>>> }
|
| { ok: true; adminUser: NonNullable<Awaited<ReturnType<typeof getAdminUser>>> }
|
||||||
@@ -63,9 +70,9 @@ export type SiteAdminPermission =
|
|||||||
* Never calls `getSession()`. Never calls `getAdminUser()`. The only
|
* Never calls `getSession()`. Never calls `getAdminUser()`. The only
|
||||||
* network round-trip is the local DB query.
|
* network round-trip is the local DB query.
|
||||||
*/
|
*/
|
||||||
export async function requireFieldSession(): Promise<FieldSession> {
|
export async function requireFieldSession(): Promise<FieldSessionResult> {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const sessionId = cookieStore.get("wl_session")?.value;
|
const sessionId = cookieStore.get(WL_SESSION_COOKIE)?.value;
|
||||||
if (!sessionId) return { ok: false, error: "Not logged in" };
|
if (!sessionId) return { ok: false, error: "Not logged in" };
|
||||||
|
|
||||||
return withPlatformAdmin(async (db) => {
|
return withPlatformAdmin(async (db) => {
|
||||||
|
|||||||
@@ -34,13 +34,21 @@ import {
|
|||||||
requireFieldSession,
|
requireFieldSession,
|
||||||
type FieldSession,
|
type FieldSession,
|
||||||
} from "@/actions/water-log/auth";
|
} from "@/actions/water-log/auth";
|
||||||
|
import {
|
||||||
|
WL_SESSION_COOKIE,
|
||||||
|
clearSessionCookie,
|
||||||
|
readSessionCookie,
|
||||||
|
setSessionCookie,
|
||||||
|
} from "@/lib/water-log/session-server";
|
||||||
|
import { TUXEDO_BRAND_ID } from "@/lib/water-log/brand";
|
||||||
|
|
||||||
|
// Re-export so existing call sites that import from
|
||||||
|
// `@/actions/water-log/field` keep working.
|
||||||
export type { FieldSession };
|
export type { FieldSession };
|
||||||
|
|
||||||
// Field sessions last 8h — a working day in the field. Long enough
|
// Field sessions last 8h — a working day in the field. Long enough
|
||||||
// that a single sign-in covers a morning shift + afternoon shift.
|
// that a single sign-in covers a morning shift + afternoon shift.
|
||||||
const FIELD_SESSION_HOURS = 8;
|
const FIELD_SESSION_HOURS = 8;
|
||||||
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
|
||||||
|
|
||||||
// ── Types ──────────────────────────────────────────────────────────────────
|
// ── Types ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -177,13 +185,8 @@ export async function verifyWaterPin(
|
|||||||
|
|
||||||
// Cookie set is fully outside the DB transaction. Same rule as
|
// Cookie set is fully outside the DB transaction. Same rule as
|
||||||
// `verifyAdminPin` (see comment in `src/lib/water-admin-pin-auth.ts`).
|
// `verifyAdminPin` (see comment in `src/lib/water-admin-pin-auth.ts`).
|
||||||
const cookieStore = await cookies();
|
await setSessionCookie(WL_SESSION_COOKIE, sessionId, {
|
||||||
cookieStore.set("wl_session", sessionId, {
|
hours: FIELD_SESSION_HOURS,
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
sameSite: "lax",
|
|
||||||
maxAge: FIELD_SESSION_HOURS * 3600,
|
|
||||||
path: "/",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -306,8 +309,7 @@ export async function submitWaterEntry(
|
|||||||
// ── Cookie/language helpers ───────────────────────────────────────────────
|
// ── Cookie/language helpers ───────────────────────────────────────────────
|
||||||
|
|
||||||
export async function logoutWater(): Promise<void> {
|
export async function logoutWater(): Promise<void> {
|
||||||
const cookieStore = await cookies();
|
const sessionId = await readSessionCookie(WL_SESSION_COOKIE);
|
||||||
const sessionId = cookieStore.get("wl_session")?.value;
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
// Best-effort DB cleanup
|
// Best-effort DB cleanup
|
||||||
try {
|
try {
|
||||||
@@ -318,7 +320,7 @@ export async function logoutWater(): Promise<void> {
|
|||||||
// ignore — the cookie is being deleted anyway
|
// ignore — the cookie is being deleted anyway
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cookieStore.delete("wl_session");
|
await clearSessionCookie(WL_SESSION_COOKIE);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logoutWaterAdmin(): Promise<void> {
|
export async function logoutWaterAdmin(): Promise<void> {
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { uploadObject, BUCKETS } from "@/lib/storage";
|
import { uploadObject, BUCKETS } from "@/lib/storage";
|
||||||
|
import {
|
||||||
|
WL_ADMIN_SESSION_COOKIE,
|
||||||
|
WL_SESSION_COOKIE,
|
||||||
|
} from "@/lib/water-log/session";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
// Validate session — irrigators use wl_session, admins use wl_admin_session
|
// Validate session — irrigators use wl_session, admins use wl_admin_session
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const sessionId = cookieStore.get("wl_session")?.value ?? cookieStore.get("wl_admin_session")?.value;
|
const sessionId =
|
||||||
|
cookieStore.get(WL_SESSION_COOKIE)?.value ??
|
||||||
|
cookieStore.get(WL_ADMIN_SESSION_COOKIE)?.value;
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ import { MobileWaterPinScreen } from "./PinScreen";
|
|||||||
import { MobileWaterHeadgateList } from "./HeadgateList";
|
import { MobileWaterHeadgateList } from "./HeadgateList";
|
||||||
import { MobileWaterLogForm } from "./LogForm";
|
import { MobileWaterLogForm } from "./LogForm";
|
||||||
import { MobileWaterSuccessScreen } from "./SuccessScreen";
|
import { MobileWaterSuccessScreen } from "./SuccessScreen";
|
||||||
|
import { WL_SESSION_COOKIE } from "@/lib/water-log/session";
|
||||||
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
import { TUXEDO_BRAND_ID } from "@/lib/water-log/brand";
|
||||||
|
|
||||||
export function MobileWaterApp() {
|
export function MobileWaterApp() {
|
||||||
const [screen, setScreen] = useState<Screen>("pin");
|
const [screen, setScreen] = useState<Screen>("pin");
|
||||||
@@ -79,7 +79,9 @@ export function MobileWaterApp() {
|
|||||||
// so SSR markup matches (avoids hydration warnings). ────────
|
// so SSR markup matches (avoids hydration warnings). ────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof document === "undefined") return;
|
if (typeof document === "undefined") return;
|
||||||
const hasSession = document.cookie.match(/wl_session=([^;]+)/);
|
const hasSession = document.cookie
|
||||||
|
.split(";")
|
||||||
|
.some((c) => c.trim().startsWith(`${WL_SESSION_COOKIE}=`));
|
||||||
if (!hasSession) return;
|
if (!hasSession) return;
|
||||||
// Wrap setScreen in a microtask so the call doesn't fire
|
// Wrap setScreen in a microtask so the call doesn't fire
|
||||||
// synchronously inside the effect body.
|
// synchronously inside the effect body.
|
||||||
|
|||||||
@@ -24,25 +24,28 @@
|
|||||||
* - `logoutWaterAdmin` in field.ts (sign-out)
|
* - `logoutWaterAdmin` in field.ts (sign-out)
|
||||||
*/
|
*/
|
||||||
import "server-only";
|
import "server-only";
|
||||||
import { cookies } from "next/headers";
|
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { withBrand } from "@/db/client";
|
import { withBrand } from "@/db/client";
|
||||||
import { waterAdminSettings } from "@/db/schema/water-log";
|
import { waterAdminSettings } from "@/db/schema/water-log";
|
||||||
import { hashPin, verifyPin, validatePin } from "@/lib/water-log-pin";
|
import { hashPin, verifyPin, validatePin } from "@/lib/water-log-pin";
|
||||||
|
import {
|
||||||
|
WL_ADMIN_SESSION_COOKIE,
|
||||||
|
type AdminSession,
|
||||||
|
clearSessionCookie,
|
||||||
|
readSessionCookie,
|
||||||
|
setSessionCookie,
|
||||||
|
} from "@/lib/water-log/session-server";
|
||||||
|
|
||||||
/** Tuxedo brand UUID — hardcoded because this portal is Tuxedo-only. */
|
// Re-export for back-compat — `TUXEDO_BRAND_ID` is now defined in
|
||||||
export const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
// `@/lib/water-log/brand.ts`. The water-log folder is the single
|
||||||
|
// source of truth for the Tuxedo-only hardcoding.
|
||||||
|
import { TUXEDO_BRAND_ID } from "@/lib/water-log/brand";
|
||||||
|
export { TUXEDO_BRAND_ID };
|
||||||
|
|
||||||
/** Session lifetime, in hours, read from `water_admin_settings.session_duration_hours`. */
|
/** Session lifetime, in hours, read from `water_admin_settings.session_duration_hours`. */
|
||||||
const DEFAULT_SESSION_HOURS = 8;
|
const DEFAULT_SESSION_HOURS = 8;
|
||||||
|
|
||||||
/** Result of a successful PIN verification. */
|
|
||||||
export type AdminSession = {
|
|
||||||
sessionId: string;
|
|
||||||
brandId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify the admin PIN against the DB-stored scrypt hash. On success,
|
* Verify the admin PIN against the DB-stored scrypt hash. On success,
|
||||||
* sets the `wl_admin_session` cookie to a random opaque bearer token
|
* sets the `wl_admin_session` cookie to a random opaque bearer token
|
||||||
@@ -88,16 +91,11 @@ export async function verifyAdminPin(
|
|||||||
|
|
||||||
if (!result.ok) return result;
|
if (!result.ok) return result;
|
||||||
|
|
||||||
// Set the cookie OUTSIDE the DB transaction (cookies().set()
|
// Cookie set is OUTSIDE the DB transaction. (cookies().set() corrupts
|
||||||
// corrupts Next.js's request-scoped AsyncLocalStorage if called from
|
// Next.js's request-scoped AsyncLocalStorage if called from inside a
|
||||||
// inside a pg transaction).
|
// pg transaction — see commit ca79896 and `lib/water-log/session.ts`.)
|
||||||
const cookieStore = await cookies();
|
await setSessionCookie(WL_ADMIN_SESSION_COOKIE, sessionId, {
|
||||||
cookieStore.set("wl_admin_session", sessionId, {
|
hours: sessionDurationHours,
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
sameSite: "lax",
|
|
||||||
maxAge: sessionDurationHours * 3600,
|
|
||||||
path: "/",
|
|
||||||
});
|
});
|
||||||
return { ok: true, session: { sessionId, brandId: TUXEDO_BRAND_ID } };
|
return { ok: true, session: { sessionId, brandId: TUXEDO_BRAND_ID } };
|
||||||
}
|
}
|
||||||
@@ -108,16 +106,14 @@ export async function verifyAdminPin(
|
|||||||
* session table — the cookie itself is the session.
|
* session table — the cookie itself is the session.
|
||||||
*/
|
*/
|
||||||
export async function getAdminSession(): Promise<AdminSession | null> {
|
export async function getAdminSession(): Promise<AdminSession | null> {
|
||||||
const cookieStore = await cookies();
|
const sessionId = await readSessionCookie(WL_ADMIN_SESSION_COOKIE);
|
||||||
const sessionId = cookieStore.get("wl_admin_session")?.value;
|
|
||||||
if (!sessionId) return null;
|
if (!sessionId) return null;
|
||||||
return { sessionId, brandId: TUXEDO_BRAND_ID };
|
return { sessionId, brandId: TUXEDO_BRAND_ID };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear the admin session cookie. Idempotent. */
|
/** Clear the admin session cookie. Idempotent. */
|
||||||
export async function clearAdminSession(): Promise<void> {
|
export async function clearAdminSession(): Promise<void> {
|
||||||
const cookieStore = await cookies();
|
await clearSessionCookie(WL_ADMIN_SESSION_COOKIE);
|
||||||
cookieStore.delete("wl_admin_session");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export `hashPin` so callers (e.g. a future "set PIN" UI) can
|
// Re-export `hashPin` so callers (e.g. a future "set PIN" UI) can
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Water Log — brand constants.
|
||||||
|
*
|
||||||
|
* The water-log module is currently single-tenant (Tuxedo only — see
|
||||||
|
* CLAUDE.md "Public Storefront Architecture"). That hardcoding is
|
||||||
|
* intentional, but having the UUID duplicated across files invites
|
||||||
|
* drift and makes it easy to forget the constant when adding a new
|
||||||
|
* code path. This file is the single source of truth.
|
||||||
|
*
|
||||||
|
* Lifting this to multi-tenant will mean replacing every read of
|
||||||
|
* `TUXEDO_BRAND_ID` with `effectiveBrandId` from `getAdminUser()` —
|
||||||
|
* which is the standard pattern used throughout the rest of the app.
|
||||||
|
*/
|
||||||
|
export const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* Water Log — server-side cookie I/O.
|
||||||
|
*
|
||||||
|
* Companion to `./session.ts` (which holds the cookie names + types).
|
||||||
|
* **Server-only** — every helper calls `cookies()` from
|
||||||
|
* `next/headers`, which is not available in client components.
|
||||||
|
*
|
||||||
|
* The "cookie set must happen outside the pg transaction" invariant
|
||||||
|
* (documented in commit `ca79896` and `src/lib/water-admin-pin-auth.ts`)
|
||||||
|
* is preserved by the shape of `setSessionCookie` and
|
||||||
|
* `clearSessionCookie` below — they are pure cookie I/O and never
|
||||||
|
* touch a database.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "server-only";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import {
|
||||||
|
WL_ADMIN_SESSION_COOKIE,
|
||||||
|
WL_SESSION_COOKIE,
|
||||||
|
type SessionCookieName,
|
||||||
|
} from "./session";
|
||||||
|
|
||||||
|
export { WL_SESSION_COOKIE, WL_ADMIN_SESSION_COOKIE } from "./session";
|
||||||
|
export type {
|
||||||
|
SessionCookieName,
|
||||||
|
FieldSession,
|
||||||
|
FieldSessionError,
|
||||||
|
FieldSessionResult,
|
||||||
|
AdminSession,
|
||||||
|
} from "./session";
|
||||||
|
|
||||||
|
export type SessionCookieOptions = {
|
||||||
|
/** Lifetime in hours. Translated to `maxAge` in seconds for `cookies.set`. */
|
||||||
|
hours: number;
|
||||||
|
/** Optional override for `secure`. Defaults to NODE_ENV === "production". */
|
||||||
|
secure?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_COOKIE_PATH = "/";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a session cookie. **Must be called outside any pg transaction.**
|
||||||
|
*
|
||||||
|
* Calling `cookies().set()` from inside a pg transaction corrupts
|
||||||
|
* Next.js's request-scoped AsyncLocalStorage — that's the bug that
|
||||||
|
* commit `ca79896` fixed in the admin portal and that the field portal
|
||||||
|
* is being brought into line with.
|
||||||
|
*/
|
||||||
|
export async function setSessionCookie(
|
||||||
|
name: SessionCookieName,
|
||||||
|
value: string,
|
||||||
|
opts: SessionCookieOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
cookieStore.set(name, value, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: opts.secure ?? process.env.NODE_ENV === "production",
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: opts.hours * 3600,
|
||||||
|
path: DEFAULT_COOKIE_PATH,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear a session cookie. Idempotent. Safe to call inside or outside a tx. */
|
||||||
|
export async function clearSessionCookie(name: SessionCookieName): Promise<void> {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
cookieStore.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the value of a session cookie (or null). Read-only.
|
||||||
|
*
|
||||||
|
* Note: `httpOnly` cookies set via `setSessionCookie` are NOT visible
|
||||||
|
* to `document.cookie` in the browser. This is intentional — it means
|
||||||
|
* XSS can't steal the session token. Client components that need to
|
||||||
|
* know "is the user signed in?" must round-trip a server action,
|
||||||
|
* not sniff `document.cookie`.
|
||||||
|
*/
|
||||||
|
export async function readSessionCookie(
|
||||||
|
name: SessionCookieName,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
return cookieStore.get(name)?.value ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience wrapper for the "is the user signed in?" check on the
|
||||||
|
* field portal — reads `wl_session`. Returns the session id (the
|
||||||
|
* `water_sessions.id` row UUID) or null.
|
||||||
|
*/
|
||||||
|
export async function readFieldSessionId(): Promise<string | null> {
|
||||||
|
return readSessionCookie(WL_SESSION_COOKIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience wrapper for the admin portal — reads `wl_admin_session`.
|
||||||
|
*/
|
||||||
|
export async function readAdminSessionId(): Promise<string | null> {
|
||||||
|
return readSessionCookie(WL_ADMIN_SESSION_COOKIE);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Water Log — session constants + shared types.
|
||||||
|
*
|
||||||
|
* This file is **safe to import from client components** — it has
|
||||||
|
* no server-only side effects (no `next/headers`, no `cookies()`).
|
||||||
|
* It just exports the cookie name strings and the public session
|
||||||
|
* result types so both client and server code can refer to them.
|
||||||
|
*
|
||||||
|
* The actual cookie I/O helpers (`setSessionCookie`,
|
||||||
|
* `clearSessionCookie`, `readSessionCookie`) live in
|
||||||
|
* `./session-server.ts` because they call `cookies()` from
|
||||||
|
* `next/headers`, which is server-only.
|
||||||
|
*
|
||||||
|
* Two session cookies live in this module's surface area:
|
||||||
|
*
|
||||||
|
* - `wl_session` — DB-backed row in `water_sessions`. Set by
|
||||||
|
* `verifyWaterPin` on a successful irrigator
|
||||||
|
* PIN. Server-side expiry is enforced by
|
||||||
|
* `requireFieldSession`.
|
||||||
|
*
|
||||||
|
* - `wl_admin_session` — cookie-only, no DB row. Set by
|
||||||
|
* `verifyAdminPin` on a successful admin PIN.
|
||||||
|
* Cookie expiry (`maxAge`) is the source of
|
||||||
|
* truth for session lifetime.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const WL_SESSION_COOKIE = "wl_session";
|
||||||
|
export const WL_ADMIN_SESSION_COOKIE = "wl_admin_session";
|
||||||
|
|
||||||
|
export type SessionCookieName =
|
||||||
|
| typeof WL_SESSION_COOKIE
|
||||||
|
| typeof WL_ADMIN_SESSION_COOKIE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `FieldSession` is the discriminated union result of `requireFieldSession()`.
|
||||||
|
* Use `FieldSessionAuthed` (the `ok: true` branch) when you've already
|
||||||
|
* narrowed via the `ok` discriminator. The legacy shape — where
|
||||||
|
* `FieldSession` itself was the union — is preserved here for callers
|
||||||
|
* that imported it from `actions/water-log/auth.ts`.
|
||||||
|
*/
|
||||||
|
export type FieldSessionError = { ok: false; error: string };
|
||||||
|
|
||||||
|
export type FieldSession = FieldSessionAuthed | FieldSessionError;
|
||||||
|
|
||||||
|
/** Narrowed success-branch type, useful for parameter types after a guard. */
|
||||||
|
export type FieldSessionAuthed = {
|
||||||
|
ok: true;
|
||||||
|
userId: string;
|
||||||
|
brandId: string;
|
||||||
|
role: "irrigator" | "water_admin";
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Back-compat alias for the union. Identical to `FieldSession`. */
|
||||||
|
export type FieldSessionResult = FieldSession;
|
||||||
|
|
||||||
|
/** Admin session (cookie-only bearer token). */
|
||||||
|
export type AdminSession = {
|
||||||
|
sessionId: string;
|
||||||
|
brandId: string;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user