Der Proxy trennt nach Host, sobald MARKETING_HOST gesetzt ist: craftvia.de zeigt nur die Produktseiten und leitet Login, Testphase, App und API auf die App-Domain aus AUTH_URL um; www leitet auf die Hauptdomain; auf der App-Domain landet / ohne Sitzung beim Login und Produktseiten gehen zurück zur Website. Interne Aufrufe (Healthcheck) bleiben unverändert. Links bleiben relativ, damit keine Domain im Image steckt. Dazu Test scripts/test-host-routing.ts, Env-Vorlagen mit MARKETING_HOST und Registry, scripts/generate-coolify-env.sh für lokal erzeugte Geheimnisse und DEPLOY.md §2.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
59 lines
2.7 KiB
TypeScript
59 lines
2.7 KiB
TypeScript
/**
|
|
* Domain-Trennung Website ↔ App: `craftvia.de` zeigt nur die Produktseiten, `app.craftvia.de` die
|
|
* Anwendung. Aktiv nur, wenn `MARKETING_HOST` gesetzt ist; die App-Domain kommt aus `AUTH_URL`.
|
|
* Ohne `MARKETING_HOST` (lokal, Tests) läuft alles wie bisher auf einem Host.
|
|
*
|
|
* Links der Produktseiten bleiben relativ (/login, /testen): der Proxy leitet sie von der
|
|
* Website-Domain auf die App-Domain um. So wird keine Domain beim Image-Bau festgeschrieben.
|
|
*/
|
|
|
|
export const MARKETING_PATHS = ["/", "/funktionen", "/sicherheit", "/preise", "/impressum"] as const;
|
|
|
|
export type HostSplit = { marketingHost: string; appHost: string; appOrigin: string };
|
|
|
|
export type HostDecision = { kind: "next" } | { kind: "redirect"; location: string; status: 307 | 308 };
|
|
|
|
export function hostSplit(env: Record<string, string | undefined> = process.env): HostSplit | null {
|
|
const marketingHost = env.MARKETING_HOST?.trim()
|
|
.toLowerCase()
|
|
.replace(/^https?:\/\//, "")
|
|
.replace(/[/:].*$/, "");
|
|
const authUrl = env.AUTH_URL?.trim();
|
|
if (!marketingHost || !authUrl) return null;
|
|
let app: URL;
|
|
try {
|
|
app = new URL(authUrl);
|
|
} catch {
|
|
return null;
|
|
}
|
|
const appHost = app.hostname.toLowerCase();
|
|
if (appHost === marketingHost) return null;
|
|
return { marketingHost, appHost, appOrigin: app.origin };
|
|
}
|
|
|
|
export function isMarketingPath(pathname: string): boolean {
|
|
return MARKETING_PATHS.some((p) => pathname === p || (p !== "/" && pathname.startsWith(p + "/")));
|
|
}
|
|
|
|
export function routeByHost(input: { host: string; pathname: string; search: string; hasSession: boolean }, split: HostSplit): HostDecision {
|
|
const host = input.host.toLowerCase().split(",")[0].trim().replace(/:\d+$/, "");
|
|
const target = input.pathname + input.search;
|
|
const marketingOrigin = `https://${split.marketingHost}`;
|
|
|
|
// www → Hauptdomain
|
|
if (host === `www.${split.marketingHost}`) return { kind: "redirect", location: marketingOrigin + target, status: 308 };
|
|
|
|
// Website-Domain: nur Produktseiten, alles andere (Login, Testphase, App, API) gehört zur App
|
|
if (host === split.marketingHost) {
|
|
return isMarketingPath(input.pathname) ? { kind: "next" } : { kind: "redirect", location: split.appOrigin + target, status: 308 };
|
|
}
|
|
|
|
// Interne Aufrufe (Healthcheck über 127.0.0.1, Worker) bleiben unverändert
|
|
if (host !== split.appHost) return { kind: "next" };
|
|
|
|
// App-Domain: Produktseiten wohnen auf der Website, die Startseite ist der Login
|
|
if (input.pathname !== "/" && isMarketingPath(input.pathname)) return { kind: "redirect", location: marketingOrigin + target, status: 308 };
|
|
if (input.pathname === "/" && !input.hasSession) return { kind: "redirect", location: `${split.appOrigin}/login`, status: 307 };
|
|
return { kind: "next" };
|
|
}
|