Website und App auf getrennten Domains (craftvia.de / app.craftvia.de)

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>
This commit is contained in:
2026-09-21 09:08:47 +02:00
co-authored by Claude Opus 5
parent f665999d81
commit 4d5be72ebb
7 changed files with 206 additions and 5 deletions
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Erzeugt die Env-Datei für den Coolify-Testserver aus .env.coolify.example — mit frischen
# Geheimnissen, die nur auf diesem Rechner entstehen. Die Datei ist per .gitignore (.env*)
# vom Repo ausgeschlossen; Inhalt in Coolify unter "Environment Variables" einfügen.
#
# ./scripts/generate-coolify-env.sh # schreibt .env.coolify.local
# APP_DOMAIN=app.craftvia.de MARKETING_HOST=craftvia.de ./scripts/generate-coolify-env.sh
set -euo pipefail
OUT="${1:-.env.coolify.local}"
APP_DOMAIN="${APP_DOMAIN:-app.craftvia.de}"
MARKETING_HOST="${MARKETING_HOST:-craftvia.de}"
if [ -e "$OUT" ]; then
echo "$OUT existiert bereits und wird nicht überschrieben (Geheimnisse wären sonst verloren)." >&2
exit 1
fi
hex() { openssl rand -hex "$1"; }
DB_PASSWORD="$(hex 24)"
sed \
-e "s|CHANGE_ME_db_password|${DB_PASSWORD}|g" \
-e "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$(hex 24)|" \
-e "s|^S3_ACCESS_KEY=.*|S3_ACCESS_KEY=GK$(hex 12)|" \
-e "s|^S3_SECRET_KEY=.*|S3_SECRET_KEY=$(hex 32)|" \
-e "s|^GARAGE_RPC_SECRET=.*|GARAGE_RPC_SECRET=$(hex 32)|" \
-e "s|^GARAGE_ADMIN_TOKEN=.*|GARAGE_ADMIN_TOKEN=$(hex 32)|" \
-e "s|^AUTH_SECRET=.*|AUTH_SECRET=$(openssl rand -base64 32 | tr -d '\n')|" \
-e "s|^PASSWORD_PEPPER=.*|PASSWORD_PEPPER=$(hex 32)|" \
-e "s|^AUTH_URL=.*|AUTH_URL=https://${APP_DOMAIN}|" \
-e "s|^MARKETING_HOST=.*|MARKETING_HOST=${MARKETING_HOST}|" \
.env.coolify.example > "$OUT"
{
echo ""
echo "# --- erzeugt von scripts/generate-coolify-env.sh ($(date +%F)) ---"
echo "MFA_ENC_KEY=$(hex 32)"
echo "BACKUP_ENC_KEY=$(hex 32)"
} >> "$OUT"
chmod 600 "$OUT"
echo "Geschrieben: $OUT (nur lokal, Rechte 600)."
echo "Noch von Hand ergänzen: SMTP_HOST/PORT/SECURE/USER/PASSWORD/FROM, ANTHROPIC_API_KEY, TRIAL_CONTACT_EMAIL."
echo "Danach sicher ablegen (Passwortmanager) — PASSWORD_PEPPER und MFA_ENC_KEY sind nicht rotierbar."
+51
View File
@@ -0,0 +1,51 @@
// Domain-Trennung craftvia.de ↔ app.craftvia.de (src/lib/marketing/hosts.ts)
//
// Lauf: npx tsx scripts/test-host-routing.ts
import { hostSplit, routeByHost, type HostDecision } from "../src/lib/marketing/hosts";
let failures = 0;
const ok = (cond: boolean, msg: string) => {
console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`);
if (!cond) failures++;
};
const env = { MARKETING_HOST: "craftvia.de", AUTH_URL: "https://app.craftvia.de" };
const split = hostSplit(env)!;
const route = (host: string, path: string, hasSession = false): HostDecision => {
const [pathname, query] = path.split("?");
return routeByHost({ host, pathname, search: query ? `?${query}` : "", hasSession }, split);
};
const to = (d: HostDecision) => (d.kind === "redirect" ? `${d.status} ${d.location}` : "next");
console.log("\n— Konfiguration —");
ok(hostSplit({ AUTH_URL: "https://app.craftvia.de" }) === null, "ohne MARKETING_HOST keine Trennung (lokal)");
ok(hostSplit({ MARKETING_HOST: "craftvia.de" }) === null, "ohne AUTH_URL keine Trennung");
ok(hostSplit({ MARKETING_HOST: "app.craftvia.de", AUTH_URL: "https://app.craftvia.de" }) === null, "gleiche Domain → keine Trennung");
ok(hostSplit({ MARKETING_HOST: "https://Craftvia.de/", AUTH_URL: "https://app.craftvia.de" })?.marketingHost === "craftvia.de", "MARKETING_HOST mit Schema/Slash/Großschreibung wird normalisiert");
console.log("\n— Website-Domain —");
ok(to(route("craftvia.de", "/")) === "next", "/ zeigt die Startseite");
ok(to(route("craftvia.de", "/funktionen/planung")) === "next", "Funktionsseiten bleiben auf der Website");
ok(to(route("craftvia.de", "/impressum")) === "next", "Impressum bleibt auf der Website");
ok(to(route("craftvia.de", "/login")) === "308 https://app.craftvia.de/login", "/login → App-Domain");
ok(to(route("craftvia.de", "/testen?ref=preise")) === "308 https://app.craftvia.de/testen?ref=preise", "/testen → App-Domain, Query bleibt erhalten");
ok(to(route("craftvia.de", "/dashboard")) === "308 https://app.craftvia.de/dashboard", "App-Seiten → App-Domain");
ok(to(route("craftvia.de", "/api/v1/sync")) === "308 https://app.craftvia.de/api/v1/sync", "API → App-Domain (308 behält die Methode)");
ok(to(route("craftvia.de:443", "/preise")) === "next", "Port im Host wird ignoriert");
ok(to(route("www.craftvia.de", "/preise")) === "308 https://craftvia.de/preise", "www → Hauptdomain");
console.log("\n— App-Domain —");
ok(to(route("app.craftvia.de", "/")) === "307 https://app.craftvia.de/login", "/ ohne Sitzung → Login");
ok(to(route("app.craftvia.de", "/", true)) === "next", "/ mit Sitzung → weiter (Seite leitet auf Dashboard bzw. /m)");
ok(to(route("app.craftvia.de", "/funktionen")) === "308 https://craftvia.de/funktionen", "Produktseiten → Website");
ok(to(route("app.craftvia.de", "/impressum")) === "308 https://craftvia.de/impressum", "Impressum → Website");
ok(to(route("app.craftvia.de", "/testen")) === "next", "Testphase bleibt in der App");
ok(to(route("app.craftvia.de", "/planning", true)) === "next", "App-Seiten bleiben in der App");
console.log("\n— Interne Aufrufe —");
ok(to(route("127.0.0.1:3000", "/")) === "next", "Healthcheck über 127.0.0.1 unverändert");
ok(to(route("craftvia-app:3000", "/login")) === "next", "Container-interne Aufrufe unverändert");
console.log(failures ? `\n✗ ${failures} Fehler` : "\n✓ alle Prüfungen grün");
process.exit(failures ? 1 : 0);