feat: Security CSRF Fix, Logout optimierung, Footer Anpassung, Rechtstexteanpassung
Docker Image bauen und veroeffentlichen / build-and-push (push) Failing after 1m33s

This commit is contained in:
Nick Szaunig committed 2026-09-03 13:51:50 +02:00
1 parent 0753a3bc8a
commit 4d3100e9ba
28 files changed
+802 -121

No files matched your search

+2
View File
@@ -12,3 +12,5 @@ export * from "./actions/shipping";
export * from "./actions/reviews";
export * from "./actions/discount-codes";
export * from "./actions/content";
export * from "./actions/apicore-sync";
export * from "./actions/dhl";
+39
View File
@@ -11,6 +11,11 @@ export async function createHarvestAction(formData: FormData): Promise<void> {
const year = num(formData, "year");
const type = str(formData, "type");
const slug = `${type.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${year}`;
// Checkbox: nur in formData vorhanden, wenn angehakt — fehlt sie ganz
// (kein Feld im Formular), gilt "veroeffentlicht" als sicherer Default fuer
// manuell im Admin-Bereich angelegte Ernten (bisheriges Verhalten vor
// Einfuehrung von isPublished).
const isPublished = !formData.has("isPublished") || formData.get("isPublished") === "on";
const harvest = await prisma.harvest.create({
data: {
@@ -20,6 +25,7 @@ export async function createHarvestAction(formData: FormData): Promise<void> {
harvestDate: new Date(str(formData, "harvestDate")),
location: str(formData, "location") || "Altenmünster",
description: str(formData, "description"),
isPublished,
},
});
@@ -31,12 +37,14 @@ export async function createHarvestAction(formData: FormData): Promise<void> {
});
revalidatePath("/admin/ernten");
revalidatePath("/ernten");
redirect("/admin/ernten");
}
export async function updateHarvestAction(formData: FormData): Promise<void> {
const admin = await requireAdmin();
const id = str(formData, "id");
const isPublished = formData.get("isPublished") === "on";
await prisma.harvest.update({
where: { id },
@@ -46,12 +54,43 @@ export async function updateHarvestAction(formData: FormData): Promise<void> {
harvestDate: new Date(str(formData, "harvestDate")),
location: str(formData, "location") || "Altenmünster",
description: str(formData, "description"),
isPublished,
},
});
await logAdminAction(admin, { action: "harvest.update", entityType: "Harvest", entityId: id });
revalidatePath("/admin/ernten");
revalidatePath("/ernten");
redirect("/admin/ernten");
}
/**
* Isolierte "Veroeffentlichen"/"Verbergen"-Aktion, getrennt von
* updateHarvestAction — so kann der Status per Knopfdruck direkt aus der
* Ernten-Liste umgeschaltet werden, ohne durch das komplette Bearbeiten-
* Formular zu muessen (insbesondere fuer den "veroeffentlichen"-Bestaetigungs-
* schritt bei aus ApiCore synchronisierten Entwuerfen, siehe apicore-sync.ts).
*/
export async function toggleHarvestPublishedAction(formData: FormData): Promise<void> {
const admin = await requireAdmin();
const id = str(formData, "id");
const isPublished = formData.get("isPublished") === "on";
const harvest = await prisma.harvest.update({
where: { id },
data: { isPublished },
});
await logAdminAction(admin, {
action: isPublished ? "harvest.publish" : "harvest.unpublish",
entityType: "Harvest",
entityId: id,
detail: `${harvest.type} ${harvest.year}`,
});
revalidatePath("/admin/ernten");
revalidatePath("/ernten");
redirect("/admin/ernten");
}
+31 -1
View File
@@ -3,14 +3,16 @@ import Link from "next/link";
import { prisma } from "@/lib/prisma";
import { BackLink } from "@/components/back-link";
import { formatPrice, formatDate } from "@/lib/format";
import { updateOrderStatusAction, updateShipmentTrackingAction } from "../../actions";
import { updateOrderStatusAction, updateShipmentTrackingAction, createDhlLabelAction } from "../../actions";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
import { isDhlConfigured } from "@/lib/dhl";
export default async function AdminOrderDetailPage(props: PageProps<"/admin/bestellungen/[id]">) {
const t = getDictionary(await getLocale());
const statusLabels = t.orderStatusBadge;
const { id } = await props.params;
const { error, message } = await props.searchParams;
const order = await prisma.order.findUnique({
where: { id },
include: {
@@ -24,11 +26,19 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
},
});
if (!order) notFound();
const dhlConfigured = isDhlConfigured();
return (
<div className="max-w-3xl">
<BackLink href="/admin/bestellungen" label={t.adminOrderDetailPage.backToOrders} />
{error ? (
<p className="mb-6 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">{error}</p>
) : null}
{message ? (
<p className="mb-6 text-sm text-forest bg-forest/5 border border-forest/20 px-4 py-3">{message}</p>
) : null}
<div className="flex flex-wrap items-start justify-between gap-4 mb-8">
<div>
<h1 className="text-2xl text-forest">{order.orderNumber}</h1>
@@ -101,6 +111,26 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
</div>
</div>
{order.fulfillmentMethod === "VERSAND" && order.payment?.status === "SUCCEEDED" ? (
<div className="bg-background border border-ink/10 p-6 mb-8">
<h2 className="text-xs tracking-wide-caps text-ink/65 mb-4">{t.adminOrderDetailPage.dhlHeading}</h2>
{order.shipment?.trackingNumber ? (
<p className="text-sm text-ink/70">
{t.adminOrderDetailPage.dhlAlreadyCreated(order.shipment.trackingNumber)}
</p>
) : dhlConfigured ? (
<form action={createDhlLabelAction}>
<input type="hidden" name="orderId" value={order.id} />
<button type="submit" className="bg-honey-dark text-cream px-4 py-2 text-sm">
{t.adminOrderDetailPage.dhlCreateButton}
</button>
</form>
) : (
<p className="text-sm text-ink/65">{t.adminOrderDetailPage.dhlNotConfigured}</p>
)}
</div>
) : null}
{order.fulfillmentMethod === "VERSAND" ? (
<div className="bg-background border border-ink/10 p-6 mb-8">
<h2 className="text-xs tracking-wide-caps text-ink/65 mb-4">{t.adminOrderDetailPage.trackingHeading}</h2>
+20 -1
View File
@@ -1,7 +1,7 @@
import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma";
import { BackLink } from "@/components/back-link";
import { updateHarvestAction } from "../../actions";
import { updateHarvestAction, toggleHarvestPublishedAction } from "../../actions";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
@@ -17,6 +17,20 @@ export default async function AdminHarvestEditPage(props: PageProps<"/admin/ernt
<div className="max-w-xl">
<BackLink href="/admin/ernten" label={t.adminHarvestEditPage.backToHarvests} />
<h1 className="text-2xl text-forest mb-8">{t.adminHarvestEditPage.title}</h1>
{harvest.isPublished ? null : (
<div className="mb-6 bg-honey/10 border border-honey/30 px-4 py-3 flex flex-col sm:flex-row sm:items-center gap-3 justify-between">
<p className="text-sm text-honey-dark">{t.adminHarvestEditPage.draftNotice}</p>
<form action={toggleHarvestPublishedAction}>
<input type="hidden" name="id" value={harvest.id} />
<input type="hidden" name="isPublished" value="on" />
<button type="submit" className="bg-forest text-cream px-4 py-2 text-sm whitespace-nowrap">
{t.adminHarvestEditPage.publishNow}
</button>
</form>
</div>
)}
<form action={updateHarvestAction} className="space-y-4 bg-background border border-ink/10 p-6">
<input type="hidden" name="id" value={harvest.id} />
<div className="grid grid-cols-2 gap-4">
@@ -68,6 +82,11 @@ export default async function AdminHarvestEditPage(props: PageProps<"/admin/ernt
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/>
</label>
<label className="flex items-center gap-2 text-sm text-ink/70">
<input type="checkbox" name="isPublished" defaultChecked={harvest.isPublished} />
{t.adminHarvestEditPage.isPublishedLabel}
</label>
<p className="text-xs text-ink/50">{t.adminHarvestEditPage.isPublishedHint}</p>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminHarvestEditPage.save}
</button>
+65 -17
View File
@@ -2,12 +2,13 @@ import Link from "next/link";
import { prisma } from "@/lib/prisma";
import { BackLink } from "@/components/back-link";
import { formatDate } from "@/lib/format";
import { createHarvestAction } from "../actions";
import { createHarvestAction, toggleHarvestPublishedAction, syncHarvestsFromApiCoreAction } from "../actions";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminHarvestsPage() {
export default async function AdminHarvestsPage(props: PageProps<"/admin/ernten">) {
const t = getDictionary(await getLocale());
const { synced, updated, skipped, failed, syncError } = await props.searchParams;
const harvests = await prisma.harvest.findMany({
orderBy: [{ year: "desc" }, { harvestDate: "desc" }],
include: { _count: { select: { batches: true, products: true } } },
@@ -17,6 +18,18 @@ export default async function AdminHarvestsPage() {
<div>
<BackLink href="/admin" label={t.adminHarvestsPage.backToDashboard} />
<h1 className="text-2xl text-forest mb-8">{t.adminHarvestsPage.title}</h1>
{synced !== undefined ? (
<p className="mb-6 text-sm text-forest bg-forest/5 border border-forest/20 px-4 py-3">
{t.adminHarvestsPage.syncResult(Number(synced), Number(updated ?? 0), Number(skipped ?? 0), Number(failed ?? 0))}
</p>
) : null}
{syncError !== undefined ? (
<p className="mb-6 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">
{t.adminHarvestsPage.syncError} ({syncError})
</p>
) : null}
<div className="overflow-x-auto bg-background border border-ink/10">
<table className="w-full text-sm">
<thead className="text-left text-ink/65 border-b border-ink/10">
@@ -27,6 +40,7 @@ export default async function AdminHarvestsPage() {
<th className="p-4">{t.adminHarvestsPage.colLocation}</th>
<th className="p-4">{t.adminHarvestsPage.colBatches}</th>
<th className="p-4">{t.adminHarvestsPage.colProducts}</th>
<th className="p-4">{t.adminHarvestsPage.colStatus}</th>
<th className="p-4" />
</tr>
</thead>
@@ -40,9 +54,27 @@ export default async function AdminHarvestsPage() {
<td className="p-4">{harvest._count.batches}</td>
<td className="p-4">{harvest._count.products}</td>
<td className="p-4">
<Link href={`/admin/ernten/${harvest.id}`} className="text-forest underline">
<span
className={
harvest.isPublished
? "text-forest bg-forest/10 px-2 py-1 text-xs"
: "text-honey-dark bg-honey/10 px-2 py-1 text-xs"
}
>
{harvest.isPublished ? t.adminHarvestsPage.statusPublished : t.adminHarvestsPage.statusDraft}
</span>
</td>
<td className="p-4 whitespace-nowrap">
<Link href={`/admin/ernten/${harvest.id}`} className="text-forest underline mr-4">
{t.adminHarvestsPage.edit}
</Link>
<form action={toggleHarvestPublishedAction} className="inline">
<input type="hidden" name="id" value={harvest.id} />
{harvest.isPublished ? null : <input type="hidden" name="isPublished" value="on" />}
<button type="submit" className="text-forest underline">
{harvest.isPublished ? t.adminHarvestsPage.unpublish : t.adminHarvestsPage.publish}
</button>
</form>
</td>
</tr>
))}
@@ -51,20 +83,36 @@ export default async function AdminHarvestsPage() {
{harvests.length === 0 ? <p className="p-6 text-ink/65">{t.adminHarvestsPage.noHarvestsYet}</p> : null}
</div>
<div className="mt-10 bg-background border border-ink/10 p-6 max-w-xl">
<h2 className="text-lg text-forest mb-4">{t.adminHarvestsPage.newHarvestHeading}</h2>
<form action={createHarvestAction} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Field name="type" label={t.adminHarvestsPage.typeLabel} required />
<Field name="year" label={t.adminHarvestsPage.yearLabel} type="number" required />
</div>
<Field name="harvestDate" label={t.adminHarvestsPage.harvestDateLabel} type="date" required />
<Field name="location" label={t.adminHarvestsPage.locationLabel} defaultValue="Altenmünster" />
<TextArea name="description" label={t.adminHarvestsPage.descriptionLabel} required />
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminHarvestsPage.create}
</button>
</form>
<div className="mt-10 grid md:grid-cols-2 gap-6">
<div className="bg-background border border-ink/10 p-6">
<h2 className="text-lg text-forest mb-4">{t.adminHarvestsPage.newHarvestHeading}</h2>
<form action={createHarvestAction} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Field name="type" label={t.adminHarvestsPage.typeLabel} required />
<Field name="year" label={t.adminHarvestsPage.yearLabel} type="number" required />
</div>
<Field name="harvestDate" label={t.adminHarvestsPage.harvestDateLabel} type="date" required />
<Field name="location" label={t.adminHarvestsPage.locationLabel} defaultValue="Altenmünster" />
<TextArea name="description" label={t.adminHarvestsPage.descriptionLabel} required />
<label className="flex items-center gap-2 text-sm text-ink/70">
<input type="checkbox" name="isPublished" defaultChecked />
{t.adminHarvestsPage.isPublishedLabel}
</label>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminHarvestsPage.create}
</button>
</form>
</div>
<div className="bg-background border border-ink/10 p-6">
<h2 className="text-lg text-forest mb-4">{t.adminHarvestsPage.syncHeading}</h2>
<p className="text-sm text-ink/65 mb-4">{t.adminHarvestsPage.syncIntro}</p>
<form action={syncHarvestsFromApiCoreAction}>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminHarvestsPage.syncButton}
</button>
</form>
</div>
</div>
</div>
);
+1
View File
@@ -28,6 +28,7 @@ export default async function AdminInhaltePage(props: PageProps<"/admin/inhalte"
const groups: { key: ContentFieldDef["group"]; heading: string }[] = [
{ key: "company", heading: t.adminContentPage.groupCompany },
{ key: "home", heading: t.adminContentPage.groupHome },
{ key: "social", heading: t.adminContentPage.groupSocial },
{ key: "legal", heading: t.adminContentPage.groupLegal },
];