fix(dev-lan): unblock phone-on-LAN testing of the dev server

Branding URLs were baked with env.APP_URL=http://localhost:3000 at
upload time and stored verbatim in system_settings, so any logo/
background loaded from a non-localhost origin (an iPhone hitting the
Mac's LAN IP) failed to resolve. Same pattern bit Socket.IO (CORS +
client connection target) and the portal logout redirect.

- Branding: getPortBrandingConfig normalizes localhost/private-LAN
  hosts to path-only; both upload routes store path-only going
  forward; email shell re-absolutizes via absolutizeBrandingUrl() so
  inboxes (no app origin) still get fetchable URLs. DB backfilled to
  strip http://localhost:3000 from existing rows.
- Socket.IO: client connects to window.location.origin (io() with no
  URL); server CORS allows localhost + private-LAN ranges in dev,
  stays locked to APP_URL in prod.
- Portal logout: redirect target built from the request URL instead
  of env.APP_URL.
- next.config: allowedDevOrigins widened from a hardcoded IP to
  192.168/10/172.16-31 wildcards so HMR works across networks
  without an edit per-network. (Without HMR the login form's React
  click handler never hydrates and the form falls back to GET,
  leaking the password into the URL.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 12:28:34 +02:00
parent 6aaccb6d33
commit be261f3f90
10 changed files with 124 additions and 30 deletions

View File

@@ -15,6 +15,27 @@ import type { ServerToClientEvents, ClientToServerEvents } from './events';
let io: Server<ClientToServerEvents, ServerToClientEvents> | null = null;
const DEV_ORIGIN_PATTERNS = [
/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/,
/^https?:\/\/192\.168\.\d+\.\d+(:\d+)?$/,
/^https?:\/\/10\.\d+\.\d+\.\d+(:\d+)?$/,
/^https?:\/\/172\.(1[6-9]|2\d|3[01])\.\d+\.\d+(:\d+)?$/,
];
function socketCorsOrigin(
origin: string | undefined,
cb: (err: Error | null, allow?: boolean) => void,
): void {
if (!origin) return cb(null, true);
if (process.env.NODE_ENV === 'production') {
return cb(null, origin === process.env.APP_URL);
}
cb(
null,
DEV_ORIGIN_PATTERNS.some((re) => re.test(origin)),
);
}
/**
* Returns true if the user is a super-admin OR holds a userPortRoles row
* for the given portId. The Socket.IO auth middleware uses this to decide
@@ -77,7 +98,11 @@ export function initSocketServer(
path: '/socket.io/',
adapter: createAdapter(pubClient, subClient),
cors: {
origin: process.env.APP_URL,
// In prod, lock to the canonical APP_URL. In dev, allow localhost
// + private-LAN origins so the same dev server serves the Mac
// (localhost) and a phone on Wi-Fi (192.168.x.x) without a config
// edit per network. Mirrors the trustedOrigins pattern in auth.
origin: socketCorsOrigin,
credentials: true,
},
connectionStateRecovery: { maxDisconnectionDuration: 2 * 60 * 1000 },