/* RAPPORT Hosting — Frontend-Logik (Vanilla JS).
*
* Reine Client-Seite: spricht ausschließlich mit dem proprietären Backend
* unter /api (RAPPORT-HOST). Hier liegt KEINE Geschäftslogik, nur fetch +
* Rendering — so bleibt RAPPORT-WEBSITE sauber AGPL/öffentlich.
*
* Token im localStorage; gerendert wird in #hosting-root je nach data-page.
*/
(function () {
"use strict";
const root = document.getElementById("hosting-root");
if (!root) return;
const page = root.dataset.page || "login";
const TOKEN_KEY = "rapport_host_token";
const tok = {
get: () => localStorage.getItem(TOKEN_KEY),
set: (t) => localStorage.setItem(TOKEN_KEY, t),
clear: () => localStorage.removeItem(TOKEN_KEY),
get isLoggedIn() { return !!localStorage.getItem(TOKEN_KEY); },
};
async function api(method, path, body) {
const headers = { "Content-Type": "application/json" };
const t = tok.get();
if (t) headers.Authorization = "Bearer " + t;
const res = await fetch("/api" + path, {
method, headers, body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || ("Fehler " + res.status));
return data;
}
const go = (p) => { window.location.href = p; };
const esc = (s) => String(s == null ? "" : s).replace(/[&<>"]/g, (c) =>
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
function card(inner, wide) {
return '
' + inner + "
";
}
function msg(el, text, kind) {
el.innerHTML = text ? '' + esc(text) + "
" : "";
}
// ── Login ──────────────────────────────────────────────────────────────
function renderLogin() {
root.innerHTML = card(
'Anmelden
' +
'Zu Ihrer Rapport-Instanz
' +
'' +
'" +
''
);
const m = root.querySelector("#m");
root.querySelector("#toReg").onclick = () => go("/register/");
root.querySelector("#f").onsubmit = async (e) => {
e.preventDefault();
const sub = root.querySelector("#sub"); sub.disabled = true; msg(m, "", "");
try {
const { token } = await api("POST", "/auth/login", {
email: root.querySelector("#email").value.trim(),
password: root.querySelector("#pw").value,
});
tok.set(token); go("/konto/");
} catch (err) { msg(m, err.message, "err"); sub.disabled = false; }
};
}
// ── Registrierung ──────────────────────────────────────────────────────
function renderRegister() {
root.innerHTML = card(
'Konto erstellen
' +
'In Minuten zur eigenen Instanz
' +
'' +
'" +
''
);
const m = root.querySelector("#m");
root.querySelector("#toLogin").onclick = () => go("/login/");
root.querySelector("#f").onsubmit = async (e) => {
e.preventDefault();
const sub = root.querySelector("#sub"); sub.disabled = true; msg(m, "", "");
try {
const { token } = await api("POST", "/auth/register", {
email: root.querySelector("#email").value.trim(),
password: root.querySelector("#pw").value,
});
tok.set(token); go("/konto/");
} catch (err) { msg(m, err.message, "err"); sub.disabled = false; }
};
}
// ── Konto / Dashboard ──────────────────────────────────────────────────
async function renderKonto() {
if (!tok.isLoggedIn) return go("/login/");
root.innerHTML = card('Lädt…
', true);
let data;
try { data = await api("GET", "/account/me"); }
catch (err) {
if (/angemeldet|abgelaufen|ungültig/i.test(err.message)) { tok.clear(); return go("/login/"); }
root.innerHTML = card('' + esc(err.message) + "
"); return;
}
const { account, subscription, instance, plans } = data;
const params = new URLSearchParams(location.search);
const justOk = params.get("provisioned") === "1";
let html =
'' +
'' + esc(account.email) + "
";
if (justOk) html += 'Zahlung erfolgreich — Ihre Instanz wird bereitgestellt.
';
if (subscription) {
html += 'Abo' + esc(subscription.plan) +
" · " + esc(subscription.status) + "
";
}
if (instance) {
html += 'Instanz' + esc(instance.status) + "
" +
'Rapport öffnen →';
} else {
html += 'Wählen Sie ein Abo, um Ihre Instanz freizuschalten:
' +
'' + (plans || []).map(planCard).join("") + "
";
}
root.innerHTML = card(html, true);
root.querySelector("#logout").onclick = () => { tok.clear(); go("/"); };
root.querySelectorAll("[data-plan]").forEach((b) => {
b.onclick = async () => {
b.disabled = true;
try {
const { url } = await api("POST", "/billing/checkout", { planId: b.dataset.plan });
go(url);
} catch (err) { alert(err.message); b.disabled = false; }
};
});
}
function planCard(p) {
return '' +
(p.recommended ? '
Empfohlen
' : "") +
'
' + esc(p.name) + "
" +
'
CHF ' + esc(p.priceChf) + '/' + esc(p.interval) + "
" +
"
" + (p.features || []).map((f) => "- " + esc(f) + "
").join("") + "
" +
'
';
}
// ── Preise (öffentlich, mit CTA in den Flow) ───────────────────────────
async function renderPreise() {
root.innerHTML = card('Lädt…
', true);
let plans = [];
try { plans = (await api("GET", "/billing/plans")).plans; } catch (_) {}
const html =
'Abo wählen
' +
'Monatlich kündbar · Preise in CHF, exkl. MwSt.
' +
'' + plans.map(planCard).join("") + "
";
root.innerHTML = card(html, true);
root.querySelectorAll("[data-plan]").forEach((b) => {
b.onclick = () => go(tok.isLoggedIn ? "/konto/" : "/register/");
});
}
({ login: renderLogin, register: renderRegister, konto: renderKonto, preise: renderPreise }[page] || renderLogin)();
})();