refactor: RAPPORT-HOST ist jetzt reines Backend
Das Frontend (Marketing + Login/Konto) ist in RAPPORT-WEBSITE umgezogen (Hugo + Vanilla-JS). RAPPORT-HOST liefert dessen gebautes public/ statisch aus und stellt /api bereit. - server/index.js: serviert RAPPORT-WEBSITE/public + /api (eine Origin) - server/env.js: websitePublicDir (WEBSITE_PUBLIC_DIR, Default Schwester-Repo); PUBLIC_BASE_URL Default auf einheitliche Origin :8787 - billing.js: Checkout-Redirects auf /konto/ bzw. /hosting-preise/ - entfernt: src/ (React), marketing/ (Hugo-Kopie), index.html, vite.config.js - package.json: nur noch server/migrate/build:website-Scripts E2E verifiziert: /, /hosting/, /login/, /konto/, /js + Font = 200; register→checkout(mock)→Instanz; Redirect → /konto/?provisioned=1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
# ═══ Server ═══
|
# ═══ Server ═══
|
||||||
PORT=8787
|
PORT=8787
|
||||||
# Basis-URL, unter der RAPPORT-HOST erreichbar ist (für Stripe-Redirects).
|
# Basis-URL, unter der RAPPORT-HOST erreichbar ist (für Stripe-Redirects).
|
||||||
# Lokal: http://localhost:5273 · Prod: https://host.rapport.studio
|
# Lokal: http://localhost:8787 · Prod: https://host.rapport.studio
|
||||||
PUBLIC_BASE_URL=http://localhost:5273
|
PUBLIC_BASE_URL=http://localhost:8787
|
||||||
|
|
||||||
# ═══ Auth ═══
|
# ═══ Auth ═══
|
||||||
# JWT-Signatur-Secret für HOST-Kundenkonten. openssl rand -hex 32
|
# JWT-Signatur-Secret für HOST-Kundenkonten. openssl rand -hex 32
|
||||||
|
|||||||
+5
-14
@@ -3,21 +3,12 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "RAPPORT-HOST — kommerzielle Hosting-/Abo-Plattform für Rapport-Instanzen (proprietär)",
|
"description": "RAPPORT-HOST — Backend für die kommerzielle Hosting-/Abo-Plattform (proprietär). Frontend lebt in RAPPORT-WEBSITE.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"server": "node --watch server/index.js",
|
"server": "node --watch server/index.js",
|
||||||
"server:migrate": "node server/migrate.js",
|
"start": "node server/index.js",
|
||||||
"dev:all": "echo 'In zwei Terminals: npm run server und npm run dev'"
|
"migrate": "node server/migrate.js",
|
||||||
},
|
"build:website": "cd ../RAPPORT-WEBSITE && hugo --quiet --baseURL / --destination public",
|
||||||
"dependencies": {
|
"dev": "npm run build:website && npm run server"
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
|
||||||
"vite": "^5.4.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
+4
-1
@@ -23,9 +23,12 @@ const e = process.env;
|
|||||||
|
|
||||||
export const env = {
|
export const env = {
|
||||||
port: parseInt(e.PORT || "8787", 10),
|
port: parseInt(e.PORT || "8787", 10),
|
||||||
publicBaseUrl: (e.PUBLIC_BASE_URL || "http://localhost:5273").replace(/\/+$/, ""),
|
publicBaseUrl: (e.PUBLIC_BASE_URL || "http://localhost:8787").replace(/\/+$/, ""),
|
||||||
jwtSecret: e.JWT_SECRET || "dev-insecure-secret-change-me",
|
jwtSecret: e.JWT_SECRET || "dev-insecure-secret-change-me",
|
||||||
databaseUrl: e.DATABASE_URL || "postgres://rapport_host:rapport_host@localhost:55432/rapport_host",
|
databaseUrl: e.DATABASE_URL || "postgres://rapport_host:rapport_host@localhost:55432/rapport_host",
|
||||||
|
// Gebautes public/ der RAPPORT-WEBSITE (Hugo). Default: Schwester-Repo lokal.
|
||||||
|
websitePublicDir: e.WEBSITE_PUBLIC_DIR ||
|
||||||
|
new URL("../../RAPPORT-WEBSITE/public", import.meta.url).pathname,
|
||||||
|
|
||||||
stripe: {
|
stripe: {
|
||||||
secretKey: e.STRIPE_SECRET_KEY || "",
|
secretKey: e.STRIPE_SECRET_KEY || "",
|
||||||
|
|||||||
+12
-4
@@ -21,12 +21,20 @@ app.use("/api/auth", authRouter);
|
|||||||
app.use("/api/billing", billingRouter);
|
app.use("/api/billing", billingRouter);
|
||||||
app.use("/api/account", accountRouter);
|
app.use("/api/account", accountRouter);
|
||||||
|
|
||||||
// In Produktion liefert dasselbe Backend das gebaute Frontend aus (dist/).
|
// ── Statische Auslieferung der RAPPORT-WEBSITE (Hugo-Build) ──────────────────
|
||||||
const dist = path.resolve(__dirname, "..", "dist");
|
// Die komplette Frontend-Oberfläche (Marketing + Hosting + Login/Konto als
|
||||||
app.use(express.static(dist));
|
// Vanilla-JS-Seiten) kommt aus dem AGPL-Repo RAPPORT-WEBSITE. Dieses Backend
|
||||||
|
// liefert dessen gebautes public/ aus und stellt darunter /api bereit.
|
||||||
|
// Saubere Trennung: die Website enthält keinen Backend-Code, nur fetch('/api').
|
||||||
|
// Pfad per WEBSITE_PUBLIC_DIR überschreibbar (Default: Schwester-Repo).
|
||||||
|
const websitePublic = env.websitePublicDir;
|
||||||
|
|
||||||
|
app.use(express.static(websitePublic));
|
||||||
app.get("*", (req, res, next) => {
|
app.get("*", (req, res, next) => {
|
||||||
if (req.path.startsWith("/api/")) return next();
|
if (req.path.startsWith("/api/")) return next();
|
||||||
res.sendFile(path.join(dist, "index.html"), (err) => err && next());
|
// Hugo erzeugt pro Seite eine eigene index.html → Clean-URLs funktionieren
|
||||||
|
// direkt über express.static. Unbekannte Pfade → 404-Seite.
|
||||||
|
res.status(404).sendFile(path.join(websitePublic, "404.html"), (err) => err && next());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Express-Fehlerhandler: fängt synchron geworfene Fehler aus Routen → 500
|
// Express-Fehlerhandler: fängt synchron geworfene Fehler aus Routen → 500
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ billingRouter.post("/checkout", requireAuth, async (req, res) => {
|
|||||||
status: "active",
|
status: "active",
|
||||||
periodEnd: new Date(Date.now() + 30 * 864e5),
|
periodEnd: new Date(Date.now() + 30 * 864e5),
|
||||||
});
|
});
|
||||||
return res.json({ mock: true, url: `${env.publicBaseUrl}/dashboard?provisioned=1` });
|
return res.json({ mock: true, url: `${env.publicBaseUrl}/konto/?provisioned=1` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Stripe-Checkout-Session ──────────────────────────────────────────────
|
// ── Stripe-Checkout-Session ──────────────────────────────────────────────
|
||||||
@@ -72,8 +72,8 @@ billingRouter.post("/checkout", requireAuth, async (req, res) => {
|
|||||||
customer_email: req.account.email,
|
customer_email: req.account.email,
|
||||||
client_reference_id: req.account.id,
|
client_reference_id: req.account.id,
|
||||||
metadata: { accountId: req.account.id, planId: plan.id },
|
metadata: { accountId: req.account.id, planId: plan.id },
|
||||||
success_url: `${env.publicBaseUrl}/dashboard?provisioned=1`,
|
success_url: `${env.publicBaseUrl}/konto/?provisioned=1`,
|
||||||
cancel_url: `${env.publicBaseUrl}/plans?canceled=1`,
|
cancel_url: `${env.publicBaseUrl}/hosting-preise/?canceled=1`,
|
||||||
});
|
});
|
||||||
res.json({ url: session.url });
|
res.json({ url: session.url });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
+31
-18
@@ -1,40 +1,53 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { auth } from "./api.js";
|
import { auth } from "./api.js";
|
||||||
import Landing from "./views/Landing.jsx";
|
|
||||||
import Register from "./views/Register.jsx";
|
import Register from "./views/Register.jsx";
|
||||||
import Login from "./views/Login.jsx";
|
import Login from "./views/Login.jsx";
|
||||||
import Plans from "./views/Plans.jsx";
|
import Plans from "./views/Plans.jsx";
|
||||||
import Dashboard from "./views/Dashboard.jsx";
|
import Dashboard from "./views/Dashboard.jsx";
|
||||||
|
|
||||||
// Minimaler Pfad-Router (kein react-router nötig für 5 Seiten).
|
// Die App wird unter /app/ ausgeliefert (Hugo-Marketing liegt unter /).
|
||||||
function usePath() {
|
// Der Mini-Router rechnet intern in App-relativen Pfaden ("/login"),
|
||||||
const [path, setPath] = useState(window.location.pathname);
|
// schreibt aber immer mit BASE-Präfix in die URL ("/app/login").
|
||||||
|
const BASE = "/app";
|
||||||
|
// Interne App-Routen. Alles andere, was navigate() bekommt (z.B. "/preise/"),
|
||||||
|
// ist ein Marketing-Link und wird als echter Seitenwechsel behandelt.
|
||||||
|
const ROUTES = ["/login", "/register", "/plans", "/dashboard"];
|
||||||
|
|
||||||
|
function toRel(pathname) {
|
||||||
|
const p = pathname.startsWith(BASE) ? pathname.slice(BASE.length) : pathname;
|
||||||
|
const clean = (p.replace(/\/$/, "") || "/");
|
||||||
|
return ROUTES.includes(clean) ? clean : "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
function useRoute() {
|
||||||
|
const [route, setRoute] = useState(toRel(window.location.pathname));
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onPop = () => setPath(window.location.pathname);
|
const onPop = () => setRoute(toRel(window.location.pathname));
|
||||||
window.addEventListener("popstate", onPop);
|
window.addEventListener("popstate", onPop);
|
||||||
return () => window.removeEventListener("popstate", onPop);
|
return () => window.removeEventListener("popstate", onPop);
|
||||||
}, []);
|
}, []);
|
||||||
const navigate = useCallback((to) => {
|
const navigate = useCallback((to) => {
|
||||||
window.history.pushState({}, "", to);
|
if (!ROUTES.includes(to)) { window.location.href = to; return; } // Marketing/extern
|
||||||
setPath(to);
|
window.history.pushState({}, "", BASE + to);
|
||||||
|
setRoute(to);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}, []);
|
}, []);
|
||||||
return [path, navigate];
|
return [route, navigate];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [path, navigate] = usePath();
|
const [route, navigate] = useRoute();
|
||||||
const loggedIn = auth.isLoggedIn;
|
const loggedIn = auth.isLoggedIn;
|
||||||
|
|
||||||
// Geschützte Seiten ohne Login → Login. Login/Register mit Login → Dashboard.
|
// Geschützte/öffentliche Weiterleitungen. "/" (App-Einstieg) → je nach Login.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (path === "/dashboard" && !loggedIn) navigate("/login");
|
if (route === "/dashboard" && !loggedIn) navigate("/login");
|
||||||
if ((path === "/login" || path === "/register") && loggedIn) navigate("/dashboard");
|
else if ((route === "/login" || route === "/register" || route === "/") && loggedIn) navigate("/dashboard");
|
||||||
}, [path, loggedIn, navigate]);
|
else if (route === "/" && !loggedIn) navigate("/login");
|
||||||
|
}, [route, loggedIn, navigate]);
|
||||||
|
|
||||||
if (path === "/register") return <Register navigate={navigate} />;
|
if (route === "/register") return <Register navigate={navigate} />;
|
||||||
if (path === "/login") return <Login navigate={navigate} />;
|
if (route === "/plans") return <Plans navigate={navigate} />;
|
||||||
if (path === "/plans") return <Plans navigate={navigate} />;
|
if (route === "/dashboard") return <Dashboard navigate={navigate} />;
|
||||||
if (path === "/dashboard") return <Dashboard navigate={navigate} />;
|
return <Login navigate={navigate} />;
|
||||||
return <Landing navigate={navigate} />;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -1,9 +1,11 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
// Frontend läuft auf :5273, Backend auf :8787 — /api wird durchgeproxt,
|
// Die React-App wird in Produktion unter /app/ ausgeliefert (base), die
|
||||||
// damit im Dev kein CORS nötig ist und Prod-URLs identisch bleiben (/api/...).
|
// Hugo-Marketing-Site liegt unter /. Im Dev läuft Vite auf :5273 und proxt
|
||||||
|
// /api → Backend, damit kein CORS nötig ist und die URLs identisch bleiben.
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: "/app/",
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port: 5273,
|
port: 5273,
|
||||||
|
|||||||
Reference in New Issue
Block a user