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:
co-authored by
Claude Sonnet 5
parent
e80aba5ca6
commit
bf0d169015
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user