diff --git a/.env.example b/.env.example index b360eb5..1cae9c2 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,8 @@ # ═══ Server ═══ PORT=8787 # Basis-URL, unter der RAPPORT-HOST erreichbar ist (für Stripe-Redirects). -# Lokal: http://localhost:5273 · Prod: https://host.rapport.studio -PUBLIC_BASE_URL=http://localhost:5273 +# Lokal: http://localhost:8787 · Prod: https://host.rapport.studio +PUBLIC_BASE_URL=http://localhost:8787 # ═══ Auth ═══ # JWT-Signatur-Secret für HOST-Kundenkonten. openssl rand -hex 32 diff --git a/package.json b/package.json index 29b19fa..b5cf6fd 100644 --- a/package.json +++ b/package.json @@ -3,21 +3,12 @@ "version": "0.1.0", "type": "module", "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": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", "server": "node --watch server/index.js", - "server:migrate": "node server/migrate.js", - "dev:all": "echo 'In zwei Terminals: npm run server und npm run dev'" - }, - "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@vitejs/plugin-react": "^4.3.1", - "vite": "^5.4.0" + "start": "node server/index.js", + "migrate": "node server/migrate.js", + "build:website": "cd ../RAPPORT-WEBSITE && hugo --quiet --baseURL / --destination public", + "dev": "npm run build:website && npm run server" } } diff --git a/public/fonts/Krungthep.ttf b/public/fonts/Krungthep.ttf deleted file mode 100644 index 9415853..0000000 Binary files a/public/fonts/Krungthep.ttf and /dev/null differ diff --git a/server/env.js b/server/env.js index e4099cd..49eb4fb 100644 --- a/server/env.js +++ b/server/env.js @@ -23,9 +23,12 @@ const e = process.env; export const env = { 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", 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: { secretKey: e.STRIPE_SECRET_KEY || "", diff --git a/server/index.js b/server/index.js index a03eb25..8a63bfb 100644 --- a/server/index.js +++ b/server/index.js @@ -21,12 +21,20 @@ app.use("/api/auth", authRouter); app.use("/api/billing", billingRouter); app.use("/api/account", accountRouter); -// In Produktion liefert dasselbe Backend das gebaute Frontend aus (dist/). -const dist = path.resolve(__dirname, "..", "dist"); -app.use(express.static(dist)); +// ── Statische Auslieferung der RAPPORT-WEBSITE (Hugo-Build) ────────────────── +// Die komplette Frontend-Oberfläche (Marketing + Hosting + Login/Konto als +// 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) => { 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 diff --git a/server/routes/billing.js b/server/routes/billing.js index 0e6ff44..448d174 100644 --- a/server/routes/billing.js +++ b/server/routes/billing.js @@ -61,7 +61,7 @@ billingRouter.post("/checkout", requireAuth, async (req, res) => { status: "active", 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 ────────────────────────────────────────────── @@ -72,8 +72,8 @@ billingRouter.post("/checkout", requireAuth, async (req, res) => { customer_email: req.account.email, client_reference_id: req.account.id, metadata: { accountId: req.account.id, planId: plan.id }, - success_url: `${env.publicBaseUrl}/dashboard?provisioned=1`, - cancel_url: `${env.publicBaseUrl}/plans?canceled=1`, + success_url: `${env.publicBaseUrl}/konto/?provisioned=1`, + cancel_url: `${env.publicBaseUrl}/hosting-preise/?canceled=1`, }); res.json({ url: session.url }); } catch (err) { diff --git a/src/App.jsx b/src/App.jsx index 99d3bb0..09cae2e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,40 +1,53 @@ import React, { useState, useEffect, useCallback } from "react"; import { auth } from "./api.js"; -import Landing from "./views/Landing.jsx"; import Register from "./views/Register.jsx"; import Login from "./views/Login.jsx"; import Plans from "./views/Plans.jsx"; import Dashboard from "./views/Dashboard.jsx"; -// Minimaler Pfad-Router (kein react-router nötig für 5 Seiten). -function usePath() { - const [path, setPath] = useState(window.location.pathname); +// Die App wird unter /app/ ausgeliefert (Hugo-Marketing liegt unter /). +// Der Mini-Router rechnet intern in App-relativen Pfaden ("/login"), +// 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(() => { - const onPop = () => setPath(window.location.pathname); + const onPop = () => setRoute(toRel(window.location.pathname)); window.addEventListener("popstate", onPop); return () => window.removeEventListener("popstate", onPop); }, []); const navigate = useCallback((to) => { - window.history.pushState({}, "", to); - setPath(to); + if (!ROUTES.includes(to)) { window.location.href = to; return; } // Marketing/extern + window.history.pushState({}, "", BASE + to); + setRoute(to); window.scrollTo(0, 0); }, []); - return [path, navigate]; + return [route, navigate]; } export default function App() { - const [path, navigate] = usePath(); + const [route, navigate] = useRoute(); 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(() => { - if (path === "/dashboard" && !loggedIn) navigate("/login"); - if ((path === "/login" || path === "/register") && loggedIn) navigate("/dashboard"); - }, [path, loggedIn, navigate]); + if (route === "/dashboard" && !loggedIn) navigate("/login"); + else if ((route === "/login" || route === "/register" || route === "/") && loggedIn) navigate("/dashboard"); + else if (route === "/" && !loggedIn) navigate("/login"); + }, [route, loggedIn, navigate]); - if (path === "/register") return ; - if (path === "/login") return ; - if (path === "/plans") return ; - if (path === "/dashboard") return ; - return ; + if (route === "/register") return ; + if (route === "/plans") return ; + if (route === "/dashboard") return ; + return ; } diff --git a/vite.config.js b/vite.config.js index dbf5a6c..b8154ad 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,9 +1,11 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; -// Frontend läuft auf :5273, Backend auf :8787 — /api wird durchgeproxt, -// damit im Dev kein CORS nötig ist und Prod-URLs identisch bleiben (/api/...). +// Die React-App wird in Produktion unter /app/ ausgeliefert (base), die +// 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({ + base: "/app/", plugins: [react()], server: { port: 5273,