feat: support zero-config LAN access for local development
Auto-rewrite loopback API URLs to current page host in browser Allow CORS for private LAN IPs when localhost origin is configured Remove display limit for related Pokémon in detail view
This commit is contained in:
@@ -197,7 +197,10 @@ const sessionCookieName = 'pokopia_session';
|
||||
const rememberedSessionDays = 30;
|
||||
const sessionOnlySessionDays = 1;
|
||||
|
||||
function configuredCorsOrigin(): true | string | string[] {
|
||||
type CorsOriginCallback = (error: Error | null, origin: boolean | string) => void;
|
||||
type CorsOriginResolver = (origin: string | undefined, callback: CorsOriginCallback) => void;
|
||||
|
||||
function configuredCorsOrigin(): true | CorsOriginResolver {
|
||||
const rawOrigin = process.env.FRONTEND_ORIGIN?.trim();
|
||||
if (!rawOrigin) {
|
||||
return true;
|
||||
@@ -208,7 +211,78 @@ function configuredCorsOrigin(): true | string | string[] {
|
||||
.map((origin) => origin.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return origins.length <= 1 ? (origins[0] ?? true) : origins;
|
||||
if (origins.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (origin, callback) => {
|
||||
callback(null, isAllowedCorsOrigin(origin, origins) ? (origin ?? false) : false);
|
||||
};
|
||||
}
|
||||
|
||||
function isAllowedCorsOrigin(origin: string | undefined, configuredOrigins: string[]): boolean {
|
||||
if (!origin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (configuredOrigins.includes(origin)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return configuredOrigins.some((configuredOrigin) => isLocalFrontendOriginAlias(origin, configuredOrigin));
|
||||
}
|
||||
|
||||
function isLocalFrontendOriginAlias(origin: string, configuredOrigin: string): boolean {
|
||||
try {
|
||||
const requestUrl = new URL(origin);
|
||||
const configuredUrl = new URL(configuredOrigin);
|
||||
|
||||
return (
|
||||
requestUrl.protocol === configuredUrl.protocol &&
|
||||
effectivePort(requestUrl) === effectivePort(configuredUrl) &&
|
||||
isLoopbackHost(configuredUrl.hostname) &&
|
||||
isLocalDevelopmentHost(requestUrl.hostname)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function effectivePort(url: URL): string {
|
||||
if (url.port) {
|
||||
return url.port;
|
||||
}
|
||||
|
||||
return url.protocol === 'https:' ? '443' : '80';
|
||||
}
|
||||
|
||||
function isLoopbackHost(hostname: string): boolean {
|
||||
return ['localhost', '127.0.0.1', '::1', '[::1]'].includes(hostname);
|
||||
}
|
||||
|
||||
function isLocalDevelopmentHost(hostname: string): boolean {
|
||||
return (
|
||||
isLoopbackHost(hostname) ||
|
||||
isPrivateIpv4Host(hostname) ||
|
||||
['host.docker.internal', 'frontend', 'frontend_gateway'].includes(hostname)
|
||||
);
|
||||
}
|
||||
|
||||
function isPrivateIpv4Host(hostname: string): boolean {
|
||||
const parts = hostname.split('.').map((part) => Number(part));
|
||||
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [first, second] = parts;
|
||||
|
||||
return (
|
||||
first === 10 ||
|
||||
first === 127 ||
|
||||
(first === 172 && second >= 16 && second <= 31) ||
|
||||
(first === 192 && second === 168) ||
|
||||
(first === 169 && second === 254)
|
||||
);
|
||||
}
|
||||
|
||||
await app.register(cors, {
|
||||
|
||||
Reference in New Issue
Block a user