Echtes Google-/PayPal-Login: OAuth 2.0 + PKCE, eigene Kundendatenbank, DSGVO-Selbstbedienung

Kundenkonten sind jetzt genauso echt wie die PayPal-Zahlung: server-geprüftes OAuth 2.0 mit
PKCE für Google und "Log in with PayPal" (functions/_shared/oauth.js, oauth-handlers.js),
neue D1-Tabelle "customers" (bewusst ohne Passwort-Feld), eigene von der Zugangscode-Schranke
getrennte Sitzungs-Logik (customer-auth.js). Echte DSGVO-Rechte direkt im Kontobereich:
Daten herunterladen (Art. 15/20) und Konto unwiderruflich löschen (Art. 17), Bestellungen
bleiben aus gesetzlichen Gründen erhalten. Datenschutzerklärung entsprechend ergänzt.

Ohne echte Google-/PayPal-Zugangsdaten zeigt der Login-Button ehrlich einen
"noch nicht eingerichtet"-Hinweis statt eine Anmeldung vorzutäuschen.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
qcigano
2026-08-04 01:45:55 +02:00
co-authored by Claude Sonnet 5
parent e80aba5ca6
commit bf0d169015
24 changed files with 985 additions and 127 deletions
+59 -9
View File
@@ -315,6 +315,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<tr><th>{t.accountDash.profileAddress}</th><td>Musterstraße 1<br />10115 Berlin<br />Deutschland</td></tr>
</tbody>
</table>
{/* Echte DSGVO-Selbstbedienungs-Rechte (Art. 15/17/20) — nur relevant/sichtbar, wenn
eine echte Google-/PayPal-Sitzung aktiv ist (siehe versucheEchteSessionZuUebernehmen()
im Skript unten). Bei einer reinen Demo-/Vorschau-Sitzung ohne Server-Konto gibt es
nichts zu exportieren/löschen, daher standardmäßig ausgeblendet. */}
<div class="profile-dsgvo-actions" id="profile-dsgvo-actions" style="display:none;">
<a class="btn btn-outline btn-sm" href="/api/account/export" id="dsgvo-export-btn">{t.accountDash.exportDataButton}</a>
<button type="button" class="btn btn-outline btn-sm" id="dsgvo-delete-btn">{t.accountDash.deleteAccountButton}</button>
<p class="small" id="dsgvo-delete-error" style="display:none; color: var(--c-sale);">{t.accountDash.deleteAccountError}</p>
</div>
</div>
</div>
</section>
@@ -528,8 +538,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</section>
</Layout>
<script define:vars={{ loginPath: "/ch/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, nichtBuchbar: t.accountDash.aboNichtBuchbar, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels };
<script define:vars={{ loginPath: "/ch/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, nichtBuchbar: t.accountDash.aboNichtBuchbar, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -616,16 +626,33 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
setActiveTab(null);
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../../scripts/account";
import { getAccount, isLoggedIn, login, setName, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
import { fuegeTeddyGratisHinzu } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { aboStufenKonfig } from "../../../data/abo";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels } = (window as any).__accountDashVars;
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
// JS geprüft und bei fehlender Anmeldung sofort zur Login-Seite weitergeleitet.
// Echte Server-Sitzung (Google-/PayPal-Login, siehe functions/_shared/customer-auth.js)
// einbinden: falls vorhanden, in das bestehende localStorage-Demo-Kontosystem übernehmen,
// damit das unveränderte Dashboard unten korrekt mit echten Daten befüllt wird. Läuft VOR dem
// Zugriffsschutz-Check, damit ein frisch per OAuth angemeldeter Besuch nicht fälschlich zurück
// zur Login-Seite geschickt wird.
let istEchteSitzung = false;
try {
const res = await fetch("/api/account/me");
const data = await res.json();
if (data?.ok && data.customer) {
istEchteSitzung = true;
if (!isLoggedIn()) login(data.customer.email);
if (data.customer.name) setName(data.customer.name);
}
} catch {
// Kein Backend erreichbar / keine Sitzung — Demo-/Vorschau-Konto (falls vorhanden) läuft normal weiter.
}
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht (echt oder Demo-
// Vorschau). Bei fehlender Anmeldung sofort zur Login-Seite weitergeleitet.
if (!isLoggedIn()) {
const note = document.getElementById("not-logged-in-note");
const preview = document.getElementById("preview-note");
@@ -927,10 +954,33 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
});
document.getElementById("profile-photo-remove")?.addEventListener("click", () => setPhoto(""));
// Logout direkt aus dem Dashboard heraus
// Logout direkt aus dem Dashboard heraus — löscht sowohl das lokale Demo-Konto als auch,
// falls vorhanden, die echte serverseitige Sitzung (Cookie).
document.getElementById("dash-logout-btn")?.addEventListener("click", () => {
logout();
window.location.href = loginPath;
if (istEchteSitzung) {
fetch("/api/account/logout", { method: "POST" }).finally(() => { window.location.href = loginPath; });
} else {
window.location.href = loginPath;
}
});
// Echte DSGVO-Selbstbedienungs-Rechte (Art. 15/17/20 DSGVO) — nur sichtbar/aktiv bei einer
// echten Server-Sitzung, siehe istEchteSitzung oben.
const dsgvoBlock = document.getElementById("profile-dsgvo-actions");
if (istEchteSitzung && dsgvoBlock) dsgvoBlock.style.display = "flex";
document.getElementById("dsgvo-delete-btn")?.addEventListener("click", async () => {
if (!window.confirm(deleteAccountConfirm)) return;
const errorEl = document.getElementById("dsgvo-delete-error");
try {
const res = await fetch("/api/account/delete", { method: "POST" });
const data = await res.json();
if (!data?.ok) throw new Error("delete failed");
logout();
window.location.href = loginPath;
} catch {
if (errorEl) errorEl.style.display = "block";
}
});
// Rechnung direkt auf der Seite ansehen: natives <dialog> pro Bestellung, geöffnet/geschlossen
+48 -19
View File
@@ -19,25 +19,24 @@ const t = useTranslations(lang);
<form class="frm card" id="login-form">
<h3>{t.account.loginTitle}</h3>
{/* Social-Login: Google als verbreitetster Anbieter, PayPal zusätzlich wegen seiner
Marktdominanz bei Zahlungen in Deutschland (siehe Recherche). Apple bewusst NICHT
dabei -- auf ausdrücklichen Wunsch entfernt (kostet 99 $/Jahr Apple-Entwicklerprogramm
allein für den Web-Login, siehe Vault-Notiz). Ohne eigenes Backend noch nicht
funktional; ein Klick zeigt ehrlich den Phase-2-Hinweis statt eine Anmeldung
vorzutäuschen. */}
{/* Echtes Google-/PayPal-Login (OAuth 2.0 + PKCE, server-seitig geprüft — siehe
functions/api/auth/ + functions/_shared/oauth.js). Ganz normale Links, kein JS
nötig, damit der Login auch ohne JavaScript funktioniert (progressive enhancement).
Apple bewusst NICHT dabei — auf ausdrücklichen Wunsch entfernt (kostet 99 $/Jahr
Apple-Entwicklerprogramm allein für den Web-Login, siehe Vault-Notiz). */}
<div class="social-login">
<p class="small" style="margin:0 0 -0.2rem;">{t.account.socialHeading}</p>
<button type="button" class="btn-social btn-social-google">
<a class="btn-social btn-social-google" href="/api/auth/google/start">
<svg width="18" height="18" viewBox="0 0 48 48" aria-hidden="true"><path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"/><path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"/><path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"/><path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"/></svg>
{t.account.socialGoogle}
</button>
<button type="button" class="btn-social btn-social-paypal">
</a>
<a class="btn-social btn-social-paypal" href="/api/auth/paypal/start">
<span class="social-icon-badge" aria-hidden="true">
<svg width="13" height="13" viewBox="0 0 24 24" fill="#fff" aria-hidden="true"><path d="M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"></path></svg>
</span>
{t.account.socialPaypal}
</button>
<p class="small social-note" id="social-note">{t.account.socialComingSoon}</p>
</a>
<p class="small social-note" id="social-note" style="display:none;"></p>
</div>
<div class="social-divider">{t.account.orDivider}</div>
@@ -98,8 +97,13 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ loginErrorUnconfigured: t.account.loginErrorUnconfigured, loginErrorDenied: t.account.loginErrorDenied, loginErrorGeneric: t.account.loginErrorGeneric, langParam: "ch" }}>
window.__loginVars = { loginErrorUnconfigured, loginErrorDenied, loginErrorGeneric, langParam };
</script>
<script>
import { login, setName } from "../../../scripts/account";
import { login, setName, isLoggedIn } from "../../../scripts/account";
const { loginErrorUnconfigured, loginErrorDenied, loginErrorGeneric, langParam } = (window as any).__loginVars;
const form = document.getElementById("login-form") as HTMLFormElement | null;
const emailInput = document.getElementById("login-email") as HTMLInputElement | null;
const error = document.getElementById("login-error");
@@ -139,12 +143,37 @@ const t = useTranslations(lang);
if (icon) icon.innerHTML = showing ? EYE_OPEN : EYE_CLOSED;
});
// Social-Login-Buttons: kein echtes OAuth ohne Backend — Klick zeigt stattdessen ehrlich
// den Phase-2-Hinweis (siehe .social-note oben im Markup).
const socialNote = document.getElementById("social-note");
document.querySelectorAll(".btn-social").forEach((btn) => {
btn.addEventListener("click", () => {
if (socialNote) socialNote.style.display = "block";
});
// Echtes Google-/PayPal-Login: Sprache als Query-Parameter mitgeben, damit der Rückweg (siehe
// functions/_shared/oauth-handlers.js) auf die richtige Sprachversion zurückführt.
document.querySelectorAll<HTMLAnchorElement>(".btn-social").forEach((a) => {
if (langParam) a.href = a.href + "?lang=" + langParam;
});
// Fehler-Rückmeldung vom echten OAuth-Ablauf.
const params = new URLSearchParams(window.location.search);
const loginErrorCode = params.get("login_error");
if (loginErrorCode) {
const note = document.getElementById("social-note");
if (note) {
let msg = loginErrorGeneric;
if (loginErrorCode.includes("unconfigured")) msg = loginErrorUnconfigured;
else if (loginErrorCode.includes("denied")) msg = loginErrorDenied;
note.textContent = msg;
note.style.display = "block";
}
}
// Schon eine gültige echte Sitzung? Dann direkt zum Dashboard weiterleiten.
if (!isLoggedIn()) {
fetch("/api/account/me")
.then((r) => r.json())
.then((data) => {
if (data?.ok && data.customer) {
login(data.customer.email);
if (data.customer.name) setName(data.customer.name);
window.location.href = "/ch/konto/angemeldet/";
}
})
.catch(() => {});
}
</script>
+59 -9
View File
@@ -315,6 +315,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<tr><th>{t.accountDash.profileAddress}</th><td>Musterstraße 1<br />10115 Berlin<br />Deutschland</td></tr>
</tbody>
</table>
{/* Echte DSGVO-Selbstbedienungs-Rechte (Art. 15/17/20) — nur relevant/sichtbar, wenn
eine echte Google-/PayPal-Sitzung aktiv ist (siehe versucheEchteSessionZuUebernehmen()
im Skript unten). Bei einer reinen Demo-/Vorschau-Sitzung ohne Server-Konto gibt es
nichts zu exportieren/löschen, daher standardmäßig ausgeblendet. */}
<div class="profile-dsgvo-actions" id="profile-dsgvo-actions" style="display:none;">
<a class="btn btn-outline btn-sm" href="/api/account/export" id="dsgvo-export-btn">{t.accountDash.exportDataButton}</a>
<button type="button" class="btn btn-outline btn-sm" id="dsgvo-delete-btn">{t.accountDash.deleteAccountButton}</button>
<p class="small" id="dsgvo-delete-error" style="display:none; color: var(--c-sale);">{t.accountDash.deleteAccountError}</p>
</div>
</div>
</div>
</section>
@@ -528,8 +538,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</section>
</Layout>
<script define:vars={{ loginPath: "/en/konto/", fallbackName: "Customer", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, nichtBuchbar: t.accountDash.aboNichtBuchbar, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels };
<script define:vars={{ loginPath: "/en/konto/", fallbackName: "Customer", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, nichtBuchbar: t.accountDash.aboNichtBuchbar, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -616,16 +626,33 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
setActiveTab(null);
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../../scripts/account";
import { getAccount, isLoggedIn, login, setName, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
import { fuegeTeddyGratisHinzu } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { aboStufenKonfig } from "../../../data/abo";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels } = (window as any).__accountDashVars;
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
// JS geprüft und bei fehlender Anmeldung sofort zur Login-Seite weitergeleitet.
// Echte Server-Sitzung (Google-/PayPal-Login, siehe functions/_shared/customer-auth.js)
// einbinden: falls vorhanden, in das bestehende localStorage-Demo-Kontosystem übernehmen,
// damit das unveränderte Dashboard unten korrekt mit echten Daten befüllt wird. Läuft VOR dem
// Zugriffsschutz-Check, damit ein frisch per OAuth angemeldeter Besuch nicht fälschlich zurück
// zur Login-Seite geschickt wird.
let istEchteSitzung = false;
try {
const res = await fetch("/api/account/me");
const data = await res.json();
if (data?.ok && data.customer) {
istEchteSitzung = true;
if (!isLoggedIn()) login(data.customer.email);
if (data.customer.name) setName(data.customer.name);
}
} catch {
// Kein Backend erreichbar / keine Sitzung — Demo-/Vorschau-Konto (falls vorhanden) läuft normal weiter.
}
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht (echt oder Demo-
// Vorschau). Bei fehlender Anmeldung sofort zur Login-Seite weitergeleitet.
if (!isLoggedIn()) {
const note = document.getElementById("not-logged-in-note");
const preview = document.getElementById("preview-note");
@@ -927,10 +954,33 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
});
document.getElementById("profile-photo-remove")?.addEventListener("click", () => setPhoto(""));
// Logout direkt aus dem Dashboard heraus
// Logout direkt aus dem Dashboard heraus — löscht sowohl das lokale Demo-Konto als auch,
// falls vorhanden, die echte serverseitige Sitzung (Cookie).
document.getElementById("dash-logout-btn")?.addEventListener("click", () => {
logout();
window.location.href = loginPath;
if (istEchteSitzung) {
fetch("/api/account/logout", { method: "POST" }).finally(() => { window.location.href = loginPath; });
} else {
window.location.href = loginPath;
}
});
// Echte DSGVO-Selbstbedienungs-Rechte (Art. 15/17/20 DSGVO) — nur sichtbar/aktiv bei einer
// echten Server-Sitzung, siehe istEchteSitzung oben.
const dsgvoBlock = document.getElementById("profile-dsgvo-actions");
if (istEchteSitzung && dsgvoBlock) dsgvoBlock.style.display = "flex";
document.getElementById("dsgvo-delete-btn")?.addEventListener("click", async () => {
if (!window.confirm(deleteAccountConfirm)) return;
const errorEl = document.getElementById("dsgvo-delete-error");
try {
const res = await fetch("/api/account/delete", { method: "POST" });
const data = await res.json();
if (!data?.ok) throw new Error("delete failed");
logout();
window.location.href = loginPath;
} catch {
if (errorEl) errorEl.style.display = "block";
}
});
// Rechnung direkt auf der Seite ansehen: natives <dialog> pro Bestellung, geöffnet/geschlossen
+45 -19
View File
@@ -19,25 +19,21 @@ const t = useTranslations(lang);
<form class="frm card" id="login-form">
<h3>{t.account.loginTitle}</h3>
{/* Social-Login: Google als verbreitetster Anbieter, PayPal zusätzlich wegen seiner
Marktdominanz bei Zahlungen in Deutschland (siehe Recherche). Apple bewusst NICHT
dabei -- auf ausdrücklichen Wunsch entfernt (kostet 99 $/Jahr Apple-Entwicklerprogramm
allein für den Web-Login, siehe Vault-Notiz). Ohne eigenes Backend noch nicht
funktional; ein Klick zeigt ehrlich den Phase-2-Hinweis statt eine Anmeldung
vorzutäuschen. */}
{/* Real Google/PayPal login (OAuth 2.0 + PKCE, server-verified — see functions/api/auth/
+ functions/_shared/oauth.js). Plain links, no JS needed for the redirect itself. */}
<div class="social-login">
<p class="small" style="margin:0 0 -0.2rem;">{t.account.socialHeading}</p>
<button type="button" class="btn-social btn-social-google">
<a class="btn-social btn-social-google" href="/api/auth/google/start">
<svg width="18" height="18" viewBox="0 0 48 48" aria-hidden="true"><path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"/><path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"/><path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"/><path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"/></svg>
{t.account.socialGoogle}
</button>
<button type="button" class="btn-social btn-social-paypal">
</a>
<a class="btn-social btn-social-paypal" href="/api/auth/paypal/start">
<span class="social-icon-badge" aria-hidden="true">
<svg width="13" height="13" viewBox="0 0 24 24" fill="#fff" aria-hidden="true"><path d="M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"></path></svg>
</span>
{t.account.socialPaypal}
</button>
<p class="small social-note" id="social-note">{t.account.socialComingSoon}</p>
</a>
<p class="small social-note" id="social-note" style="display:none;"></p>
</div>
<div class="social-divider">{t.account.orDivider}</div>
@@ -98,8 +94,13 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ loginErrorUnconfigured: t.account.loginErrorUnconfigured, loginErrorDenied: t.account.loginErrorDenied, loginErrorGeneric: t.account.loginErrorGeneric, langParam: "en" }}>
window.__loginVars = { loginErrorUnconfigured, loginErrorDenied, loginErrorGeneric, langParam };
</script>
<script>
import { login, setName } from "../../../scripts/account";
import { login, setName, isLoggedIn } from "../../../scripts/account";
const { loginErrorUnconfigured, loginErrorDenied, loginErrorGeneric, langParam } = (window as any).__loginVars;
const form = document.getElementById("login-form") as HTMLFormElement | null;
const emailInput = document.getElementById("login-email") as HTMLInputElement | null;
const error = document.getElementById("login-error");
@@ -139,12 +140,37 @@ const t = useTranslations(lang);
if (icon) icon.innerHTML = showing ? EYE_OPEN : EYE_CLOSED;
});
// Social-Login-Buttons: kein echtes OAuth ohne Backend — Klick zeigt stattdessen ehrlich
// den Phase-2-Hinweis (siehe .social-note oben im Markup).
const socialNote = document.getElementById("social-note");
document.querySelectorAll(".btn-social").forEach((btn) => {
btn.addEventListener("click", () => {
if (socialNote) socialNote.style.display = "block";
});
// Echtes Google-/PayPal-Login: Sprache als Query-Parameter mitgeben, damit der Rückweg (siehe
// functions/_shared/oauth-handlers.js) auf die richtige Sprachversion zurückführt.
document.querySelectorAll<HTMLAnchorElement>(".btn-social").forEach((a) => {
if (langParam) a.href = a.href + "?lang=" + langParam;
});
// Fehler-Rückmeldung vom echten OAuth-Ablauf.
const params = new URLSearchParams(window.location.search);
const loginErrorCode = params.get("login_error");
if (loginErrorCode) {
const note = document.getElementById("social-note");
if (note) {
let msg = loginErrorGeneric;
if (loginErrorCode.includes("unconfigured")) msg = loginErrorUnconfigured;
else if (loginErrorCode.includes("denied")) msg = loginErrorDenied;
note.textContent = msg;
note.style.display = "block";
}
}
// Schon eine gültige echte Sitzung? Dann direkt zum Dashboard weiterleiten.
if (!isLoggedIn()) {
fetch("/api/account/me")
.then((r) => r.json())
.then((data) => {
if (data?.ok && data.customer) {
login(data.customer.email);
if (data.customer.name) setName(data.customer.name);
window.location.href = "/en/konto/angemeldet/";
}
})
.catch(() => {});
}
</script>
+59 -9
View File
@@ -315,6 +315,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<tr><th>{t.accountDash.profileAddress}</th><td>Musterstraße 1<br />10115 Berlin<br />Deutschland</td></tr>
</tbody>
</table>
{/* Echte DSGVO-Selbstbedienungs-Rechte (Art. 15/17/20) — nur relevant/sichtbar, wenn
eine echte Google-/PayPal-Sitzung aktiv ist (siehe versucheEchteSessionZuUebernehmen()
im Skript unten). Bei einer reinen Demo-/Vorschau-Sitzung ohne Server-Konto gibt es
nichts zu exportieren/löschen, daher standardmäßig ausgeblendet. */}
<div class="profile-dsgvo-actions" id="profile-dsgvo-actions" style="display:none;">
<a class="btn btn-outline btn-sm" href="/api/account/export" id="dsgvo-export-btn">{t.accountDash.exportDataButton}</a>
<button type="button" class="btn btn-outline btn-sm" id="dsgvo-delete-btn">{t.accountDash.deleteAccountButton}</button>
<p class="small" id="dsgvo-delete-error" style="display:none; color: var(--c-sale);">{t.accountDash.deleteAccountError}</p>
</div>
</div>
</div>
</section>
@@ -528,8 +538,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</section>
</Layout>
<script define:vars={{ loginPath: "/fr/konto/", fallbackName: "Cliente", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, nichtBuchbar: t.accountDash.aboNichtBuchbar, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels };
<script define:vars={{ loginPath: "/fr/konto/", fallbackName: "Cliente", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, nichtBuchbar: t.accountDash.aboNichtBuchbar, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -616,16 +626,33 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
setActiveTab(null);
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../../scripts/account";
import { getAccount, isLoggedIn, login, setName, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
import { fuegeTeddyGratisHinzu } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { aboStufenKonfig } from "../../../data/abo";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels } = (window as any).__accountDashVars;
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
// JS geprüft und bei fehlender Anmeldung sofort zur Login-Seite weitergeleitet.
// Echte Server-Sitzung (Google-/PayPal-Login, siehe functions/_shared/customer-auth.js)
// einbinden: falls vorhanden, in das bestehende localStorage-Demo-Kontosystem übernehmen,
// damit das unveränderte Dashboard unten korrekt mit echten Daten befüllt wird. Läuft VOR dem
// Zugriffsschutz-Check, damit ein frisch per OAuth angemeldeter Besuch nicht fälschlich zurück
// zur Login-Seite geschickt wird.
let istEchteSitzung = false;
try {
const res = await fetch("/api/account/me");
const data = await res.json();
if (data?.ok && data.customer) {
istEchteSitzung = true;
if (!isLoggedIn()) login(data.customer.email);
if (data.customer.name) setName(data.customer.name);
}
} catch {
// Kein Backend erreichbar / keine Sitzung — Demo-/Vorschau-Konto (falls vorhanden) läuft normal weiter.
}
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht (echt oder Demo-
// Vorschau). Bei fehlender Anmeldung sofort zur Login-Seite weitergeleitet.
if (!isLoggedIn()) {
const note = document.getElementById("not-logged-in-note");
const preview = document.getElementById("preview-note");
@@ -927,10 +954,33 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
});
document.getElementById("profile-photo-remove")?.addEventListener("click", () => setPhoto(""));
// Logout direkt aus dem Dashboard heraus
// Logout direkt aus dem Dashboard heraus — löscht sowohl das lokale Demo-Konto als auch,
// falls vorhanden, die echte serverseitige Sitzung (Cookie).
document.getElementById("dash-logout-btn")?.addEventListener("click", () => {
logout();
window.location.href = loginPath;
if (istEchteSitzung) {
fetch("/api/account/logout", { method: "POST" }).finally(() => { window.location.href = loginPath; });
} else {
window.location.href = loginPath;
}
});
// Echte DSGVO-Selbstbedienungs-Rechte (Art. 15/17/20 DSGVO) — nur sichtbar/aktiv bei einer
// echten Server-Sitzung, siehe istEchteSitzung oben.
const dsgvoBlock = document.getElementById("profile-dsgvo-actions");
if (istEchteSitzung && dsgvoBlock) dsgvoBlock.style.display = "flex";
document.getElementById("dsgvo-delete-btn")?.addEventListener("click", async () => {
if (!window.confirm(deleteAccountConfirm)) return;
const errorEl = document.getElementById("dsgvo-delete-error");
try {
const res = await fetch("/api/account/delete", { method: "POST" });
const data = await res.json();
if (!data?.ok) throw new Error("delete failed");
logout();
window.location.href = loginPath;
} catch {
if (errorEl) errorEl.style.display = "block";
}
});
// Rechnung direkt auf der Seite ansehen: natives <dialog> pro Bestellung, geöffnet/geschlossen
+48 -19
View File
@@ -19,25 +19,24 @@ const t = useTranslations(lang);
<form class="frm card" id="login-form">
<h3>{t.account.loginTitle}</h3>
{/* Social-Login: Google als verbreitetster Anbieter, PayPal zusätzlich wegen seiner
Marktdominanz bei Zahlungen in Deutschland (siehe Recherche). Apple bewusst NICHT
dabei -- auf ausdrücklichen Wunsch entfernt (kostet 99 $/Jahr Apple-Entwicklerprogramm
allein für den Web-Login, siehe Vault-Notiz). Ohne eigenes Backend noch nicht
funktional; ein Klick zeigt ehrlich den Phase-2-Hinweis statt eine Anmeldung
vorzutäuschen. */}
{/* Echtes Google-/PayPal-Login (OAuth 2.0 + PKCE, server-seitig geprüft — siehe
functions/api/auth/ + functions/_shared/oauth.js). Ganz normale Links, kein JS
nötig, damit der Login auch ohne JavaScript funktioniert (progressive enhancement).
Apple bewusst NICHT dabei — auf ausdrücklichen Wunsch entfernt (kostet 99 $/Jahr
Apple-Entwicklerprogramm allein für den Web-Login, siehe Vault-Notiz). */}
<div class="social-login">
<p class="small" style="margin:0 0 -0.2rem;">{t.account.socialHeading}</p>
<button type="button" class="btn-social btn-social-google">
<a class="btn-social btn-social-google" href="/api/auth/google/start">
<svg width="18" height="18" viewBox="0 0 48 48" aria-hidden="true"><path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"/><path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"/><path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"/><path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"/></svg>
{t.account.socialGoogle}
</button>
<button type="button" class="btn-social btn-social-paypal">
</a>
<a class="btn-social btn-social-paypal" href="/api/auth/paypal/start">
<span class="social-icon-badge" aria-hidden="true">
<svg width="13" height="13" viewBox="0 0 24 24" fill="#fff" aria-hidden="true"><path d="M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"></path></svg>
</span>
{t.account.socialPaypal}
</button>
<p class="small social-note" id="social-note">{t.account.socialComingSoon}</p>
</a>
<p class="small social-note" id="social-note" style="display:none;"></p>
</div>
<div class="social-divider">{t.account.orDivider}</div>
@@ -98,8 +97,13 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ loginErrorUnconfigured: t.account.loginErrorUnconfigured, loginErrorDenied: t.account.loginErrorDenied, loginErrorGeneric: t.account.loginErrorGeneric, langParam: "fr" }}>
window.__loginVars = { loginErrorUnconfigured, loginErrorDenied, loginErrorGeneric, langParam };
</script>
<script>
import { login, setName } from "../../../scripts/account";
import { login, setName, isLoggedIn } from "../../../scripts/account";
const { loginErrorUnconfigured, loginErrorDenied, loginErrorGeneric, langParam } = (window as any).__loginVars;
const form = document.getElementById("login-form") as HTMLFormElement | null;
const emailInput = document.getElementById("login-email") as HTMLInputElement | null;
const error = document.getElementById("login-error");
@@ -139,12 +143,37 @@ const t = useTranslations(lang);
if (icon) icon.innerHTML = showing ? EYE_OPEN : EYE_CLOSED;
});
// Social-Login-Buttons: kein echtes OAuth ohne Backend — Klick zeigt stattdessen ehrlich
// den Phase-2-Hinweis (siehe .social-note oben im Markup).
const socialNote = document.getElementById("social-note");
document.querySelectorAll(".btn-social").forEach((btn) => {
btn.addEventListener("click", () => {
if (socialNote) socialNote.style.display = "block";
});
// Echtes Google-/PayPal-Login: Sprache als Query-Parameter mitgeben, damit der Rückweg (siehe
// functions/_shared/oauth-handlers.js) auf die richtige Sprachversion zurückführt.
document.querySelectorAll<HTMLAnchorElement>(".btn-social").forEach((a) => {
if (langParam) a.href = a.href + "?lang=" + langParam;
});
// Fehler-Rückmeldung vom echten OAuth-Ablauf.
const params = new URLSearchParams(window.location.search);
const loginErrorCode = params.get("login_error");
if (loginErrorCode) {
const note = document.getElementById("social-note");
if (note) {
let msg = loginErrorGeneric;
if (loginErrorCode.includes("unconfigured")) msg = loginErrorUnconfigured;
else if (loginErrorCode.includes("denied")) msg = loginErrorDenied;
note.textContent = msg;
note.style.display = "block";
}
}
// Schon eine gültige echte Sitzung? Dann direkt zum Dashboard weiterleiten.
if (!isLoggedIn()) {
fetch("/api/account/me")
.then((r) => r.json())
.then((data) => {
if (data?.ok && data.customer) {
login(data.customer.email);
if (data.customer.name) setName(data.customer.name);
window.location.href = "/fr/konto/angemeldet/";
}
})
.catch(() => {});
}
</script>
+59 -9
View File
@@ -316,6 +316,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<tr><th>{t.accountDash.profileAddress}</th><td>Musterstraße 1<br />10115 Berlin<br />Deutschland</td></tr>
</tbody>
</table>
{/* Echte DSGVO-Selbstbedienungs-Rechte (Art. 15/17/20) — nur relevant/sichtbar, wenn
eine echte Google-/PayPal-Sitzung aktiv ist (siehe versucheEchteSessionZuUebernehmen()
im Skript unten). Bei einer reinen Demo-/Vorschau-Sitzung ohne Server-Konto gibt es
nichts zu exportieren/löschen, daher standardmäßig ausgeblendet. */}
<div class="profile-dsgvo-actions" id="profile-dsgvo-actions" style="display:none;">
<a class="btn btn-outline btn-sm" href="/api/account/export" id="dsgvo-export-btn">{t.accountDash.exportDataButton}</a>
<button type="button" class="btn btn-outline btn-sm" id="dsgvo-delete-btn">{t.accountDash.deleteAccountButton}</button>
<p class="small" id="dsgvo-delete-error" style="display:none; color: var(--c-sale);">{t.accountDash.deleteAccountError}</p>
</div>
</div>
</div>
</section>
@@ -537,8 +547,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</section>
</Layout>
<script define:vars={{ loginPath: "/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, nichtBuchbar: t.accountDash.aboNichtBuchbar, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels };
<script define:vars={{ loginPath: "/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, nichtBuchbar: t.accountDash.aboNichtBuchbar, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -629,16 +639,33 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
setActiveTab(gueltigeHashTabs.includes(hashTab) ? hashTab : null);
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../scripts/account";
import { getAccount, isLoggedIn, login, setName, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../scripts/invoice-pdf";
import { fuegeTeddyGratisHinzu } from "../../scripts/cart";
import { formatPrice } from "../../i18n/format";
import { aboStufenKonfig } from "../../data/abo";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels } = (window as any).__accountDashVars;
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
// JS geprüft und bei fehlender Anmeldung sofort zur Login-Seite weitergeleitet.
// Echte Server-Sitzung (Google-/PayPal-Login, siehe functions/_shared/customer-auth.js)
// einbinden: falls vorhanden, in das bestehende localStorage-Demo-Kontosystem übernehmen,
// damit das unveränderte Dashboard unten korrekt mit echten Daten befüllt wird. Läuft VOR dem
// Zugriffsschutz-Check, damit ein frisch per OAuth angemeldeter Besuch nicht fälschlich zurück
// zur Login-Seite geschickt wird.
let istEchteSitzung = false;
try {
const res = await fetch("/api/account/me");
const data = await res.json();
if (data?.ok && data.customer) {
istEchteSitzung = true;
if (!isLoggedIn()) login(data.customer.email);
if (data.customer.name) setName(data.customer.name);
}
} catch {
// Kein Backend erreichbar / keine Sitzung — Demo-/Vorschau-Konto (falls vorhanden) läuft normal weiter.
}
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht (echt oder Demo-
// Vorschau). Bei fehlender Anmeldung sofort zur Login-Seite weitergeleitet.
if (!isLoggedIn()) {
const note = document.getElementById("not-logged-in-note");
const preview = document.getElementById("preview-note");
@@ -943,10 +970,33 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
});
document.getElementById("profile-photo-remove")?.addEventListener("click", () => setPhoto(""));
// Logout direkt aus dem Dashboard heraus
// Logout direkt aus dem Dashboard heraus — löscht sowohl das lokale Demo-Konto als auch,
// falls vorhanden, die echte serverseitige Sitzung (Cookie).
document.getElementById("dash-logout-btn")?.addEventListener("click", () => {
logout();
window.location.href = loginPath;
if (istEchteSitzung) {
fetch("/api/account/logout", { method: "POST" }).finally(() => { window.location.href = loginPath; });
} else {
window.location.href = loginPath;
}
});
// Echte DSGVO-Selbstbedienungs-Rechte (Art. 15/17/20 DSGVO) — nur sichtbar/aktiv bei einer
// echten Server-Sitzung, siehe istEchteSitzung oben.
const dsgvoBlock = document.getElementById("profile-dsgvo-actions");
if (istEchteSitzung && dsgvoBlock) dsgvoBlock.style.display = "flex";
document.getElementById("dsgvo-delete-btn")?.addEventListener("click", async () => {
if (!window.confirm(deleteAccountConfirm)) return;
const errorEl = document.getElementById("dsgvo-delete-error");
try {
const res = await fetch("/api/account/delete", { method: "POST" });
const data = await res.json();
if (!data?.ok) throw new Error("delete failed");
logout();
window.location.href = loginPath;
} catch {
if (errorEl) errorEl.style.display = "block";
}
});
// Rechnung direkt auf der Seite ansehen: natives <dialog> pro Bestellung, geöffnet/geschlossen
+52 -19
View File
@@ -19,25 +19,24 @@ const t = useTranslations(lang);
<form class="frm card" id="login-form">
<h3>{t.account.loginTitle}</h3>
{/* Social-Login: Google als verbreitetster Anbieter, PayPal zusätzlich wegen seiner
Marktdominanz bei Zahlungen in Deutschland (siehe Recherche). Apple bewusst NICHT
dabei -- auf ausdrücklichen Wunsch entfernt (kostet 99 $/Jahr Apple-Entwicklerprogramm
allein für den Web-Login, siehe Vault-Notiz). Ohne eigenes Backend noch nicht
funktional; ein Klick zeigt ehrlich den Phase-2-Hinweis statt eine Anmeldung
vorzutäuschen. */}
{/* Echtes Google-/PayPal-Login (OAuth 2.0 + PKCE, server-seitig geprüft — siehe
functions/api/auth/ + functions/_shared/oauth.js). Ganz normale Links, kein JS
nötig, damit der Login auch ohne JavaScript funktioniert (progressive enhancement).
Apple bewusst NICHT dabei — auf ausdrücklichen Wunsch entfernt (kostet 99 $/Jahr
Apple-Entwicklerprogramm allein für den Web-Login, siehe Vault-Notiz). */}
<div class="social-login">
<p class="small" style="margin:0 0 -0.2rem;">{t.account.socialHeading}</p>
<button type="button" class="btn-social btn-social-google">
<a class="btn-social btn-social-google" href="/api/auth/google/start">
<svg width="18" height="18" viewBox="0 0 48 48" aria-hidden="true"><path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"/><path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"/><path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"/><path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"/></svg>
{t.account.socialGoogle}
</button>
<button type="button" class="btn-social btn-social-paypal">
</a>
<a class="btn-social btn-social-paypal" href="/api/auth/paypal/start">
<span class="social-icon-badge" aria-hidden="true">
<svg width="13" height="13" viewBox="0 0 24 24" fill="#fff" aria-hidden="true"><path d="M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"></path></svg>
</span>
{t.account.socialPaypal}
</button>
<p class="small social-note" id="social-note">{t.account.socialComingSoon}</p>
</a>
<p class="small social-note" id="social-note" style="display:none;"></p>
</div>
<div class="social-divider">{t.account.orDivider}</div>
@@ -98,8 +97,13 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ loginErrorUnconfigured: t.account.loginErrorUnconfigured, loginErrorDenied: t.account.loginErrorDenied, loginErrorGeneric: t.account.loginErrorGeneric, langParam: "" }}>
window.__loginVars = { loginErrorUnconfigured, loginErrorDenied, loginErrorGeneric, langParam };
</script>
<script>
import { login, setName } from "../../scripts/account";
import { login, setName, isLoggedIn } from "../../scripts/account";
const { loginErrorUnconfigured, loginErrorDenied, loginErrorGeneric, langParam } = (window as any).__loginVars;
const form = document.getElementById("login-form") as HTMLFormElement | null;
const emailInput = document.getElementById("login-email") as HTMLInputElement | null;
const error = document.getElementById("login-error");
@@ -139,12 +143,41 @@ const t = useTranslations(lang);
if (icon) icon.innerHTML = showing ? EYE_OPEN : EYE_CLOSED;
});
// Social-Login-Buttons: kein echtes OAuth ohne Backend — Klick zeigt stattdessen ehrlich
// den Phase-2-Hinweis (siehe .social-note oben im Markup).
const socialNote = document.getElementById("social-note");
document.querySelectorAll(".btn-social").forEach((btn) => {
btn.addEventListener("click", () => {
if (socialNote) socialNote.style.display = "block";
});
// Echtes Google-/PayPal-Login: Sprache als Query-Parameter mitgeben, damit der Rückweg (siehe
// functions/_shared/oauth-handlers.js) auf die richtige Sprachversion von /konto/angemeldet/
// zurückführt.
document.querySelectorAll<HTMLAnchorElement>(".btn-social").forEach((a) => {
if (langParam) a.href = a.href + "?lang=" + langParam;
});
// Fehler-Rückmeldung vom echten OAuth-Ablauf (siehe functions/_shared/oauth-handlers.js) —
// kommt als ?login_error=... in der URL zurück, wenn z.B. noch keine Google-/PayPal-
// Zugangsdaten hinterlegt sind oder die Anmeldung abgebrochen wurde.
const params = new URLSearchParams(window.location.search);
const loginErrorCode = params.get("login_error");
if (loginErrorCode) {
const note = document.getElementById("social-note");
if (note) {
let msg = loginErrorGeneric;
if (loginErrorCode.includes("unconfigured")) msg = loginErrorUnconfigured;
else if (loginErrorCode.includes("denied")) msg = loginErrorDenied;
note.textContent = msg;
note.style.display = "block";
}
}
// Schon eine gültige echte Sitzung? Dann direkt zum Dashboard weiterleiten, statt das
// Login-Formular nochmal anzuzeigen.
if (!isLoggedIn()) {
fetch("/api/account/me")
.then((r) => r.json())
.then((data) => {
if (data?.ok && data.customer) {
login(data.customer.email);
if (data.customer.name) setName(data.customer.name);
window.location.href = "/konto/angemeldet/";
}
})
.catch(() => {});
}
</script>