diff --git a/src/app/konto/actions/account.ts b/src/app/konto/actions/account.ts index a1ad07b..7d223b5 100644 --- a/src/app/konto/actions/account.ts +++ b/src/app/konto/actions/account.ts @@ -22,7 +22,7 @@ export async function deleteOwnAccountAction(formData: FormData): Promise if (!customer) return fail("/konto", "Bitte melde dich an."); if (customer.role === "ADMIN") { - return fail("/konto", "Admin-Konten können nicht selbst gelöscht werden. Bitte kontaktiere einen anderen Admin."); + return fail("/konto/einstellungen", "Admin-Konten können nicht selbst gelöscht werden. Bitte kontaktiere einen anderen Admin."); } const ip = await getClientIp(); @@ -33,13 +33,13 @@ export async function deleteOwnAccountAction(formData: FormData): Promise ACCOUNT_PASSWORD_CONFIRM_WINDOW_MS ) ) { - return fail("/konto", "Zu viele Versuche. Bitte versuche es später erneut."); + return fail("/konto/einstellungen", "Zu viele Versuche. Bitte versuche es später erneut."); } const password = String(formData.get("password") ?? ""); const passwordValid = await verifyPassword(password, customer.passwordHash); if (!passwordValid) { - return fail("/konto", "Passwort ist falsch."); + return fail("/konto/einstellungen", "Passwort ist falsch."); } const { subject, html, text } = await securityAlertEmail(await getLocale(), "accountDeleted"); diff --git a/src/app/konto/actions/newsletter.ts b/src/app/konto/actions/newsletter.ts index e4083c5..6b0febd 100644 --- a/src/app/konto/actions/newsletter.ts +++ b/src/app/konto/actions/newsletter.ts @@ -29,6 +29,6 @@ export async function toggleNewsletterAction(): Promise { }); } - revalidatePath("/konto"); + revalidatePath("/konto/einstellungen"); await setFlash("success", "Deine Erntepost-Einstellung wurde gespeichert."); } diff --git a/src/app/konto/einstellungen/page.tsx b/src/app/konto/einstellungen/page.tsx new file mode 100644 index 0000000..d8922d8 --- /dev/null +++ b/src/app/konto/einstellungen/page.tsx @@ -0,0 +1,141 @@ +import type { Metadata } from "next"; +import { redirect } from "next/navigation"; +import { Container, SectionHeading, Button, LinkButton } from "@/components/ui"; +import { BackLink } from "@/components/back-link"; +import { ShieldIcon } from "@/components/account-ui"; +import { ConfirmForm } from "@/components/confirm-form"; +import { getLocale } from "@/i18n/locale"; +import { getDictionary } from "@/i18n/get-dictionary"; +import { getCurrentCustomer } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { toggleNewsletterAction, logoutAllDevicesAction, deleteOwnAccountAction } from "../actions"; + +export const metadata: Metadata = { title: "Einstellungen" }; +export const dynamic = "force-dynamic"; + +/** + * Buendelt alles, was das Konto selbst betrifft statt einer Bestellung: + * Sicherheit (Zwei-Faktor, Sitzungen) und Datenschutz (Einwilligung in die + * Erntepost, Auskunft nach Art. 15, Loeschung nach Art. 17 DSGVO). Die + * Aktionen selbst leben unveraendert in ../actions — diese Seite ist nur + * ihr gemeinsamer Ort. + */ +export default async function EinstellungenPage() { + const t = getDictionary(await getLocale()); + + const customer = await getCurrentCustomer(); + if (!customer) redirect("/konto"); + + const newsletter = await prisma.newsletterSubscriber.findUnique({ + where: { customerId: customer.id }, + }); + const isSubscribed = Boolean(newsletter && newsletter.unsubscribedAt === null); + + return ( +
+ + + + +
+

+ {t.account.settings.securityHeading} +

+ +
+
+

+ + {t.account.settings.twoFactorHeading} +

+

+ {customer.twoFactorEnabled + ? t.account.settings.twoFactorActiveDescription + : t.account.settings.twoFactorInactiveDescription} +

+
+ + {customer.twoFactorEnabled ? t.account.settings.twoFactorManage : t.account.settings.twoFactorSetup} + +
+ +
+
+

{t.account.logoutAllDevicesHeading}

+

{t.account.logoutAllDevicesDescription}

+
+
+ +
+
+
+ +
+

+ {t.account.privacy.heading} +

+ +
+
+

{t.account.newsletterHeading}

+

+ {isSubscribed ? t.account.newsletterSubscribed : t.account.newsletterNotSubscribed} +

+
+
+ +
+
+ +
+
+

{t.account.privacy.downloadDataHeading}

+

{t.account.privacy.downloadDataDescription}

+
+ + {t.account.privacy.downloadDataButton} + +
+ +
+

{t.account.privacy.deleteAccountHeading}

+ {customer.role === "ADMIN" ? ( +

{t.account.privacy.deleteAccountAdminBlocked}

+ ) : ( + <> +

{t.account.privacy.deleteAccountDescription}

+ + + + + + )} +
+
+
+
+ ); +} diff --git a/src/app/konto/layout.tsx b/src/app/konto/layout.tsx index ec88976..c2c2597 100644 --- a/src/app/konto/layout.tsx +++ b/src/app/konto/layout.tsx @@ -2,7 +2,7 @@ import type { Metadata } from "next"; import { noIndexPrivate } from "@/lib/seo"; // Der Kontobereich enthaelt ausschliesslich personenbezogene Seiten -// (Bestellungen, Adressen, Wunschliste, Zwei-Faktor-Einrichtung) sowie die +// (Bestellungen, Adressen, Wunschliste, Einstellungen, Zwei-Faktor-Einrichtung) sowie die // Anmeldung selbst. Nichts davon gehoert in den Suchindex. // // Gesetzt einmal hier im Layout statt in jeder Unterseite: /konto hat aktuell diff --git a/src/app/konto/page.tsx b/src/app/konto/page.tsx index e134ee6..23d5e61 100644 --- a/src/app/konto/page.tsx +++ b/src/app/konto/page.tsx @@ -1,23 +1,18 @@ import type { Metadata } from "next"; import Link from "next/link"; -import { Container, SectionHeading, Button, LinkButton } from "@/components/ui"; -import { AccountNavCard, OrderIcon, AddressIcon, HeartIcon, ShieldIcon } from "@/components/account-ui"; +import { Container, SectionHeading, Button } from "@/components/ui"; +import { AccountNavCard, OrderIcon, AddressIcon, HeartIcon, ShieldIcon, SettingsIcon } from "@/components/account-ui"; import { TurnstileField } from "@/components/turnstile-widget"; import { PasswordField } from "@/components/password-field"; -import { ConfirmForm } from "@/components/confirm-form"; import { getLocale } from "@/i18n/locale"; import { getDictionary } from "@/i18n/get-dictionary"; import { getCurrentCustomer } from "@/lib/auth"; import { isEntraConfigured } from "@/lib/entra"; -import { prisma } from "@/lib/prisma"; import { registerAction, loginAction, logoutAction, - toggleNewsletterAction, - logoutAllDevicesAction, resendVerificationEmailAction, - deleteOwnAccountAction, } from "./actions"; export const metadata: Metadata = { title: "Mein Konto" }; @@ -111,11 +106,6 @@ export default async function KontoPage() { ); } - const newsletter = await prisma.newsletterSubscriber.findUnique({ - where: { customerId: customer.id }, - }); - const isSubscribed = Boolean(newsletter && newsletter.unsubscribedAt === null); - return (
@@ -155,10 +145,10 @@ export default async function KontoPage() { description={t.account.navWishlist.description} /> } - title={t.account.navSecurity.title} - description={`${t.account.navSecurity.twoFactorLabel}: ${customer.twoFactorEnabled ? t.account.navSecurity.twoFactorActive : t.account.navSecurity.twoFactorInactive}`} + href="/konto/einstellungen" + icon={} + title={t.account.navSettings.title} + description={`${t.account.navSettings.description} · ${t.account.navSettings.twoFactorLabel}: ${customer.twoFactorEnabled ? t.account.navSettings.twoFactorActive : t.account.navSettings.twoFactorInactive}`} /> {customer.role === "ADMIN" ? ( -
-
-

{t.account.newsletterHeading}

-

- {isSubscribed ? t.account.newsletterSubscribed : t.account.newsletterNotSubscribed} -

-
-
- -
-
- -
-
-

{t.account.logoutAllDevicesHeading}

-

- {t.account.logoutAllDevicesDescription} -

-
-
- -
-
- -
+
- -

{t.account.privacy.heading}

-
-
-

{t.account.privacy.downloadDataHeading}

-

{t.account.privacy.downloadDataDescription}

-
- - {t.account.privacy.downloadDataButton} - -
- -
-

{t.account.privacy.deleteAccountHeading}

-

{t.account.privacy.deleteAccountDescription}

- - - - -
); diff --git a/src/app/konto/zwei-faktor/page.tsx b/src/app/konto/zwei-faktor/page.tsx index 83d5e0d..f6f91e1 100644 --- a/src/app/konto/zwei-faktor/page.tsx +++ b/src/app/konto/zwei-faktor/page.tsx @@ -27,7 +27,7 @@ export default async function ZweiFaktorPage() {
- + + + + + + ); +} + export function ShieldIcon() { return ( diff --git a/src/i18n/dictionaries/de.ts b/src/i18n/dictionaries/de.ts index 72a2a45..d0f40a7 100644 --- a/src/i18n/dictionaries/de.ts +++ b/src/i18n/dictionaries/de.ts @@ -732,7 +732,7 @@ export const de: Dictionary = { twoFactorPage: { title: "Zwei-Faktor-Authentifizierung", subtitle: "Schützt dein Konto zusätzlich zum Passwort mit einem Code aus einer Authenticator-App.", - backLabel: "Zurück zur Übersicht", + backLabel: "Zurück zu den Einstellungen", activeStatus: (count) => `Zwei-Faktor-Authentifizierung ist aktiv. Noch ${count} Backup-Code${count === 1 ? "" : "s"} verfügbar.`, disableHeading: "Deaktivieren", passwordConfirmLabel: "Passwort zur Bestätigung", @@ -1631,8 +1631,9 @@ export const de: Dictionary = { navOrders: { title: "Bestellungen", description: "Bestellhistorie, Versandstatus und Rechnungen" }, navAddresses: { title: "Adressen", description: "Lieferadressen verwalten" }, navWishlist: { title: "Wunschliste", description: "Gemerkte Produkte ansehen" }, - navSecurity: { - title: "Sicherheit", + navSettings: { + title: "Einstellungen", + description: "Sicherheit und Datenschutz", twoFactorLabel: "Zwei-Faktor-Authentifizierung", twoFactorActive: "aktiv", twoFactorInactive: "nicht aktiv", @@ -1646,6 +1647,18 @@ export const de: Dictionary = { logoutAllDevicesHeading: "Alle Geräte abmelden", logoutAllDevicesDescription: "Beendet alle aktiven Sitzungen, auch dieses Gerät. Du musst dich danach neu anmelden.", logout: "Abmelden", + settings: { + title: "Einstellungen", + subtitle: "Sicherheit deines Kontos und deine Daten an einem Ort.", + backLabel: "Zurück zu meinem Konto", + securityHeading: "Sicherheit", + twoFactorHeading: "Zwei-Faktor-Authentifizierung", + twoFactorActiveDescription: "Aktiv. Bei der Anmeldung fragen wir zusätzlich einen Code aus deiner Authenticator-App ab.", + twoFactorInactiveDescription: "Nicht aktiv. Wir empfehlen, dein Konto zusätzlich mit einem Code aus einer Authenticator-App zu schützen.", + twoFactorManage: "Verwalten", + twoFactorSetup: "Einrichten", + logoutAllDevicesButton: "Überall abmelden", + }, privacy: { heading: "Datenschutz", downloadDataHeading: "Meine Daten herunterladen", diff --git a/src/i18n/dictionaries/en.ts b/src/i18n/dictionaries/en.ts index bc4c4c5..e897376 100644 --- a/src/i18n/dictionaries/en.ts +++ b/src/i18n/dictionaries/en.ts @@ -708,7 +708,7 @@ export const en: Dictionary = { twoFactorPage: { title: "Two-Factor Authentication", subtitle: "Protects your account beyond just a password, with a code from an authenticator app.", - backLabel: "Back to overview", + backLabel: "Back to settings", activeStatus: (count) => `Two-factor authentication is active. ${count} backup code${count === 1 ? "" : "s"} remaining.`, disableHeading: "Disable", passwordConfirmLabel: "Password to confirm", @@ -1605,8 +1605,9 @@ export const en: Dictionary = { navOrders: { title: "Orders", description: "Order history, shipping status, and invoices" }, navAddresses: { title: "Addresses", description: "Manage delivery addresses" }, navWishlist: { title: "Wishlist", description: "View saved products" }, - navSecurity: { - title: "Security", + navSettings: { + title: "Settings", + description: "Security and privacy", twoFactorLabel: "Two-factor authentication", twoFactorActive: "active", twoFactorInactive: "not active", @@ -1620,6 +1621,18 @@ export const en: Dictionary = { logoutAllDevicesHeading: "Log out all devices", logoutAllDevicesDescription: "Ends all active sessions, including this device. You'll need to sign in again.", logout: "Log out", + settings: { + title: "Settings", + subtitle: "Your account security and your data in one place.", + backLabel: "Back to my account", + securityHeading: "Security", + twoFactorHeading: "Two-factor authentication", + twoFactorActiveDescription: "Active. When you sign in, we also ask for a code from your authenticator app.", + twoFactorInactiveDescription: "Not active. We recommend protecting your account with an additional code from an authenticator app.", + twoFactorManage: "Manage", + twoFactorSetup: "Set up", + logoutAllDevicesButton: "Sign out everywhere", + }, privacy: { heading: "Privacy", downloadDataHeading: "Download my data", diff --git a/src/i18n/dictionary-type.ts b/src/i18n/dictionary-type.ts index f1cb020..62a4fae 100644 --- a/src/i18n/dictionary-type.ts +++ b/src/i18n/dictionary-type.ts @@ -1167,7 +1167,13 @@ export interface Dictionary { navOrders: { title: string; description: string }; navAddresses: { title: string; description: string }; navWishlist: { title: string; description: string }; - navSecurity: { title: string; twoFactorLabel: string; twoFactorActive: string; twoFactorInactive: string }; + navSettings: { + title: string; + description: string; + twoFactorLabel: string; + twoFactorActive: string; + twoFactorInactive: string; + }; navAdmin: { title: string; description: string }; newsletterHeading: string; newsletterSubscribed: string; @@ -1177,6 +1183,18 @@ export interface Dictionary { logoutAllDevicesHeading: string; logoutAllDevicesDescription: string; logout: string; + settings: { + title: string; + subtitle: string; + backLabel: string; + securityHeading: string; + twoFactorHeading: string; + twoFactorActiveDescription: string; + twoFactorInactiveDescription: string; + twoFactorManage: string; + twoFactorSetup: string; + logoutAllDevicesButton: string; + }; privacy: { heading: string; downloadDataHeading: string;