Neue Gruppe "Logo und Favicon" unter Admin > Inhalte mit drei Bildfeldern: Logo (Kopfzeile), optionales Logo fuer dunkle Flaechen (Mobilmenue, Fusszeile, Admin-Leiste; faellt sonst auf das normale Logo zurueck) und Favicon. Leer bleibt alles wie bisher (gezeichnetes Waben-Zeichen bzw. Standard-Icon). - lib/brand.ts liest die Pfade und prueft sie erneut gegen das Upload-Muster, bevor sie in ein src-Attribut gelangen. - Statische app/icon.svg und app/favicon.ico ersetzt durch dynamische app/icon.tsx (512 px PNG) und app/apple-icon.tsx (180 px auf Creme); lib/brand-icon.ts rendert Upload oder Standard-Icon per sharp und liest Dateien nur ueber resolveUploadPath. Cache-Control no-cache, damit ein neues Favicon ohne Tage Verzoegerung erscheint. - /favicon.ico per Rewrite auf /icon, Manifest nutzt /icon. - Hochgeladenes Logo mit fester Hoehe und begrenzter Breite (Kopfzeile mobil 56 px, ab sm 112 px), damit breite Wortmarken nichts sprengen. Uploads bleiben auf Rasterbilder beschraenkt (kein SVG, XSS-Schutz). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
77 lines
3.2 KiB
TypeScript
77 lines
3.2 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { redirect } from "next/navigation";
|
|
import { noticeUrl } from "@/lib/notice";
|
|
import { getCurrentAdmin } from "@/lib/auth";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { logoutAction } from "@/app/konto/actions";
|
|
import { AdminShell } from "@/components/admin-shell";
|
|
import { getLocale } from "@/i18n/locale";
|
|
import { getDictionary } from "@/i18n/get-dictionary";
|
|
import { getBrandAssets } from "@/lib/brand";
|
|
|
|
// Admin-Daten (Bestellungen, Bestand) muessen immer aktuell sein; zudem
|
|
// entkoppelt das den Docker-Build-Schritt von einer DB-Verbindung.
|
|
export const dynamic = "force-dynamic";
|
|
|
|
// Einzelne Admin-Unterseiten (bestellungen, produkte, ...) haben bewusst
|
|
// keine eigene Metadata — der Bereich ist ueber robots.ts von der Indexierung
|
|
// ausgeschlossen. Dieses Template sorgt trotzdem fuer einen sinnvollen
|
|
// Browser-Tab-Titel statt des generischen Startseiten-Titels ueberall im
|
|
// Admin-Bereich, ohne jede Unterseite einzeln pflegen zu muessen.
|
|
export async function generateMetadata(): Promise<Metadata> {
|
|
const t = getDictionary(await getLocale());
|
|
return {
|
|
title: { default: t.account.navAdmin.title, template: `%s | ${t.account.navAdmin.title}` },
|
|
robots: { index: false, follow: false },
|
|
};
|
|
}
|
|
|
|
export default async function AdminLayout({ children }: LayoutProps<"/admin">) {
|
|
// Der Proxy (src/proxy.ts) blockt fremde Requests bereits vor dem Rendern;
|
|
// diese zweite, gegen die Datenbank autoritative Pruefung faengt u.a. den
|
|
// Fall ab, dass die ADMIN-Rolle eines eingeloggten Kontos nach dem Login
|
|
// entzogen wurde (das JWT traegt die Rolle nur als Snapshot vom Login-Zeitpunkt).
|
|
const admin = await getCurrentAdmin();
|
|
if (!admin) {
|
|
redirect(noticeUrl("admin-login"));
|
|
}
|
|
|
|
// Erzwungen statt nur empfohlen: Admin-Konten haben Zugriff auf
|
|
// Bestellungen, Rueckerstattungen und Kundendaten — ohne 2FA waere ein
|
|
// gestohlenes Passwort allein genug fuer vollen Zugriff. Weiterleitung
|
|
// statt Blockade, damit das Konto die Einrichtung direkt nachholen kann.
|
|
if (!admin.twoFactorEnabled) {
|
|
redirect(noticeUrl("admin-2fa"));
|
|
}
|
|
|
|
// Was gerade auf Bearbeitung wartet — erscheint als Zaehler in der
|
|
// Seitenleiste. Nur echte Arbeit: bezahlte, noch nicht versendete oder
|
|
// abgeholte Bestellungen (keine offenen Zahlungen), neue Retouren,
|
|
// unfreigegebene Bewertungen, neue Firmenanfragen. Reine COUNT-Abfragen.
|
|
const brand = await getBrandAssets();
|
|
const [openOrders, openReturns, pendingReviews, newInquiries] = await Promise.all([
|
|
prisma.order.count({
|
|
where: { status: { in: ["BEZAHLT", "IN_VORBEREITUNG", "VERSANDBEREIT", "ABHOLBEREIT"] }, archivedAt: null },
|
|
}),
|
|
prisma.returnRequest.count({ where: { status: "ANGEFRAGT" } }),
|
|
prisma.review.count({ where: { isApproved: false } }),
|
|
prisma.businessInquiry.count({ where: { status: "NEU" } }),
|
|
]);
|
|
|
|
return (
|
|
<AdminShell
|
|
adminName={`${admin.firstName} ${admin.lastName}`}
|
|
logoSrc={brand.logoInverted}
|
|
badges={{
|
|
"/admin/bestellungen": openOrders,
|
|
"/admin/retouren": openReturns,
|
|
"/admin/bewertungen": pendingReviews,
|
|
"/admin/anfragen": newInquiries,
|
|
}}
|
|
logoutAction={logoutAction}
|
|
>
|
|
{children}
|
|
</AdminShell>
|
|
);
|
|
}
|