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:
@@ -1,10 +1,13 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextResponse, type NextRequest } from 'next/server';
|
||||
|
||||
import { PORTAL_COOKIE } from '@/lib/portal/auth';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
export async function POST(): Promise<NextResponse> {
|
||||
const response = NextResponse.redirect(new URL('/portal/login', env.APP_URL));
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
// Build the redirect from the request URL so we stay on whatever host
|
||||
// the user is actually browsing from (localhost, LAN IP, prod domain).
|
||||
// Reading env.APP_URL here used to redirect phone-on-LAN users back
|
||||
// to localhost.
|
||||
const response = NextResponse.redirect(new URL('/portal/login', req.url));
|
||||
|
||||
response.cookies.delete(PORTAL_COOKIE);
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
setPortLogo,
|
||||
type LogoCrop,
|
||||
} from '@/lib/services/logo.service';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
const MAX_RAW_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
@@ -50,14 +49,13 @@ export const GET = withAuth(
|
||||
if (!file) {
|
||||
return NextResponse.json({ data: null });
|
||||
}
|
||||
const baseUrl = env.APP_URL.replace(/\/+$/, '');
|
||||
// Stream from the public-by-id surface (gated on `category='branding'`)
|
||||
// so the URL works as a direct `<img src>` — the authenticated
|
||||
// `/api/v1/files/<id>/preview` returns JSON, not image bytes.
|
||||
// Path-only — the admin UI renders this as `<img src>` and the
|
||||
// browser resolves against the current origin. Stays valid whether
|
||||
// the admin opens the page from localhost or a LAN IP.
|
||||
return NextResponse.json({
|
||||
data: {
|
||||
fileId: file.id,
|
||||
previewUrl: `${baseUrl}/api/public/files/${file.id}`,
|
||||
previewUrl: `/api/public/files/${file.id}`,
|
||||
sizeBytes: file.sizeBytes,
|
||||
mimeType: file.mimeType,
|
||||
},
|
||||
@@ -95,11 +93,10 @@ export const POST = withAuth(
|
||||
ipAddress: ctx.ipAddress,
|
||||
userAgent: ctx.userAgent,
|
||||
});
|
||||
const baseUrl = env.APP_URL.replace(/\/+$/, '');
|
||||
return NextResponse.json({
|
||||
data: {
|
||||
fileId: result.fileId,
|
||||
previewUrl: `${baseUrl}/api/public/files/${result.fileId}`,
|
||||
previewUrl: `/api/public/files/${result.fileId}`,
|
||||
warnings: result.warnings,
|
||||
finalDimensions: processed.finalDimensions,
|
||||
finalBytes: processed.finalBytes,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { db } from '@/lib/db';
|
||||
import { ports } from '@/lib/db/schema/ports';
|
||||
import { uploadFile } from '@/lib/services/files';
|
||||
import { errorResponse, ValidationError } from '@/lib/errors';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
const MAX_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
@@ -77,11 +76,11 @@ export const POST = withAuth(
|
||||
},
|
||||
);
|
||||
|
||||
const baseUrl = env.APP_URL.replace(/\/+$/, '');
|
||||
// Branding assets must survive in email-inbox land where no session
|
||||
// cookie travels — route through the public-by-id surface gated on
|
||||
// `category='branding'` rather than the authenticated preview path.
|
||||
const url = `${baseUrl}/api/public/files/${record.id}`;
|
||||
// Path-only so the in-app `<img src>` resolves against whatever
|
||||
// host the page was loaded from (localhost, LAN IP, prod domain).
|
||||
// Email shell calls `absolutizeBrandingUrl()` to prepend APP_URL
|
||||
// for mail clients, which have no origin context.
|
||||
const url = `/api/public/files/${record.id}`;
|
||||
|
||||
return NextResponse.json({ data: { fileId: record.id, url } });
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user