Two separate reasons the phone could not use the scanner, only one of
which is about auth.
1. NO LOGIN PATH. `(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 /scan rendered the scanner for
anyone, the first API call came back "Not authenticated.", and
there was no way forward from the page: the operator had to guess
that /login existed somewhere else in the app. Now gated to
`/login?next=/scan`, with the path run through `isSafeNextPath`.
The gate lives in its own client component because `metadata`
cannot be exported from a "use client" module — caught by loading
the page, not by tsc.
2. THE LAN ENTRY WAS BLOCKED, and the allowlist could never have
worked. `allowedDevOrigins` is matched by `isCsrfOriginAllowed`,
which splits the host on "." and compares label by label, treating
`*` as exactly ONE label. Checked against Next 16.2.6:
isCsrfOriginAllowed("192.168.1.35", ["*"]) -> false
isCsrfOriginAllowed("192.168.1.35", ["192.168.*"]) -> false
isCsrfOriginAllowed("192.168.1.35", ["192.168.1.*"]) -> true
So the `"*"` that had been sitting in the list for months did
nothing, and the list hard-coded three individual LAN addresses
(.42, .122, .128) that go stale whenever the router hands out a new
DHCP lease. When the lease moved to .35 the dev server refused the
phone's dev resources and the page hung on the spinner forever.
Now wildcards the last label of the subnet.
Also separates the two failure states the UI conflated: "Camera
blocked — this page is not on HTTPS" is now distinct from "this device
has no camera", because on a phone over plain HTTP those look
identical and the first is the one you can actually fix.
55 lines
3.1 KiB
TypeScript
55 lines
3.1 KiB
TypeScript
import type { NextConfig } from "next";
|
|
|
|
const nextConfig: NextConfig = {
|
|
output: "standalone",
|
|
// Server-only deps we don't want webpack to touch. Both pdf-parse and
|
|
// its transitive pdfjs-dist use ES2022 features + top-level `__esModule`
|
|
// marker that webpack's RSC runtime chokes on
|
|
// ("Object.defineProperty called on non-object"). Marking them external
|
|
// means Node resolves them at runtime from node_modules instead of going
|
|
// 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 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`).
|
|
// Redirect the underscore variants to the hyphenated route so the agent
|
|
// stops confusing the PB collection name with the URL path.
|
|
async redirects() {
|
|
return [
|
|
{ source: "/api/v1/order_items", destination: "/api/v1/order-items", permanent: true },
|
|
{ source: "/api/v1/stock_items", destination: "/api/v1/stock-items", permanent: true },
|
|
{ source: "/api/v1/quote_items", destination: "/api/v1/quote-items", permanent: true },
|
|
{ source: "/api/v1/stock_purchases", destination: "/api/v1/stock-purchases", permanent: true },
|
|
{ source: "/api/v1/stock_quick_draft", destination: "/api/v1/stock-quick-draft", permanent: true },
|
|
// Counter PWA shortcut — keeps the install URL short.
|
|
{ source: "/counter", destination: "/new-order", permanent: true },
|
|
// TV dashboard shortcut — /tv → /pin (which itself bounces
|
|
// to /dashboard once the cookie is valid).
|
|
{ source: "/tv", destination: "/pin", permanent: true },
|
|
];
|
|
},
|
|
};
|
|
|
|
export default nextConfig;
|