diff --git a/next.config.ts b/next.config.ts index 86d29ea..d626a76 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,11 +10,27 @@ const nextConfig: NextConfig = { // through webpack's loader — so pdfjs-dist's init runs in a clean CJS // context, the way its author expects. serverExternalPackages: ["pdf-parse", "pdfjs-dist", "@napi-rs/canvas"], - // Dev server blocks cross-origin HMR requests by default in Next 16. The - // app is normally loaded on `localhost`, but we also load it on - // `127.0.0.1` (and on the LAN IP for phone testing). Listing both keeps - // the HMR client from hanging on the first load. - allowedDevOrigins: ["127.0.0.1", "localhost", "192.168.1.42", "192.168.1.122", "192.168.1.128", "coolify.mywire.org", "*"], + // Dev server blocks cross-origin requests to its own dev resources by + // default in Next 16, which shows up as a page stuck on the loading + // spinner when you open the app from your phone on the LAN. + // + // IMPORTANT — this list is matched with `isCsrfOriginAllowed`, which + // splits the host on "." and compares label by label, treating `*` as + // exactly ONE label. Two consequences, both learned the hard way: + // + // - A bare "*" is useless. It does not mean "any host" — checked + // against Next 16.2.6, isCsrfOriginAllowed("192.168.1.35", ["*"]) + // is false. It was in this list for months doing nothing. + // - "192.168.*" is also useless for the same reason: it is two + // labels against four. + // - "192.168.1.*" DOES work, and survives the router handing out a + // different address on a new DHCP lease. The previous version + // hard-coded individual IPs (.42, .122, .128); when the lease + // moved to .35 the phone silently stopped loading the app. + // + // So: wildcard the last label of the LAN subnet, keep the real + // hostname, and leave the stale per-address entries out. + allowedDevOrigins: ["127.0.0.1", "localhost", "192.168.1.*", "coolify.mywire.org"], // The agent (and any external tools) sometimes reach for the PocketBase // collection name verbatim — `order_items`, `stock_items`, `quote_items` // — and get a 404 because the route folders use hyphens (`order-items`). diff --git a/src/app/scan/auth-gate.tsx b/src/app/scan/auth-gate.tsx new file mode 100644 index 0000000..1b9f5c8 --- /dev/null +++ b/src/app/scan/auth-gate.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import { useAuth } from "@/components/auth-provider"; +import { isSafeNextPath } from "@/lib/safe-next-path"; + +/** + * Auth gate for the standalone /scan PWA. + * + * `(app)/layout.tsx` is the app's auth gate — it redirects an + * unauthenticated user to /login. /scan is a separate route group and + * inherits only the ROOT layout, which provides `AuthProvider` — and + * `AuthProvider` merely EXPOSES the session, it never renders a login + * form. So without this gate the page rendered the scanner for anyone + * and the first API call came back "Not authenticated." with no way + * forward from the page: the operator had to guess that `/login` + * existed somewhere else in the app. + * + * `/login?next=/scan` brings them straight back here after signing in; + * `(auth)/login/page.tsx` already honours `next`, and the value is run + * through `isSafeNextPath` so it can only ever be a same-origin path. + * + * Lives in its own file rather than in `layout.tsx` because `metadata` + * cannot be exported from a "use client" module. + */ +export function ScanAuthGate({ children }: { children: React.ReactNode }) { + const { user, loading } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!loading && !user) { + const next = isSafeNextPath("/scan") ? "/scan" : "/dashboard"; + router.push(`/login?next=${encodeURIComponent(next)}`); + } + }, [user, loading, router]); + + if (loading) { + return ( +
+
+
+ ); + } + + if (!user) return null; + + return <>{children}; +} diff --git a/src/app/scan/layout.tsx b/src/app/scan/layout.tsx index 58818d0..e9639ec 100644 --- a/src/app/scan/layout.tsx +++ b/src/app/scan/layout.tsx @@ -1,10 +1,15 @@ import type { Metadata, Viewport } from "next"; +import { ScanAuthGate } from "./auth-gate"; /** * /scan is its own route group (outside `(app)`) so it gets a * dedicated PWA manifest — "add to home screen" should give the - * operator a scanner, not the full CRM. Mirrors the pattern already + * operator a scanner, not the CRM. Mirrors the pattern already * used by `(counter)` for `/manifest-counter.json`. + * + * Stays a Server Component because `metadata` cannot be exported + * from a `"use client"` module. The auth gate is a separate client + * component — see `./auth-gate.tsx` for why it is needed at all. */ export const metadata: Metadata = { title: "DN Scanner · WFDASH", @@ -20,5 +25,5 @@ export const viewport: Viewport = { }; export default function ScanLayout({ children }: { children: React.ReactNode }) { - return children; + return {children}; } diff --git a/src/app/scan/page.tsx b/src/app/scan/page.tsx index ea986f1..50efc5d 100644 --- a/src/app/scan/page.tsx +++ b/src/app/scan/page.tsx @@ -361,11 +361,13 @@ export default function ScanPage() {

- {support?.camera - ? note - ? "Camera off — press Start camera to scan the next note" - : "Camera is off" - : "This browser will not give the page a camera"} + {support?.secureContext === false + ? "Camera blocked — this page is not on HTTPS" + : support?.camera + ? note + ? "Camera off — press Start camera to scan the next note" + : "Camera is off" + : "This device has no camera the browser will share"}

) : null} @@ -567,9 +569,17 @@ export default function ScanPage() { ) : (
{support && !support.secureContext ? ( -

- This page is not on HTTPS, so the browser blocks the camera. Use the office - network's secure address, or type the number below. +

+ Camera is unavailable on this address. Browsers only expose + a camera over HTTPS, and this page is on plain HTTP — so the scanner + will not run here even though the phone has a camera. Typing the delivery + note number below still works. +
+ To scan for real, open the app on its https:// address, or use a + secure tunnel to this dev server.

) : null}