Files
route-commerce/src/app/admin/water-log/headgates/[id]/page.tsx
T
Tyler 658f6a5b8b
Deploy to route.crispygoat.com / deploy (push) Successful in 4m12s
fix(water-log): remove platform-login dependency from PIN flows
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.
2026-07-01 17:27:37 -06:00

109 lines
3.9 KiB
TypeScript

import { getAdminUser } from "@/lib/admin-permissions";
import { redirect } from "next/navigation";
import { getWaterAdminSession } from "@/actions/water-log/auth";
import { getWaterHeadgatesAdmin } from "@/actions/water-log/admin";
import HeadgateEditForm from "@/components/admin/HeadgateEditForm";
import Link from "next/link";
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
export const dynamic = "force-dynamic";
type HeadgatePageProps = {
params: Promise<{ id: string }>;
searchParams: Promise<{ from?: string }>;
};
export default async function WaterLogHeadgatePage({ params, searchParams }: HeadgatePageProps) {
const [{ id }, { from }, adminUser, waterSession] = await Promise.all([
params,
searchParams,
getAdminUser(),
getWaterAdminSession(),
]);
const isSiteAdmin =
adminUser?.role === "platform_admin" ||
(adminUser?.role === "brand_admin" &&
adminUser?.brand_id === TUXEDO_BRAND_ID &&
adminUser?.can_manage_water_log);
const isWaterAdmin = waterSession !== null && waterSession.role === "water_admin";
if (!isSiteAdmin && !isWaterAdmin) redirect("/admin/pickup");
const headgates = await getWaterHeadgatesAdmin(TUXEDO_BRAND_ID);
const headgate = headgates.find((h) => h.id === id);
if (!headgate) {
return (
<main className="min-h-screen bg-zinc-950 px-6 py-12">
<div className="mx-auto max-w-4xl">
<h1 className="text-3xl font-bold text-red-400">Headgate not found</h1>
<p className="mt-2 text-zinc-400">This headgate may have been deleted.</p>
<a href={from ?? "/admin/water-log"} className="mt-4 inline-block text-zinc-400 hover:text-zinc-100">
Back
</a>
</div>
</main>
);
}
const backHref = from ?? "/admin/water-log";
return (
<main className="min-h-screen bg-zinc-950 px-6 py-12">
<div className="mx-auto max-w-4xl">
<Link
href={backHref}
className="text-sm text-zinc-500 hover:text-zinc-300"
>
Back
</Link>
<div className="mt-6 rounded-2xl bg-zinc-900 p-8 shadow-black/20 ring-1 ring-zinc-700">
<div className="flex items-start justify-between">
<div>
<p className="text-sm font-semibold uppercase tracking-wide text-zinc-500">Water Headgate</p>
<h1 className="mt-2 text-3xl font-bold text-zinc-100">{headgate.name}</h1>
</div>
<span
className={`shrink-0 rounded-full px-3 py-1 text-xs font-bold uppercase tracking-wide ${
headgate.active ? "bg-green-900/40 text-green-400" : "bg-zinc-950 text-zinc-500"
}`}
>
{headgate.active ? "Active" : "Inactive"}
</span>
</div>
<div className="mt-6 grid grid-cols-2 gap-6">
<div>
<p className="text-sm font-medium text-zinc-500">Default Unit</p>
<p className="mt-1 text-2xl font-bold text-zinc-100">{headgate.unit}</p>
</div>
<div>
<p className="text-sm font-medium text-zinc-500">Created</p>
<p className="mt-1 text-lg font-semibold text-zinc-100">
{new Date(headgate.created_at).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})}
</p>
</div>
</div>
</div>
<div className="mt-6 rounded-2xl bg-zinc-900 p-8 shadow-black/20 ring-1 ring-zinc-700">
<h2 className="text-2xl font-bold text-zinc-100">Edit Headgate</h2>
<p className="mt-1 text-zinc-400">
Update the headgate name, status, and default measurement unit.
</p>
<div className="mt-6">
<HeadgateEditForm headgate={headgate} backHref={backHref} />
</div>
</div>
</div>
</main>
);
}