feat: Wabenhain Honig-Shop Anwendung

Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei:
Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich
(Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare
Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung,
PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma-
Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
Nick Szaunig committed 2026-08-17 14:30:28 +02:00
1 parent 5c76fcc266
commit c7927f2883
240 files changed
+26096 -137

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getCurrentCustomer } from "@/lib/auth";
import { rateLimit, getClientIp } from "@/lib/rate-limit";
const EXPORT_ATTEMPTS_PER_WINDOW = 5;
const EXPORT_WINDOW_MS = 60 * 60 * 1000;
export async function GET() {
const customer = await getCurrentCustomer();
if (!customer) {
return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 });
}
const ip = await getClientIp();
if (!rateLimit(`data-export:${ip}:${customer.id}`, EXPORT_ATTEMPTS_PER_WINDOW, EXPORT_WINDOW_MS)) {
return NextResponse.json({ error: "Zu viele Anfragen. Bitte versuche es später erneut." }, { status: 429 });
}
const [addresses, orders, reviews, wishlistItems, newsletter] = await Promise.all([
prisma.address.findMany({ where: { customerId: customer.id } }),
prisma.order.findMany({
where: { customerId: customer.id },
include: { items: true, invoice: true },
orderBy: { createdAt: "desc" },
}),
prisma.review.findMany({
where: { customerId: customer.id },
include: { product: { select: { name: true } } },
}),
prisma.wishlistItem.findMany({
where: { customerId: customer.id },
include: { product: { select: { name: true } } },
}),
prisma.newsletterSubscriber.findUnique({ where: { customerId: customer.id } }),
]);
const exportData = {
exportedAt: new Date().toISOString(),
account: {
email: customer.email,
firstName: customer.firstName,
lastName: customer.lastName,
phone: customer.phone,
createdAt: customer.createdAt,
twoFactorEnabled: customer.twoFactorEnabled,
},
addresses: addresses.map((a) => ({
label: a.label,
firstName: a.firstName,
lastName: a.lastName,
street: a.street,
zip: a.zip,
city: a.city,
country: a.country,
isDefault: a.isDefault,
})),
orders: orders.map((o) => ({
orderNumber: o.orderNumber,
createdAt: o.createdAt,
status: o.status,
totalCents: o.totalCents,
currency: o.currency,
fulfillmentMethod: o.fulfillmentMethod,
invoiceNumber: o.invoice?.invoiceNumber ?? null,
items: o.items.map((item) => ({
productName: item.productName,
quantity: item.quantity,
unitPriceCents: item.unitPriceCents,
totalCents: item.totalCents,
})),
})),
reviews: reviews.map((r) => ({
product: r.product.name,
rating: r.rating,
comment: r.comment,
createdAt: r.createdAt,
isApproved: r.isApproved,
})),
wishlist: wishlistItems.map((w) => ({ product: w.product.name, addedAt: w.createdAt })),
newsletter: newsletter
? { subscribed: newsletter.unsubscribedAt === null, confirmed: newsletter.confirmed }
: { subscribed: false, confirmed: false },
};
return new NextResponse(JSON.stringify(exportData, null, 2), {
headers: {
"Content-Type": "application/json",
"Content-Disposition": `attachment; filename="wabenhain-meine-daten-${customer.id.slice(-8)}.json"`,
},
});
}
@@ -0,0 +1,40 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { hashVerificationToken } from "@/lib/email-verification";
import { getBaseUrl } from "@/lib/url";
export async function GET(request: Request) {
const token = new URL(request.url).searchParams.get("token");
const baseUrl = getBaseUrl();
const fail = (message: string) =>
NextResponse.redirect(`${baseUrl}/konto?error=${encodeURIComponent(message)}`);
if (!token) {
return fail("Dieser Bestätigungslink ist ungültig.");
}
const verificationToken = await prisma.emailVerificationToken.findUnique({
where: { tokenHash: hashVerificationToken(token) },
});
const isValid =
verificationToken && verificationToken.usedAt === null && verificationToken.expiresAt > new Date();
if (!isValid) {
return fail("Dieser Bestätigungslink ist ungültig oder abgelaufen. Bitte fordere über /konto einen neuen an.");
}
await prisma.$transaction([
prisma.customer.update({
where: { id: verificationToken.customerId },
data: { emailVerifiedAt: new Date() },
}),
prisma.emailVerificationToken.update({
where: { id: verificationToken.id },
data: { usedAt: new Date() },
}),
]);
return NextResponse.redirect(
`${baseUrl}/konto?message=${encodeURIComponent("Deine E-Mail-Adresse wurde bestätigt.")}`
);
}