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
+11 -7
View File
@@ -21,24 +21,28 @@
"body": "Bei Zahlung per PayPal geben wir die für die Zahlungsabwicklung erforderlichen Daten (u. a. Name, Anschrift, Bestellsumme) an die PayPal (Europe) S.à r.l. et Cie, S.C.A., 22-24 Boulevard Royal, L-2449 Luxembourg, weiter. PayPal verarbeitet diese Daten teilweise auch in den USA über die Muttergesellschaft PayPal, Inc.; PayPal ist nach dem EU-US Data Privacy Framework zertifiziert, das ein angemessenes Datenschutzniveau sicherstellt. Es gelten zusätzlich PayPals eigene Datenschutzhinweise (paypal.com/de/webapps/mpp/ua/privacy-full). Bei Banküberweisung werden Daten an unser kontoführendes Kreditinstitut übermittelt."
},
{
"heading": "6. Kontaktformular",
"heading": "6. Kundenkonto (Anmeldung mit Google oder PayPal)",
"body": "Für ein Kundenkonto bieten wir ausschließlich die Anmeldung über bestehende Google- oder PayPal-Konten an (OAuth 2.0 / OpenID Connect) — ein eigenes Passwort bei uns gibt es nicht. Beim ersten Login übermitteln uns Google bzw. PayPal deine E-Mail-Adresse, deinen Namen und eine eindeutige, anbieterseitige Kennung; wir speichern diese Angaben zusammen mit dem Anmeldezeitpunkt in unserer Datenbank (Cloudflare D1), um dich bei künftigen Besuchen wiederzuerkennen. Die Verarbeitung erfolgt auf Grundlage des Art. 6 Abs. 1 lit. b DSGVO (Erfüllung des Nutzungsvertrags für das Kundenkonto) sowie deiner Einwilligung durch den aktiven Klick auf den jeweiligen Anmelde-Button (Art. 6 Abs. 1 lit. a DSGVO). Verantwortlich für die Verarbeitung deiner Daten bei Google ist die Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland; die Muttergesellschaft Google LLC (USA) ist nach unserer Kenntnis nach dem EU-US Data Privacy Framework zertifiziert, das ein angemessenes Datenschutzniveau sicherstellt. Für PayPal gelten dieselben Angaben wie unter Punkt 5 (PayPal (Europe) S.à r.l. et Cie, S.C.A., DPF-Zertifizierung der US-Muttergesellschaft). Es gelten zusätzlich die jeweiligen Datenschutzhinweise von Google (policies.google.com/privacy) bzw. PayPal (paypal.com/de/webapps/mpp/ua/privacy-full). Du kannst dein Kundenkonto jederzeit selbst und ohne Rückfrage in deinem Kontobereich unwiderruflich löschen (Art. 17 DSGVO) oder deine gespeicherten Kontodaten als Datei herunterladen (Art. 15, 20 DSGVO); bereits aufgegebene Bestellungen bleiben davon unberührt, da für sie eigenständige, gesetzliche Aufbewahrungspflichten gelten (siehe Punkt 10)."
},
{
"heading": "7. Kontaktformular",
"body": "Wenn du uns über das Kontaktformular oder per E-Mail Anfragen zukommen lässt, werden deine Angaben aus dem Anfrageformular inklusive der von dir dort angegebenen Kontaktdaten zwecks Bearbeitung der Anfrage und für den Fall von Anschlussfragen bei uns gespeichert. Die Verarbeitung dieser Daten erfolgt auf Grundlage deiner Einwilligung (Art. 6 Abs. 1 lit. a DSGVO) bzw., sofern die Anfrage der Anbahnung eines Vertrags dient, auf Grundlage des Art. 6 Abs. 1 lit. b DSGVO. Eine erteilte Einwilligung kannst du jederzeit mit Wirkung für die Zukunft widerrufen, etwa per E-Mail an die oben genannte Adresse."
},
{
"heading": "7. Cookies",
"body": "Diese Website verwendet, soweit technisch erforderlich, Cookies zur Bereitstellung des Warenkorbs. Weitere, nicht zwingend erforderliche Cookies (z. B. Statistik/Marketing) werden nur nach vorheriger Einwilligung gesetzt. [Cookie-Banner/Consent-Tool ergänzen, sobald solche Cookies tatsächlich eingesetzt werden.]"
"heading": "8. Cookies",
"body": "Diese Website verwendet, soweit technisch erforderlich, Cookies zur Bereitstellung des Warenkorbs sowie — sofern du dich anmeldest — zur Aufrechterhaltung deiner Anmeldesitzung. Weitere, nicht zwingend erforderliche Cookies (z. B. Statistik/Marketing) werden nur nach vorheriger Einwilligung gesetzt. [Cookie-Banner/Consent-Tool ergänzen, sobald solche Cookies tatsächlich eingesetzt werden.]"
},
{
"heading": "8. Deine Rechte",
"heading": "9. Deine Rechte",
"body": "Du hast jederzeit das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16 DSGVO), Löschung (Art. 17 DSGVO), Einschränkung der Verarbeitung (Art. 18 DSGVO), Datenübertragbarkeit (Art. 20 DSGVO) und Widerspruch gegen die Verarbeitung deiner personenbezogenen Daten (Art. 21 DSGVO). Soweit die Verarbeitung auf deiner Einwilligung beruht, kannst du diese jederzeit mit Wirkung für die Zukunft widerrufen (Art. 7 Abs. 3 DSGVO), ohne dass die Rechtmäßigkeit der bis zum Widerruf erfolgten Verarbeitung berührt wird. Außerdem hast du das Recht, dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren — insbesondere in dem Mitgliedstaat deines gewöhnlichen Aufenthaltsorts, deines Arbeitsplatzes oder des Orts des mutmaßlichen Verstoßes."
},
{
"heading": "9. Automatisierte Entscheidungsfindung",
"heading": "10. Automatisierte Entscheidungsfindung",
"body": "Wir setzen keine automatisierte Entscheidungsfindung einschließlich Profiling im Sinne von Art. 22 DSGVO ein."
},
{
"heading": "10. Speicherdauer",
"body": "Wir speichern personenbezogene Daten nur so lange, wie es für den jeweiligen Zweck erforderlich ist oder gesetzliche Aufbewahrungsfristen (insbesondere handels- und steuerrechtlich) dies vorschreiben."
"heading": "11. Speicherdauer",
"body": "Wir speichern personenbezogene Daten nur so lange, wie es für den jeweiligen Zweck erforderlich ist oder gesetzliche Aufbewahrungsfristen (insbesondere handels- und steuerrechtlich) dies vorschreiben. Dein Kundenkonto (Google-/PayPal-Login) bleibt bestehen, bis du es selbst löschst; bereits aufgegebene Bestellungen werden davon unabhängig aufgrund handels- und steuerrechtlicher Aufbewahrungsfristen (§ 257 HGB, § 147 AO) für die gesetzlich vorgeschriebene Dauer weitergespeichert."
}
]
}
+28 -8
View File
@@ -200,14 +200,16 @@ export const ui = {
},
account: {
eyebrow: "Mein Konto", title: "Anmelden oder als Gast bestellen",
todoNote: "Phase 1 Demo-Anmeldung ohne echte Passwortprüfung: Jede E-Mail-Adresse funktioniert, das Passwort-Feld ist reine Vorschau. Die Bestellungen im Konto sind Beispieldaten. Eine echte Anmeldung mit Datenbank kommt in Phase 2.",
todoNote: "Anmeldung mit Google/PayPal ist technisch fertig und echt (eigenes Kundenkonto, sichere Sitzung) — bis Google/PayPal-Zugangsdaten hinterlegt sind, meldet der Button einen klaren Hinweis statt eine Anmeldung vorzutäuschen. Die E-Mail/Passwort-Anmeldung darunter ist weiterhin eine Vorschau ohne echte Passwortprüfung.",
loginTitle: "Anmelden", email: "E-Mail", password: "Passwort", loginButton: "Anmelden", forgotPassword: "Passwort vergessen?",
newTitle: "Neu hier?", newText: "Lege ein Konto an, um Bestellhistorie, Wunschliste und Sendungsverfolgung an einem Ort zu haben oder bestelle einfach als Gast, ganz ohne Konto.",
guestButton: "Als Gast weiter einkaufen",
loginError: "Bitte gib eine E-Mail-Adresse ein.",
socialHeading: "Schneller anmelden mit",
socialGoogle: "Mit Google anmelden", socialPaypal: "Mit PayPal anmelden",
socialComingSoon: "Kommt in Phase 2: Sobald die echte Konto-Anbindung steht, kannst du dich hier direkt anmelden — ganz ohne neues Passwort.",
loginErrorUnconfigured: "Diese Anmeldeart ist noch nicht eingerichtet. Bitte melde dich per E-Mail an oder versuche es später erneut.",
loginErrorDenied: "Anmeldung abgebrochen.",
loginErrorGeneric: "Anmeldung fehlgeschlagen. Bitte versuche es erneut.",
orDivider: "oder mit E-Mail",
showPassword: "Passwort anzeigen", hidePassword: "Passwort verbergen",
rememberMe: "Angemeldet bleiben",
@@ -220,6 +222,9 @@ export const ui = {
previewNote: "Phase 1: Deine Anmeldung, dein Profil und deine Rezensionen werden schon jetzt echt auf diesem Gerät gespeichert. Nur die Bestellungen unten sind Beispieldaten zur Gestaltung — echte Bestellungen kommen in Phase 2 mit Zahlungsanbindung und Datenbank (Cloudflare Worker + D1).",
notLoggedIn: "Du bist nicht angemeldet — du wirst zur Anmeldung weitergeleitet …",
logout: "Abmelden",
exportDataButton: "📥 Meine Daten herunterladen", deleteAccountButton: "🗑️ Konto löschen",
deleteAccountConfirm: "Dein Konto wird unwiderruflich gelöscht (nicht deine bereits aufgegebenen Bestellungen, die aus gesetzlichen Gründen aufbewahrt werden müssen). Fortfahren?",
deleteAccountError: "Konto konnte nicht gelöscht werden. Bitte versuche es erneut.",
navLabel: "Kontobereich-Navigation",
statOrders: "Bestellungen", statWishlist: "Wunschliste", statOpen: "Offene Sendungen", statData: "Meine Daten", statLoyalty: "Treuebonus",
datenHeaderBtn: "👤 Meine Daten",
@@ -502,14 +507,16 @@ export const ui = {
},
account: {
eyebrow: "My Account", title: "Sign in or check out as a guest",
todoNote: "Phase 1 demo sign-in without real password checking: any email address works, the password field is a preview only. Orders shown in the account are example data. A real login with a database is coming in Phase 2.",
todoNote: "Sign-in with Google/PayPal is technically finished and real (your own customer account, secure session) — until Google/PayPal credentials are set up, the button shows a clear notice instead of faking a sign-in. The email/password sign-in below remains a preview without real password checking.",
loginTitle: "Sign in", email: "Email", password: "Password", loginButton: "Sign in", forgotPassword: "Forgot password?",
newTitle: "New here?", newText: "Create an account to keep order history, wishlist and tracking in one place or simply check out as a guest, no account needed.",
guestButton: "Continue shopping as guest",
loginError: "Please enter an email address.",
socialHeading: "Sign in faster with",
socialGoogle: "Sign in with Google", socialPaypal: "Sign in with PayPal",
socialComingSoon: "Coming in Phase 2: once the real account backend is live, you'll be able to sign in here directly — no new password needed.",
loginErrorUnconfigured: "This sign-in method isn't set up yet. Please sign in by email or try again later.",
loginErrorDenied: "Sign-in cancelled.",
loginErrorGeneric: "Sign-in failed. Please try again.",
orDivider: "or with email",
showPassword: "Show password", hidePassword: "Hide password",
rememberMe: "Keep me signed in",
@@ -522,6 +529,9 @@ export const ui = {
previewNote: "Phase 1: your sign-in, your profile and your reviews are already genuinely saved on this device. Only the orders below are example data for design purposes — real orders arrive in Phase 2 with payment integration and a database (Cloudflare Worker + D1).",
notLoggedIn: "You're not signed in — redirecting you to sign in …",
logout: "Sign out",
exportDataButton: "📥 Download my data", deleteAccountButton: "🗑️ Delete account",
deleteAccountConfirm: "Your account will be deleted permanently (not your past orders, which must be kept for legal reasons). Continue?",
deleteAccountError: "Account could not be deleted. Please try again.",
navLabel: "Account section navigation",
statOrders: "Orders", statWishlist: "Wishlist", statOpen: "Open shipments", statData: "My details", statLoyalty: "Loyalty reward",
datenHeaderBtn: "👤 My details",
@@ -804,14 +814,16 @@ export const ui = {
},
account: {
eyebrow: "Mis Konto", title: "Aamälde oder als Gast bstelle",
todoNote: "Phase 1 Demo-Aamäldig ohni echti Passwortprüefig: jedi E-Mail-Adrässe funktioniert, s'Passwort-Fäld isch nume Vorschau. D'Bstellige im Konto sind Bispieldate. E echti Aamäldig mit Datebank chunnt i Phase 2.",
todoNote: "Aamäldig mit Google/PayPal isch technisch fertig und echt (eigets Kundekonto, sicheri Sitzig) — bis Google/PayPal-Zuegangsdate hinterlegt sind, zeigt dr Knopf en klare Hiwys statt e Aamäldig vorztüsche. D'E-Mail-/Passwort-Aamäldig drunder blibt wyterhin nume Vorschau ohni echti Passwortprüefig.",
loginTitle: "Aamälde", email: "E-Mail", password: "Passwort", loginButton: "Aamälde", forgotPassword: "Passwort vergässe?",
newTitle: "Neu da?", newText: "Leg es Konto a, für Bstellhistorie, Wunschlischte und Sändigsverfolgig a einem Ort z'ha oder bstell eifach als Gast, ganz ohni Konto.",
guestButton: "Als Gast wyter yichaufe",
loginError: "Bitte gib e E-Mail-Adrässe i.",
socialHeading: "Schnäller aamälde mit",
socialGoogle: "Mit Google aamälde", socialPaypal: "Mit PayPal aamälde",
socialComingSoon: "Chunnt i Phase 2: Sobald d'echti Konto-Aabindig staht, chasch di da direkt aamälde — ganz ohni nöis Passwort.",
loginErrorUnconfigured: "Die Aamäldeart isch no nid iigrichtet. Bitte mäld di per E-Mail a oder versuech's spöter nochmal.",
loginErrorDenied: "Aamäldig abbroche.",
loginErrorGeneric: "Aamäldig fählgschlage. Bitte versuech's nochmal.",
orDivider: "oder mit E-Mail",
showPassword: "Passwort azeige", hidePassword: "Passwort verstecke",
rememberMe: "Aagmäldet bliebe",
@@ -823,6 +835,9 @@ export const ui = {
eyebrow: "Mis Konto", greeting: (name: string) => `Hoi ${name} 🩵`, lead: "Schön, bisch wieder da. Da findsch alli dini Bstellige, dini Wunschlischte und dini Date uf ein Blick.",
previewNote: "Phase 1: dini Aamäldig, dis Profil und dini Bewärtige werde scho jetzt echt uf däm Gerät gspeicheret. Nume d'Bstellige unde sind Bispieldate für d'Gstaltig — echti Bstellige chöme i Phase 2 mit Zahligsaabindig und Datebank (Cloudflare Worker + D1).",
notLoggedIn: "Du bisch nid aagmäldet — du wirsch zur Aamäldig wytergleitet …",
exportDataButton: "📥 Mini Date abelade", deleteAccountButton: "🗑️ Konto lösche",
deleteAccountConfirm: "Dis Konto wird unwiderruflich glöscht (nid dini scho ufgäbene Bstellige, wo us gsetzlichne Gründ ufbewahrt werde müesse). Wyterfahre?",
deleteAccountError: "Konto het nid chöne glöscht werde. Bitte versuech's nomol.",
logout: "Abmälde",
navLabel: "Kontobereich-Navigation",
statOrders: "Bstellige", statWishlist: "Wunschlischte", statOpen: "Offni Sändige", statData: "Mini Date", statLoyalty: "Treuebonus",
@@ -1106,14 +1121,16 @@ export const ui = {
},
account: {
eyebrow: "Mon compte", title: "Se connecter ou commander en tant qu'invité",
todoNote: "Phase 1 connexion de démonstration sans vérification réelle du mot de passe : n'importe quelle adresse e-mail fonctionne, le champ mot de passe n'est qu'un aperçu. Les commandes affichées dans le compte sont des exemples. Une vraie connexion avec base de données arrivera en phase 2.",
todoNote: "La connexion avec Google/PayPal est techniquement terminée et réelle (votre propre compte client, session sécurisée) — tant que les identifiants Google/PayPal ne sont pas configurés, le bouton affiche un message clair plutôt que de simuler une connexion. La connexion par e-mail/mot de passe ci-dessous reste un aperçu sans vérification réelle du mot de passe.",
loginTitle: "Se connecter", email: "E-mail", password: "Mot de passe", loginButton: "Se connecter", forgotPassword: "Mot de passe oublié ?",
newTitle: "Nouveau ici ?", newText: "Créez un compte pour retrouver l'historique des commandes, la liste de souhaits et le suivi au même endroit ou commandez simplement en tant qu'invité, sans compte.",
guestButton: "Continuer mes achats en tant qu'invité",
loginError: "Veuillez saisir une adresse e-mail.",
socialHeading: "Connexion plus rapide avec",
socialGoogle: "Se connecter avec Google", socialPaypal: "Se connecter avec PayPal",
socialComingSoon: "Disponible en phase 2 : dès que la vraie connexion au compte sera en place, vous pourrez vous connecter directement ici — sans nouveau mot de passe.",
loginErrorUnconfigured: "Ce mode de connexion n'est pas encore configuré. Veuillez vous connecter par e-mail ou réessayer plus tard.",
loginErrorDenied: "Connexion annulée.",
loginErrorGeneric: "Échec de la connexion. Veuillez réessayer.",
orDivider: "ou avec e-mail",
showPassword: "Afficher le mot de passe", hidePassword: "Masquer le mot de passe",
rememberMe: "Rester connecté(e)",
@@ -1125,6 +1142,9 @@ export const ui = {
eyebrow: "Mon compte", greeting: (name: string) => `Bonjour ${name} 🩵`, lead: "Ravie de vous revoir. Retrouvez ici toutes vos commandes, votre liste de souhaits et vos données en un coup d'œil.",
previewNote: "Phase 1 : votre connexion, votre profil et vos avis sont déjà réellement enregistrés sur cet appareil. Seules les commandes ci-dessous sont des exemples destinés à la conception — les vraies commandes arriveront en phase 2 avec le paiement et une base de données (Cloudflare Worker + D1).",
notLoggedIn: "Vous n'êtes pas connecté(e) — redirection vers la connexion …",
exportDataButton: "📥 Télécharger mes données", deleteAccountButton: "🗑️ Supprimer le compte",
deleteAccountConfirm: "Votre compte sera supprimé définitivement (pas vos commandes déjà passées, qui doivent être conservées pour des raisons légales). Continuer ?",
deleteAccountError: "Le compte n'a pas pu être supprimé. Veuillez réessayer.",
logout: "Se déconnecter",
navLabel: "Navigation de l'espace client",
statOrders: "Commandes", statWishlist: "Liste de souhaits", statOpen: "Envois en cours", statData: "Mes données", statLoyalty: "Récompense",
+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>
+5
View File
@@ -1862,6 +1862,11 @@ a:focus-visible, button:focus-visible {
.profile-field-row { display: flex; gap: 0.6rem; }
.profile-field-row input { flex: 1; }
/* DSGVO-Selbstbedienung (Art. 15/17/20) — nur bei echter Server-Sitzung eingeblendet, siehe
istEchteSitzung in angemeldet.astro. */
.profile-dsgvo-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.7rem; margin-top: 1.2rem; padding-top: 1.2rem; border-top: 1px solid rgba(255, 255, 255, 0.12); }
.profile-dsgvo-actions #dsgvo-delete-btn { color: var(--c-sale); border-color: var(--c-sale); }
/* Kundenrezensionen — Herzen in Lila-Babyblau statt Sterne (siehe HeartRating.astro), damit
Bewertungen sich erkennbar in die restliche Seite einfügen statt wie ein fremdes Bewertungs-
Widget zu wirken. */