Refactor admin area and remove legacy root paths
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-13 16:14:04 +01:00
parent e32ac4cacb
commit db897e22bf
65 changed files with 216 additions and 140 deletions
+2 -2
View File
@@ -5,5 +5,5 @@ NEXT_PUBLIC_ADMIN_URL="https://root.mohfarawati.de"
NEXT_TELEMETRY_DISABLED="1" NEXT_TELEMETRY_DISABLED="1"
ADMIN_PASSWORD="change-me" ADMIN_PASSWORD="change-me"
ADMIN_AUTH_SECRET="replace-with-a-long-random-secret" ADMIN_AUTH_SECRET="replace-with-a-long-random-secret"
ROOT_BASIC_AUTH_USER="change-me" ADMIN_BASIC_AUTH_USER="change-me"
ROOT_BASIC_AUTH_PASS="change-me" ADMIN_BASIC_AUTH_PASS="change-me"
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getMaintenanceMode } from "@/lib/app-config"; import { getMaintenanceMode } from "@/lib/app-config";
import { AppCard } from "@/components/ui/app-card"; import { AppCard } from "@/components/ui/app-card";
@@ -27,7 +27,7 @@ const copy = {
backToSite: "Zur Website", backToSite: "Zur Website",
}; };
export default async function RootMaintenancePage() { export default async function AdminMaintenancePage() {
const authenticated = await isAdminAuthenticated(); const authenticated = await isAdminAuthenticated();
if (!authenticated) { if (!authenticated) {
@@ -43,7 +43,7 @@ export default async function RootMaintenancePage() {
} }
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="maintenance" active="maintenance"
logoutAction={logoutAction} logoutAction={logoutAction}
@@ -63,6 +63,6 @@ export default async function RootMaintenancePage() {
</CardContent> </CardContent>
</AppCard> </AppCard>
</MotionFade> </MotionFade>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { MarqueeSettingsForm } from "@/components/root/marquee-settings-form"; import { MarqueeSettingsForm } from "@/components/admin/marquee-settings-form";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getMarqueeSettings } from "@/lib/app-config"; import { getMarqueeSettings } from "@/lib/app-config";
@@ -25,7 +25,7 @@ const copy = {
backToSite: "Zur Website", backToSite: "Zur Website",
}; };
export default async function RootMarqueePage() { export default async function AdminMarqueePage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
@@ -40,7 +40,7 @@ export default async function RootMarqueePage() {
const marqueeSettings = await getMarqueeSettings(); const marqueeSettings = await getMarqueeSettings();
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="marquee" active="marquee"
logoutAction={logoutAction} logoutAction={logoutAction}
@@ -55,6 +55,6 @@ export default async function RootMarqueePage() {
/> />
</MotionFade> </MotionFade>
</div> </div>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { MediaLibraryManager } from "@/components/root/media-library-manager"; import { MediaLibraryManager } from "@/components/admin/media-library-manager";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminMediaAssets } from "@/lib/media"; import { getAdminMediaAssets } from "@/lib/media";
@@ -20,7 +20,7 @@ const copy = {
backToSite: "Zur Website", backToSite: "Zur Website",
}; };
export default async function RootMediaPage() { export default async function AdminMediaPage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
@@ -35,7 +35,7 @@ export default async function RootMediaPage() {
const mediaAssets = await getAdminMediaAssets(); const mediaAssets = await getAdminMediaAssets();
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="media" active="media"
logoutAction={logoutAction} logoutAction={logoutAction}
@@ -45,6 +45,6 @@ export default async function RootMediaPage() {
<div className="space-y-6"> <div className="space-y-6">
<MediaLibraryManager mediaAssets={mediaAssets} /> <MediaLibraryManager mediaAssets={mediaAssets} />
</div> </div>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
+8 -8
View File
@@ -4,7 +4,7 @@ import { redirect } from "next/navigation";
import { Container } from "@/components/layout/container"; import { Container } from "@/components/layout/container";
import { StatsCard } from "@/components/dashboard/stats-card"; import { StatsCard } from "@/components/dashboard/stats-card";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/card";
@@ -24,7 +24,7 @@ import { getMaintenanceMode } from "@/lib/app-config";
import { getAdminMediaAssets } from "@/lib/media"; import { getAdminMediaAssets } from "@/lib/media";
import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio"; import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio";
type RootPageProps = { type AdminPageProps = {
searchParams?: Promise<{ searchParams?: Promise<{
error?: string; error?: string;
}>; }>;
@@ -43,14 +43,14 @@ const copy = {
uiKit: "UI Kit", uiKit: "UI Kit",
logout: "Ausloggen", logout: "Ausloggen",
backToSite: "Zur Website", backToSite: "Zur Website",
loginTitle: "Root Login", loginTitle: "Admin Login",
loginText: "Nur autorisierte Nutzer duerfen diesen Bereich verwenden.", loginText: "Nur autorisierte Nutzer duerfen diesen Bereich verwenden.",
passwordLabel: "Passwort", passwordLabel: "Passwort",
loginButton: "Einloggen", loginButton: "Einloggen",
invalidLogin: "Falsches Passwort.", invalidLogin: "Falsches Passwort.",
lockedLogin: "Zu viele Fehlversuche. Bitte spaeter erneut versuchen.", lockedLogin: "Zu viele Fehlversuche. Bitte spaeter erneut versuchen.",
configMissing: "ADMIN_PASSWORD und ADMIN_AUTH_SECRET fehlen in env.", configMissing: "ADMIN_PASSWORD und ADMIN_AUTH_SECRET fehlen in env.",
basicAuthMissing: "ROOT_BASIC_AUTH_USER und ROOT_BASIC_AUTH_PASS fehlen in env.", basicAuthMissing: "ADMIN_BASIC_AUTH_USER und ADMIN_BASIC_AUTH_PASS fehlen in env.",
maintenanceOn: "Aktiv", maintenanceOn: "Aktiv",
maintenanceOff: "Inaktiv", maintenanceOff: "Inaktiv",
portfolioOnline: "Portfolio online", portfolioOnline: "Portfolio online",
@@ -60,11 +60,11 @@ const copy = {
activeCategories: "Active Categories", activeCategories: "Active Categories",
}; };
export default async function RootPage({ searchParams }: RootPageProps) { export default async function AdminPage({ searchParams }: AdminPageProps) {
const resolvedSearchParams = await searchParams; const resolvedSearchParams = await searchParams;
const authConfigured = isAdminAuthConfigured(); const authConfigured = isAdminAuthConfigured();
const basicConfigured = Boolean( const basicConfigured = Boolean(
process.env.ROOT_BASIC_AUTH_USER && process.env.ROOT_BASIC_AUTH_PASS, process.env.ADMIN_BASIC_AUTH_USER && process.env.ADMIN_BASIC_AUTH_PASS,
); );
const authenticated = await isAdminAuthenticated(); const authenticated = await isAdminAuthenticated();
const lockState = await getAdminLockState(); const lockState = await getAdminLockState();
@@ -182,7 +182,7 @@ export default async function RootPage({ searchParams }: RootPageProps) {
const imageCount = mediaAssets.filter((asset) => asset.kind === "IMAGE").length; const imageCount = mediaAssets.filter((asset) => asset.kind === "IMAGE").length;
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="overview" active="overview"
logoutAction={logoutAction} logoutAction={logoutAction}
@@ -238,6 +238,6 @@ export default async function RootPage({ searchParams }: RootPageProps) {
</MotionFade> </MotionFade>
</section> </section>
</div> </div>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { PortfolioCategoriesManager } from "@/components/root/portfolio-categories-manager"; import { PortfolioCategoriesManager } from "@/components/admin/portfolio-categories-manager";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminPortfolioCategories } from "@/lib/portfolio"; import { getAdminPortfolioCategories } from "@/lib/portfolio";
@@ -22,7 +22,7 @@ const copy = {
backToSite: "Zur Website", backToSite: "Zur Website",
}; };
export default async function RootPortfolioCategoriesPage() { export default async function AdminPortfolioCategoriesPage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
@@ -39,7 +39,7 @@ export default async function RootPortfolioCategoriesPage() {
const assignedProjects = categories.reduce((sum, category) => sum + category.projectCount, 0); const assignedProjects = categories.reduce((sum, category) => sum + category.projectCount, 0);
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="portfolio" active="portfolio"
portfolioChild="categories" portfolioChild="categories"
@@ -56,6 +56,6 @@ export default async function RootPortfolioCategoriesPage() {
removeCategoryAction={deleteCategoryAction} removeCategoryAction={deleteCategoryAction}
/> />
</div> </div>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
@@ -1,5 +1,5 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default function RootPortfolioMediaRedirectPage() { export default function AdminPortfolioMediaRedirectPage() {
redirect("/media"); redirect("/media");
} }
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { PortfolioProjectsOverview } from "@/components/root/portfolio-projects-overview"; import { PortfolioProjectsOverview } from "@/components/admin/portfolio-projects-overview";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio"; import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio";
@@ -20,7 +20,7 @@ const copy = {
backToSite: "Zur Website", backToSite: "Zur Website",
}; };
type RootPortfolioPageProps = { type AdminPortfolioPageProps = {
searchParams?: Promise<{ searchParams?: Promise<{
category?: string; category?: string;
status?: "all" | "draft" | "published"; status?: "all" | "draft" | "published";
@@ -29,7 +29,7 @@ type RootPortfolioPageProps = {
}>; }>;
}; };
export default async function RootPortfolioPage({ searchParams }: RootPortfolioPageProps) { export default async function AdminPortfolioPage({ searchParams }: AdminPortfolioPageProps) {
const resolvedSearchParams = await searchParams; const resolvedSearchParams = await searchParams;
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
@@ -58,7 +58,7 @@ export default async function RootPortfolioPage({ searchParams }: RootPortfolioP
]); ]);
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="portfolio" active="portfolio"
portfolioChild="overview" portfolioChild="overview"
@@ -74,6 +74,6 @@ export default async function RootPortfolioPage({ searchParams }: RootPortfolioP
selectedStatus={selectedStatus} selectedStatus={selectedStatus}
/> />
</div> </div>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
@@ -1,8 +1,8 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { PortfolioProjectForm } from "@/components/root/portfolio-project-form"; import { PortfolioProjectForm } from "@/components/admin/portfolio-project-form";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { AppCard } from "@/components/ui/app-card"; import { AppCard } from "@/components/ui/app-card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card"; import { CardContent } from "@/components/ui/card";
@@ -43,15 +43,15 @@ const copy = {
deleteProject: "Projekt loeschen", deleteProject: "Projekt loeschen",
}; };
type RootPortfolioProjectPageProps = { type AdminPortfolioProjectPageProps = {
params: Promise<{ params: Promise<{
id: string; id: string;
}>; }>;
}; };
export default async function RootPortfolioProjectPage({ export default async function AdminPortfolioProjectPage({
params, params,
}: RootPortfolioProjectPageProps) { }: AdminPortfolioProjectPageProps) {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
@@ -76,7 +76,7 @@ export default async function RootPortfolioProjectPage({
} }
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="portfolio" active="portfolio"
portfolioChild="projects" portfolioChild="projects"
@@ -132,6 +132,6 @@ export default async function RootPortfolioProjectPage({
</AppCard> </AppCard>
</MotionFade> </MotionFade>
</div> </div>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
@@ -1,8 +1,8 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { PortfolioProjectForm } from "@/components/root/portfolio-project-form"; import { PortfolioProjectForm } from "@/components/admin/portfolio-project-form";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getMediaOptions } from "@/lib/media"; import { getMediaOptions } from "@/lib/media";
import { getActivePortfolioCategories } from "@/lib/portfolio"; import { getActivePortfolioCategories } from "@/lib/portfolio";
@@ -24,7 +24,7 @@ const copy = {
backToSite: "Zur Website", backToSite: "Zur Website",
}; };
export default async function RootNewPortfolioProjectPage() { export default async function AdminNewPortfolioProjectPage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
@@ -42,7 +42,7 @@ export default async function RootNewPortfolioProjectPage() {
]); ]);
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="portfolio" active="portfolio"
portfolioChild="new-project" portfolioChild="new-project"
@@ -61,6 +61,6 @@ export default async function RootNewPortfolioProjectPage() {
/> />
</MotionFade> </MotionFade>
</div> </div>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { PortfolioProjectsOverview } from "@/components/root/portfolio-projects-overview"; import { PortfolioProjectsOverview } from "@/components/admin/portfolio-projects-overview";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio"; import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio";
@@ -20,16 +20,16 @@ const copy = {
backToSite: "Zur Website", backToSite: "Zur Website",
}; };
type RootPortfolioProjectsPageProps = { type AdminPortfolioProjectsPageProps = {
searchParams?: Promise<{ searchParams?: Promise<{
category?: string; category?: string;
status?: "all" | "draft" | "published"; status?: "all" | "draft" | "published";
}>; }>;
}; };
export default async function RootPortfolioProjectsPage({ export default async function AdminPortfolioProjectsPage({
searchParams, searchParams,
}: RootPortfolioProjectsPageProps) { }: AdminPortfolioProjectsPageProps) {
const resolvedSearchParams = await searchParams; const resolvedSearchParams = await searchParams;
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
@@ -58,7 +58,7 @@ export default async function RootPortfolioProjectsPage({
]); ]);
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="portfolio" active="portfolio"
portfolioChild="projects" portfolioChild="projects"
@@ -74,6 +74,6 @@ export default async function RootPortfolioProjectsPage({
selectedStatus={selectedStatus} selectedStatus={selectedStatus}
/> />
</div> </div>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
@@ -2,8 +2,8 @@ import { MediaKind } from "@prisma/client";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { SiteSettingsForm } from "@/components/root/site-settings-form"; import { SiteSettingsForm } from "@/components/admin/site-settings-form";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { import {
getSiteSettings, getSiteSettings,
@@ -29,7 +29,7 @@ const copy = {
backToSite: "Zur Website", backToSite: "Zur Website",
}; };
export default async function RootSiteSettingsPage() { export default async function AdminSiteSettingsPage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
@@ -48,7 +48,7 @@ export default async function RootSiteSettingsPage() {
]); ]);
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="site-settings" active="site-settings"
logoutAction={logoutAction} logoutAction={logoutAction}
@@ -65,6 +65,6 @@ export default async function RootSiteSettingsPage() {
/> />
</MotionFade> </MotionFade>
</div> </div>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
@@ -1,8 +1,8 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { ContactProtectionForm } from "@/components/root/contact-protection-form"; import { ContactProtectionForm } from "@/components/admin/contact-protection-form";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getContactProtectionFormValues } from "@/lib/app-config"; import { getContactProtectionFormValues } from "@/lib/app-config";
@@ -25,7 +25,7 @@ const copy = {
backToSite: "Zur Website", backToSite: "Zur Website",
}; };
export default async function RootSMTPProtectionPage() { export default async function AdminSMTPProtectionPage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
@@ -40,7 +40,7 @@ export default async function RootSMTPProtectionPage() {
const settings = await getContactProtectionFormValues(); const settings = await getContactProtectionFormValues();
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="smtp" active="smtp"
smtpChild="contact-protection" smtpChild="contact-protection"
@@ -56,6 +56,6 @@ export default async function RootSMTPProtectionPage() {
/> />
</MotionFade> </MotionFade>
</div> </div>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
@@ -1,8 +1,8 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { SMTPSettingsForm } from "@/components/root/smtp-settings-form"; import { SMTPSettingsForm } from "@/components/admin/smtp-settings-form";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getMailSettingsFormValues } from "@/lib/app-config"; import { getMailSettingsFormValues } from "@/lib/app-config";
@@ -26,7 +26,7 @@ const copy = {
backToSite: "Zur Website", backToSite: "Zur Website",
}; };
export default async function RootSMTPPage() { export default async function AdminSMTPPage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
@@ -41,7 +41,7 @@ export default async function RootSMTPPage() {
const mailSettings = await getMailSettingsFormValues(); const mailSettings = await getMailSettingsFormValues();
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="smtp" active="smtp"
smtpChild="settings" smtpChild="settings"
@@ -64,6 +64,6 @@ export default async function RootSMTPPage() {
/> />
</MotionFade> </MotionFade>
</div> </div>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { UiKitShowcase } from "@/components/ui/ui-kit-showcase"; import { UiKitShowcase } from "@/components/ui/ui-kit-showcase";
@@ -9,7 +9,7 @@ export const dynamic = "force-dynamic";
const copy = { const copy = {
title: "UI Kit", title: "UI Kit",
subtitle: "Globale Referenz fuer das visuelle System im Root Bereich.", subtitle: "Globale Referenz fuer das visuelle System im Admin Bereich.",
maintenance: "Wartungsmodus", maintenance: "Wartungsmodus",
overview: "Uebersicht", overview: "Uebersicht",
uiKit: "UI Kit", uiKit: "UI Kit",
@@ -20,7 +20,7 @@ const copy = {
backToSite: "Zur Website", backToSite: "Zur Website",
}; };
export default async function RootUiKitPage() { export default async function AdminUiKitPage() {
const authenticated = await isAdminAuthenticated(); const authenticated = await isAdminAuthenticated();
if (!authenticated) { if (!authenticated) {
@@ -35,7 +35,7 @@ export default async function RootUiKitPage() {
} }
return ( return (
<RootDashboardShell <AdminDashboardShell
copy={copy} copy={copy}
active="ui-kit" active="ui-kit"
logoutAction={logoutAction} logoutAction={logoutAction}
@@ -45,6 +45,6 @@ export default async function RootUiKitPage() {
<MotionFade delay={0.1}> <MotionFade delay={0.1}>
<UiKitShowcase localeKey="de" /> <UiKitShowcase localeKey="de" />
</MotionFade> </MotionFade>
</RootDashboardShell> </AdminDashboardShell>
); );
} }
+2 -2
View File
@@ -1,2 +1,2 @@
export { metadata } from "../root/layout"; export { metadata } from "../_admin/layout";
export { default } from "../root/layout"; export { default } from "../_admin/layout";
+2 -2
View File
@@ -1,2 +1,2 @@
export { dynamic } from "../../root/maintenance/page"; export { dynamic } from "../../_admin/maintenance/page";
export { default } from "../../root/maintenance/page"; export { default } from "../../_admin/maintenance/page";
+1 -1
View File
@@ -1 +1 @@
export { default } from "../../root/marquee/page"; export { default } from "../../_admin/marquee/page";
+2 -2
View File
@@ -1,2 +1,2 @@
export { dynamic } from "../../root/media/page"; export { dynamic } from "../../_admin/media/page";
export { default } from "../../root/media/page"; export { default } from "../../_admin/media/page";
+2 -2
View File
@@ -1,2 +1,2 @@
export { dynamic } from "../root/page"; export { dynamic } from "../_admin/page";
export { default } from "../root/page"; export { default } from "../_admin/page";
@@ -1 +1 @@
export { default } from "../../../root/portfolio/categories/page"; export { default } from "../../../_admin/portfolio/categories/page";
+1 -1
View File
@@ -1 +1 @@
export { default } from "../../../root/portfolio/media/page"; export { default } from "../../../_admin/portfolio/media/page";
+1 -1
View File
@@ -1 +1 @@
export { default } from "../../root/portfolio/page"; export { default } from "../../_admin/portfolio/page";
@@ -1 +1 @@
export { default } from "../../../../root/portfolio/projects/[id]/page"; export { default } from "../../../../_admin/portfolio/projects/[id]/page";
@@ -1 +1 @@
export { default } from "../../../../root/portfolio/projects/new/page"; export { default } from "../../../../_admin/portfolio/projects/new/page";
@@ -1 +1 @@
export { default } from "../../../root/portfolio/projects/page"; export { default } from "../../../_admin/portfolio/projects/page";
+1 -1
View File
@@ -1 +1 @@
export { default } from "../../root/site-settings/page"; export { default } from "../../_admin/site-settings/page";
@@ -1 +1 @@
export { default } from "../../../root/smtp/contact-protection/page"; export { default } from "../../../_admin/smtp/contact-protection/page";
+1 -1
View File
@@ -1 +1 @@
export { default } from "../../root/smtp/page"; export { default } from "../../_admin/smtp/page";
+1 -1
View File
@@ -1 +1 @@
export { default } from "../../root/ui-kit/page"; export { default } from "../../_admin/ui-kit/page";
+1 -1
View File
@@ -3,7 +3,7 @@ import { unstable_noStore as noStore } from "next/cache";
import localFont from "next/font/local"; import localFont from "next/font/local";
import Script from "next/script"; import Script from "next/script";
import { getLocale } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { QueryToastBridge } from "@/components/root/query-toast-bridge"; import { QueryToastBridge } from "@/components/admin/query-toast-bridge";
import { SoundProvider } from "@/components/sound-provider"; import { SoundProvider } from "@/components/sound-provider";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
@@ -15,17 +15,17 @@ import Link from "next/link";
import { DashboardLayout } from "@/components/dashboard/dashboard-layout"; import { DashboardLayout } from "@/components/dashboard/dashboard-layout";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { SidebarMaintenanceControl } from "@/components/root/sidebar-maintenance-control"; import { SidebarMaintenanceControl } from "@/components/admin/sidebar-maintenance-control";
import { SoundToggle } from "@/components/sound-toggle"; import { SoundToggle } from "@/components/sound-toggle";
import { ThemeToggle } from "@/components/theme-toggle"; import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { buildSiteUrl } from "@/lib/admin-routing"; import { buildSiteUrl } from "@/lib/admin-routing";
import { getMaintenanceMode, getSiteSettingsMediaBindings } from "@/lib/app-config"; import { getMaintenanceMode, getSiteSettingsMediaBindings } from "@/lib/app-config";
import { getRootNavigation } from "@/lib/root-navigation"; import { getAdminNavigation } from "@/lib/admin-navigation";
import { updateMaintenanceModeAction } from "@/app/root/maintenance/actions"; import { updateMaintenanceModeAction } from "@/app/_admin/maintenance/actions";
type RootDashboardCopy = { type AdminDashboardCopy = {
title: string; title: string;
subtitle: string; subtitle: string;
overview: string; overview: string;
@@ -41,8 +41,8 @@ type RootDashboardCopy = {
backToSite: string; backToSite: string;
}; };
type RootDashboardShellProps = { type AdminDashboardShellProps = {
copy: RootDashboardCopy; copy: AdminDashboardCopy;
active: "overview" | "maintenance" | "ui-kit" | "portfolio" | "media" | "site-settings" | "smtp" | "marquee"; active: "overview" | "maintenance" | "ui-kit" | "portfolio" | "media" | "site-settings" | "smtp" | "marquee";
smtpChild?: "settings" | "contact-protection"; smtpChild?: "settings" | "contact-protection";
portfolioChild?: "overview" | "projects" | "new-project" | "categories"; portfolioChild?: "overview" | "projects" | "new-project" | "categories";
@@ -55,7 +55,7 @@ type RootDashboardShellProps = {
children: ReactNode; children: ReactNode;
}; };
export async function RootDashboardShell({ export async function AdminDashboardShell({
copy, copy,
active, active,
smtpChild, smtpChild,
@@ -67,12 +67,12 @@ export async function RootDashboardShell({
sidebarTopContent, sidebarTopContent,
toolbar, toolbar,
children, children,
}: RootDashboardShellProps) { }: AdminDashboardShellProps) {
const [mediaBindings, maintenanceEnabled] = await Promise.all([ const [mediaBindings, maintenanceEnabled] = await Promise.all([
getSiteSettingsMediaBindings(), getSiteSettingsMediaBindings(),
getMaintenanceMode(), getMaintenanceMode(),
]); ]);
const sidebarItems = getRootNavigation(copy, active, smtpChild, portfolioChild); const sidebarItems = getAdminNavigation(copy, active, smtpChild, portfolioChild);
const normalizedSidebarItems = sidebarItems.filter( const normalizedSidebarItems = sidebarItems.filter(
(item) => (item) =>
item.href !== "/maintenance" && item.href !== "/maintenance" &&
@@ -28,7 +28,7 @@ import { Label } from "@/components/ui/label";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { createMediaAssetAction, deleteMediaAssetAction } from "@/app/root/media/actions"; import { createMediaAssetAction, deleteMediaAssetAction } from "@/app/_admin/media/actions";
const copy = { const copy = {
addMedia: "Upload Media File", addMedia: "Upload Media File",
@@ -13,7 +13,7 @@ import {
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import type { deleteCategoryAction, upsertCategoryAction } from "@/app/root/portfolio/actions"; import type { deleteCategoryAction, upsertCategoryAction } from "@/app/_admin/portfolio/actions";
import { StatsCard } from "@/components/dashboard/stats-card"; import { StatsCard } from "@/components/dashboard/stats-card";
import { AppCard } from "@/components/ui/app-card"; import { AppCard } from "@/components/ui/app-card";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
@@ -4,7 +4,7 @@ import { FolderKanban, MoreVertical, Trash2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { deleteProjectAction } from "@/app/root/portfolio/actions"; import { deleteProjectAction } from "@/app/_admin/portfolio/actions";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -15,7 +15,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { type ReactNode, useEffect, useRef, useState } from "react"; import { type ReactNode, useEffect, useRef, useState } from "react";
import { MediaFieldPicker, type MediaFieldState } from "@/components/root/media-field-picker"; import { MediaFieldPicker, type MediaFieldState } from "@/components/admin/media-field-picker";
import { AppCard } from "@/components/ui/app-card"; import { AppCard } from "@/components/ui/app-card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -3,7 +3,7 @@ import Link from "next/link";
import { StatsCard } from "@/components/dashboard/stats-card"; import { StatsCard } from "@/components/dashboard/stats-card";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { PortfolioProjectActions } from "@/components/root/portfolio-project-actions"; import { PortfolioProjectActions } from "@/components/admin/portfolio-project-actions";
import { AppCard } from "@/components/ui/app-card"; import { AppCard } from "@/components/ui/app-card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
+3 -3
View File
@@ -11,18 +11,18 @@ import {
SheetTitle, SheetTitle,
SheetTrigger, SheetTrigger,
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import type { RootNavItem } from "@/lib/root-navigation"; import type { AdminNavItem } from "@/lib/admin-navigation";
type DashboardLayoutProps = { type DashboardLayoutProps = {
title: string; title: string;
description: string; description: string;
icon?: LucideIcon; icon?: LucideIcon;
items: RootNavItem[]; items: AdminNavItem[];
sidebarIconSrc?: string; sidebarIconSrc?: string;
sidebarBrandHref?: string; sidebarBrandHref?: string;
sidebarBrandHoverLabel?: string; sidebarBrandHoverLabel?: string;
sidebarTop?: ReactNode; sidebarTop?: ReactNode;
sidebarFooterItems?: RootNavItem[]; sidebarFooterItems?: AdminNavItem[];
sidebarFooter?: ReactNode; sidebarFooter?: ReactNode;
headerActions?: ReactNode; headerActions?: ReactNode;
children: ReactNode; children: ReactNode;
+1 -1
View File
@@ -66,7 +66,7 @@ export function AppSidebar({
<AppCard className="overflow-hidden bg-sidebar text-sidebar-foreground shadow-sidebar"> <AppCard className="overflow-hidden bg-sidebar text-sidebar-foreground shadow-sidebar">
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-sidebar-foreground/56"> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-sidebar-foreground/56">
Root Access Admin Access
</p> </p>
<CardTitle className="text-base font-semibold">{title}</CardTitle> <CardTitle className="text-base font-semibold">{title}</CardTitle>
<p className="text-sm leading-6 text-sidebar-foreground/72">{description}</p> <p className="text-sm leading-6 text-sidebar-foreground/72">{description}</p>
+1 -1
View File
@@ -36,7 +36,7 @@ export function SiteFooter({ isAdmin = false }: SiteFooterProps) {
))} ))}
{isAdmin ? ( {isAdmin ? (
<a href={buildAdminUrl()} className="text-muted-foreground hover:text-foreground"> <a href={buildAdminUrl()} className="text-muted-foreground hover:text-foreground">
{tNav("root")} {tNav("admin")}
</a> </a>
) : null} ) : null}
</nav> </nav>
+2 -2
View File
@@ -315,7 +315,7 @@ export function SiteHeader({
/> />
{isAdmin ? ( {isAdmin ? (
<Button asChild variant="ghost" size="icon" className={desktopControlButtonClassName}> <Button asChild variant="ghost" size="icon" className={desktopControlButtonClassName}>
<Link href={buildAdminUrl()} aria-label={t("root")}> <Link href={buildAdminUrl()} aria-label={t("admin")}>
<LayoutDashboard className="h-4 w-4 transition-transform duration-300 ease-out hover:scale-110" /> <LayoutDashboard className="h-4 w-4 transition-transform duration-300 ease-out hover:scale-110" />
</Link> </Link>
</Button> </Button>
@@ -465,7 +465,7 @@ export function SiteHeader({
/> />
{isAdmin ? ( {isAdmin ? (
<Button asChild variant="ghost" size="icon" className={mobileControlButtonClassName}> <Button asChild variant="ghost" size="icon" className={mobileControlButtonClassName}>
<Link href={buildAdminUrl()} aria-label={t("root")} onClick={() => setIsOpen(false)}> <Link href={buildAdminUrl()} aria-label={t("admin")} onClick={() => setIsOpen(false)}>
<LayoutDashboard className="h-4 w-4 transition-transform duration-300 ease-out hover:scale-110" /> <LayoutDashboard className="h-4 w-4 transition-transform duration-300 ease-out hover:scale-110" />
</Link> </Link>
</Button> </Button>
+2 -2
View File
@@ -13,8 +13,8 @@ services:
DATABASE_URL: postgresql://postgres:postgres@db:5432/moh_sass?schema=public DATABASE_URL: postgresql://postgres:postgres@db:5432/moh_sass?schema=public
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-me} ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-me}
ADMIN_AUTH_SECRET: ${ADMIN_AUTH_SECRET:-change-me-long-secret} ADMIN_AUTH_SECRET: ${ADMIN_AUTH_SECRET:-change-me-long-secret}
ROOT_BASIC_AUTH_USER: ${ROOT_BASIC_AUTH_USER:-root} ADMIN_BASIC_AUTH_USER: ${ADMIN_BASIC_AUTH_USER:-root}
ROOT_BASIC_AUTH_PASS: ${ROOT_BASIC_AUTH_PASS:-change-me-root} ADMIN_BASIC_AUTH_PASS: ${ADMIN_BASIC_AUTH_PASS:-change-me-root}
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
+71
View File
@@ -15,6 +15,75 @@ function getPassword(): string {
return process.env.ADMIN_PASSWORD ?? ""; return process.env.ADMIN_PASSWORD ?? "";
} }
function parseHostname(value: string | undefined): string | undefined {
if (!value) {
return undefined;
}
const trimmed = value.trim().toLowerCase();
if (!trimmed) {
return undefined;
}
try {
return new URL(trimmed).hostname.toLowerCase();
} catch {
return trimmed.replace(/^https?:\/\//, "").split("/")[0]?.replace(/:\d+$/, "") || undefined;
}
}
function isCookieDomainCandidate(hostname: string | undefined): hostname is string {
return Boolean(
hostname &&
hostname !== "localhost" &&
!hostname.endsWith(".localhost") &&
!/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) &&
!hostname.includes(":"),
);
}
function getSharedCookieHostname(hostnames: string[]): string | undefined {
const validHostnames = hostnames.filter(isCookieDomainCandidate);
if (validHostnames.length === 0) {
return undefined;
}
if (validHostnames.length === 1) {
return validHostnames[0];
}
const reversedSegments = validHostnames.map((hostname) => hostname.split(".").reverse());
const sharedSegments: string[] = [];
for (let index = 0; index < reversedSegments[0].length; index += 1) {
const segment = reversedSegments[0][index];
if (!segment || reversedSegments.some((parts) => parts[index] !== segment)) {
break;
}
sharedSegments.push(segment);
}
if (sharedSegments.length < 2) {
return validHostnames[0];
}
return sharedSegments.reverse().join(".");
}
function getAdminCookieDomain(): string | undefined {
const hostname = getSharedCookieHostname([
parseHostname(process.env.NEXT_PUBLIC_SITE_URL),
parseHostname(process.env.NEXT_PUBLIC_ADMIN_URL),
parseHostname(process.env.ADMIN_HOST),
].filter((value): value is string => Boolean(value)));
return hostname ? `.${hostname}` : undefined;
}
function signValue(value: string): string { function signValue(value: string): string {
return createHmac("sha256", getSecret()).update(value).digest("hex"); return createHmac("sha256", getSecret()).update(value).digest("hex");
} }
@@ -71,6 +140,7 @@ export async function setAdminSessionCookie(): Promise<void> {
sameSite: "lax", sameSite: "lax",
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
path: "/", path: "/",
domain: getAdminCookieDomain(),
maxAge: 60 * 60 * 8, maxAge: 60 * 60 * 8,
}); });
} }
@@ -82,6 +152,7 @@ export async function clearAdminSessionCookie(): Promise<void> {
sameSite: "lax", sameSite: "lax",
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
path: "/", path: "/",
domain: getAdminCookieDomain(),
maxAge: 0, maxAge: 0,
}); });
} }
@@ -12,7 +12,7 @@ import {
type LucideIcon, type LucideIcon,
} from "lucide-react"; } from "lucide-react";
type RootNavigationCopy = { type AdminNavigationCopy = {
overview: string; overview: string;
maintenance: string; maintenance: string;
uiKit: string; uiKit: string;
@@ -24,21 +24,21 @@ type RootNavigationCopy = {
contactProtection?: string; contactProtection?: string;
}; };
export type RootNavItem = { export type AdminNavItem = {
label: string; label: string;
href: string; href: string;
icon: LucideIcon; icon: LucideIcon;
active?: boolean; active?: boolean;
expanded?: boolean; expanded?: boolean;
children?: RootNavItem[]; children?: AdminNavItem[];
}; };
export function getRootNavigation( export function getAdminNavigation(
copy: RootNavigationCopy, copy: AdminNavigationCopy,
active: "overview" | "maintenance" | "ui-kit" | "portfolio" | "media" | "site-settings" | "smtp" | "marquee", active: "overview" | "maintenance" | "ui-kit" | "portfolio" | "media" | "site-settings" | "smtp" | "marquee",
smtpChild?: "settings" | "contact-protection", smtpChild?: "settings" | "contact-protection",
portfolioChild?: "overview" | "projects" | "new-project" | "categories", portfolioChild?: "overview" | "projects" | "new-project" | "categories",
): RootNavItem[] { ): AdminNavItem[] {
return [ return [
{ {
label: copy.overview, label: copy.overview,
+1 -1
View File
@@ -6,7 +6,7 @@
"about": "معلومات عني", "about": "معلومات عني",
"contact": "تواصل معي", "contact": "تواصل معي",
"soon": "قــريــبــاً", "soon": "قــريــبــاً",
"root": "Root", "admin": "Admin",
"openMenu": "فتح القائمة", "openMenu": "فتح القائمة",
"closeMenu": "إغلاق القائمة", "closeMenu": "إغلاق القائمة",
"themeToggle": "تبديل المظهر", "themeToggle": "تبديل المظهر",
+1 -1
View File
@@ -6,7 +6,7 @@
"about": "Über mich", "about": "Über mich",
"contact": "Kontakt", "contact": "Kontakt",
"soon": "Bald", "soon": "Bald",
"root": "Root", "admin": "Admin",
"openMenu": "Menü öffnen", "openMenu": "Menü öffnen",
"closeMenu": "Menü schliessen", "closeMenu": "Menü schliessen",
"themeToggle": "Theme wechseln", "themeToggle": "Theme wechseln",
+1 -1
View File
@@ -6,7 +6,7 @@
"about": "About Me", "about": "About Me",
"contact": "Contact", "contact": "Contact",
"soon": "Soon", "soon": "Soon",
"root": "Root", "admin": "Admin",
"openMenu": "Open menu", "openMenu": "Open menu",
"closeMenu": "Close menu", "closeMenu": "Close menu",
"themeToggle": "Toggle theme", "themeToggle": "Toggle theme",
+18 -13
View File
@@ -13,12 +13,20 @@ import {
const intlMiddleware = createMiddleware(routing); const intlMiddleware = createMiddleware(routing);
function isRootBasicAuthConfigured(): boolean { function getAdminBasicAuthUser(): string {
return Boolean(process.env.ROOT_BASIC_AUTH_USER && process.env.ROOT_BASIC_AUTH_PASS); return process.env.ADMIN_BASIC_AUTH_USER ?? "";
} }
function isRootBasicAuthValid(request: NextRequest): boolean { function getAdminBasicAuthPass(): string {
if (!isRootBasicAuthConfigured()) { return process.env.ADMIN_BASIC_AUTH_PASS ?? "";
}
function isAdminBasicAuthConfigured(): boolean {
return Boolean(getAdminBasicAuthUser() && getAdminBasicAuthPass());
}
function isAdminBasicAuthValid(request: NextRequest): boolean {
if (!isAdminBasicAuthConfigured()) {
return false; return false;
} }
@@ -37,10 +45,7 @@ function isRootBasicAuthValid(request: NextRequest): boolean {
const user = decoded.slice(0, index); const user = decoded.slice(0, index);
const pass = decoded.slice(index + 1); const pass = decoded.slice(index + 1);
return ( return user === getAdminBasicAuthUser() && pass === getAdminBasicAuthPass();
user === process.env.ROOT_BASIC_AUTH_USER &&
pass === process.env.ROOT_BASIC_AUTH_PASS
);
} catch { } catch {
return false; return false;
} }
@@ -52,7 +57,7 @@ export default async function middleware(request: NextRequest) {
request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? request.nextUrl.hostname, request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? request.nextUrl.hostname,
); );
const isAdminRequest = isAdminHost(hostname); const isAdminRequest = isAdminHost(hostname);
const rootRobotsHeaders = { const adminRobotsHeaders = {
"X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet, noimageindex", "X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet, noimageindex",
}; };
@@ -63,12 +68,12 @@ export default async function middleware(request: NextRequest) {
} }
if (isAdminRequest) { if (isAdminRequest) {
if (isRootBasicAuthConfigured() && !isRootBasicAuthValid(request)) { if (isAdminBasicAuthConfigured() && !isAdminBasicAuthValid(request)) {
return new NextResponse("Authentication required", { return new NextResponse("Authentication required", {
status: 401, status: 401,
headers: { headers: {
"WWW-Authenticate": 'Basic realm="Root Area", charset="UTF-8"', "WWW-Authenticate": 'Basic realm="Admin Area", charset="UTF-8"',
...rootRobotsHeaders, ...adminRobotsHeaders,
}, },
}); });
} }
@@ -77,7 +82,7 @@ export default async function middleware(request: NextRequest) {
rewriteUrl.pathname = toInternalAdminPath(pathname); rewriteUrl.pathname = toInternalAdminPath(pathname);
const response = NextResponse.rewrite(rewriteUrl); const response = NextResponse.rewrite(rewriteUrl);
response.headers.set("X-Robots-Tag", rootRobotsHeaders["X-Robots-Tag"]); response.headers.set("X-Robots-Tag", adminRobotsHeaders["X-Robots-Tag"]);
return response; return response;
} }