diff --git a/apps/web/app/admin/audit/[id]/page.tsx b/apps/web/app/admin/audit/[id]/page.tsx index fbc2634a..e48e4130 100644 --- a/apps/web/app/admin/audit/[id]/page.tsx +++ b/apps/web/app/admin/audit/[id]/page.tsx @@ -9,6 +9,7 @@ import { formatAdminTimestamp, } from "@/components/admin-ui"; import { requireOwnerUser } from "@/lib/auth"; +import { isRouteUuid } from "@/lib/input-validation"; type AdminAuditDetailPageProps = { params: Promise<{ id: string }>; @@ -50,6 +51,9 @@ export default async function AdminAuditDetailPage({ }: AdminAuditDetailPageProps) { const ownerUser = await requireOwnerUser(); const { id } = await params; + if (!isRouteUuid(id)) { + notFound(); + } const event = await getAdminAuditEventById(ownerUser.id, id); if (!event) { diff --git a/apps/web/app/admin/barcodes/[id]/page.tsx b/apps/web/app/admin/barcodes/[id]/page.tsx index 8c8b60b6..34994996 100644 --- a/apps/web/app/admin/barcodes/[id]/page.tsx +++ b/apps/web/app/admin/barcodes/[id]/page.tsx @@ -14,6 +14,7 @@ import { updateAdminBarcodeProductAction, } from "@/lib/admin-actions"; import { requireAdminUser } from "@/lib/auth"; +import { isRouteUuid } from "@/lib/input-validation"; type AdminBarcodeDetailPageProps = { params: Promise<{ id: string }>; @@ -29,6 +30,9 @@ export default async function AdminBarcodeDetailPage({ }: AdminBarcodeDetailPageProps) { const adminUser = await requireAdminUser(); const [{ id }, query] = await Promise.all([params, searchParams]); + if (!isRouteUuid(id)) { + notFound(); + } const [barcode, audit] = await Promise.all([ getAdminBarcodeProductById(adminUser.id, id), listAdminAuditEvents(adminUser.id, { diff --git a/apps/web/app/admin/users/[userId]/page.tsx b/apps/web/app/admin/users/[userId]/page.tsx index 253669cc..9de2680b 100644 --- a/apps/web/app/admin/users/[userId]/page.tsx +++ b/apps/web/app/admin/users/[userId]/page.tsx @@ -13,6 +13,7 @@ import { import { ConfirmSubmitButton } from "@/components/confirm-delete-button"; import { changeUserRoleAction } from "@/lib/admin-actions"; import { requireAdminUser } from "@/lib/auth"; +import { isRouteUuid } from "@/lib/input-validation"; import { getTemplateMacroTotals } from "@/lib/template-macros"; type AdminUserDetailPageProps = { @@ -46,6 +47,9 @@ export default async function AdminUserDetailPage({ }: AdminUserDetailPageProps) { const adminUser = await requireAdminUser(); const [{ userId }, query] = await Promise.all([params, searchParams]); + if (!isRouteUuid(userId)) { + notFound(); + } const detail = await getAdminUserDetail(adminUser.id, userId); if (!detail) { diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index ec34fab7..bd0e628c 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -8,6 +8,7 @@ import { LayoutNav } from "@/components/layout-nav"; import { OfflineBanner } from "@/components/offline-banner"; import { OrientationLock } from "@/components/orientation-lock"; import { ServiceWorkerRegister } from "@/components/service-worker-register"; +import { getRequestTodayFromHeaders } from "@/lib/server-timezone"; import { TIMEZONE_COOKIE_MAX_AGE_SECONDS, TIMEZONE_COOKIE_NAME, @@ -83,7 +84,11 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const nonce = (await headers()).get(NONCE_HEADER) ?? undefined; + const requestHeaders = await headers(); + const nonce = requestHeaders.get(NONCE_HEADER) ?? undefined; + // Stable per-request day snapshot for the bottom nav so SSR and the + // first client render agree; the browser corrects post-hydration. + const today = getRequestTodayFromHeaders(requestHeaders); return (