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"
ADMIN_PASSWORD="change-me"
ADMIN_AUTH_SECRET="replace-with-a-long-random-secret"
ROOT_BASIC_AUTH_USER="change-me"
ROOT_BASIC_AUTH_PASS="change-me"
ADMIN_BASIC_AUTH_USER="change-me"
ADMIN_BASIC_AUTH_PASS="change-me"
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
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 { getMaintenanceMode } from "@/lib/app-config";
import { AppCard } from "@/components/ui/app-card";
@@ -27,7 +27,7 @@ const copy = {
backToSite: "Zur Website",
};
export default async function RootMaintenancePage() {
export default async function AdminMaintenancePage() {
const authenticated = await isAdminAuthenticated();
if (!authenticated) {
@@ -43,7 +43,7 @@ export default async function RootMaintenancePage() {
}
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="maintenance"
logoutAction={logoutAction}
@@ -63,6 +63,6 @@ export default async function RootMaintenancePage() {
</CardContent>
</AppCard>
</MotionFade>
</RootDashboardShell>
</AdminDashboardShell>
);
}
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
import { MarqueeSettingsForm } from "@/components/root/marquee-settings-form";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
import { MarqueeSettingsForm } from "@/components/admin/marquee-settings-form";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { MotionFade } from "@/components/motion-fade";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getMarqueeSettings } from "@/lib/app-config";
@@ -25,7 +25,7 @@ const copy = {
backToSite: "Zur Website",
};
export default async function RootMarqueePage() {
export default async function AdminMarqueePage() {
if (!(await isAdminAuthenticated())) {
redirect("/");
}
@@ -40,7 +40,7 @@ export default async function RootMarqueePage() {
const marqueeSettings = await getMarqueeSettings();
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="marquee"
logoutAction={logoutAction}
@@ -55,6 +55,6 @@ export default async function RootMarqueePage() {
/>
</MotionFade>
</div>
</RootDashboardShell>
</AdminDashboardShell>
);
}
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
import { MediaLibraryManager } from "@/components/root/media-library-manager";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
import { MediaLibraryManager } from "@/components/admin/media-library-manager";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminMediaAssets } from "@/lib/media";
@@ -20,7 +20,7 @@ const copy = {
backToSite: "Zur Website",
};
export default async function RootMediaPage() {
export default async function AdminMediaPage() {
if (!(await isAdminAuthenticated())) {
redirect("/");
}
@@ -35,7 +35,7 @@ export default async function RootMediaPage() {
const mediaAssets = await getAdminMediaAssets();
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="media"
logoutAction={logoutAction}
@@ -45,6 +45,6 @@ export default async function RootMediaPage() {
<div className="space-y-6">
<MediaLibraryManager mediaAssets={mediaAssets} />
</div>
</RootDashboardShell>
</AdminDashboardShell>
);
}
+8 -8
View File
@@ -4,7 +4,7 @@ import { redirect } from "next/navigation";
import { Container } from "@/components/layout/container";
import { StatsCard } from "@/components/dashboard/stats-card";
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 { Button } from "@/components/ui/button";
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 { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio";
type RootPageProps = {
type AdminPageProps = {
searchParams?: Promise<{
error?: string;
}>;
@@ -43,14 +43,14 @@ const copy = {
uiKit: "UI Kit",
logout: "Ausloggen",
backToSite: "Zur Website",
loginTitle: "Root Login",
loginTitle: "Admin Login",
loginText: "Nur autorisierte Nutzer duerfen diesen Bereich verwenden.",
passwordLabel: "Passwort",
loginButton: "Einloggen",
invalidLogin: "Falsches Passwort.",
lockedLogin: "Zu viele Fehlversuche. Bitte spaeter erneut versuchen.",
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",
maintenanceOff: "Inaktiv",
portfolioOnline: "Portfolio online",
@@ -60,11 +60,11 @@ const copy = {
activeCategories: "Active Categories",
};
export default async function RootPage({ searchParams }: RootPageProps) {
export default async function AdminPage({ searchParams }: AdminPageProps) {
const resolvedSearchParams = await searchParams;
const authConfigured = isAdminAuthConfigured();
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 lockState = await getAdminLockState();
@@ -182,7 +182,7 @@ export default async function RootPage({ searchParams }: RootPageProps) {
const imageCount = mediaAssets.filter((asset) => asset.kind === "IMAGE").length;
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="overview"
logoutAction={logoutAction}
@@ -238,6 +238,6 @@ export default async function RootPage({ searchParams }: RootPageProps) {
</MotionFade>
</section>
</div>
</RootDashboardShell>
</AdminDashboardShell>
);
}
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
import { PortfolioCategoriesManager } from "@/components/root/portfolio-categories-manager";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
import { PortfolioCategoriesManager } from "@/components/admin/portfolio-categories-manager";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminPortfolioCategories } from "@/lib/portfolio";
@@ -22,7 +22,7 @@ const copy = {
backToSite: "Zur Website",
};
export default async function RootPortfolioCategoriesPage() {
export default async function AdminPortfolioCategoriesPage() {
if (!(await isAdminAuthenticated())) {
redirect("/");
}
@@ -39,7 +39,7 @@ export default async function RootPortfolioCategoriesPage() {
const assignedProjects = categories.reduce((sum, category) => sum + category.projectCount, 0);
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="portfolio"
portfolioChild="categories"
@@ -56,6 +56,6 @@ export default async function RootPortfolioCategoriesPage() {
removeCategoryAction={deleteCategoryAction}
/>
</div>
</RootDashboardShell>
</AdminDashboardShell>
);
}
@@ -1,5 +1,5 @@
import { redirect } from "next/navigation";
export default function RootPortfolioMediaRedirectPage() {
export default function AdminPortfolioMediaRedirectPage() {
redirect("/media");
}
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
import { PortfolioProjectsOverview } from "@/components/root/portfolio-projects-overview";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { PortfolioProjectsOverview } from "@/components/admin/portfolio-projects-overview";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio";
@@ -20,7 +20,7 @@ const copy = {
backToSite: "Zur Website",
};
type RootPortfolioPageProps = {
type AdminPortfolioPageProps = {
searchParams?: Promise<{
category?: string;
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;
if (!(await isAdminAuthenticated())) {
@@ -58,7 +58,7 @@ export default async function RootPortfolioPage({ searchParams }: RootPortfolioP
]);
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="portfolio"
portfolioChild="overview"
@@ -74,6 +74,6 @@ export default async function RootPortfolioPage({ searchParams }: RootPortfolioP
selectedStatus={selectedStatus}
/>
</div>
</RootDashboardShell>
</AdminDashboardShell>
);
}
@@ -1,8 +1,8 @@
import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade";
import { PortfolioProjectForm } from "@/components/root/portfolio-project-form";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
import { PortfolioProjectForm } from "@/components/admin/portfolio-project-form";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { AppCard } from "@/components/ui/app-card";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
@@ -43,15 +43,15 @@ const copy = {
deleteProject: "Projekt loeschen",
};
type RootPortfolioProjectPageProps = {
type AdminPortfolioProjectPageProps = {
params: Promise<{
id: string;
}>;
};
export default async function RootPortfolioProjectPage({
export default async function AdminPortfolioProjectPage({
params,
}: RootPortfolioProjectPageProps) {
}: AdminPortfolioProjectPageProps) {
if (!(await isAdminAuthenticated())) {
redirect("/");
}
@@ -76,7 +76,7 @@ export default async function RootPortfolioProjectPage({
}
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="portfolio"
portfolioChild="projects"
@@ -132,6 +132,6 @@ export default async function RootPortfolioProjectPage({
</AppCard>
</MotionFade>
</div>
</RootDashboardShell>
</AdminDashboardShell>
);
}
@@ -1,8 +1,8 @@
import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade";
import { PortfolioProjectForm } from "@/components/root/portfolio-project-form";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
import { PortfolioProjectForm } from "@/components/admin/portfolio-project-form";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getMediaOptions } from "@/lib/media";
import { getActivePortfolioCategories } from "@/lib/portfolio";
@@ -24,7 +24,7 @@ const copy = {
backToSite: "Zur Website",
};
export default async function RootNewPortfolioProjectPage() {
export default async function AdminNewPortfolioProjectPage() {
if (!(await isAdminAuthenticated())) {
redirect("/");
}
@@ -42,7 +42,7 @@ export default async function RootNewPortfolioProjectPage() {
]);
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="portfolio"
portfolioChild="new-project"
@@ -61,6 +61,6 @@ export default async function RootNewPortfolioProjectPage() {
/>
</MotionFade>
</div>
</RootDashboardShell>
</AdminDashboardShell>
);
}
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
import { PortfolioProjectsOverview } from "@/components/root/portfolio-projects-overview";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
import { PortfolioProjectsOverview } from "@/components/admin/portfolio-projects-overview";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio";
@@ -20,16 +20,16 @@ const copy = {
backToSite: "Zur Website",
};
type RootPortfolioProjectsPageProps = {
type AdminPortfolioProjectsPageProps = {
searchParams?: Promise<{
category?: string;
status?: "all" | "draft" | "published";
}>;
};
export default async function RootPortfolioProjectsPage({
export default async function AdminPortfolioProjectsPage({
searchParams,
}: RootPortfolioProjectsPageProps) {
}: AdminPortfolioProjectsPageProps) {
const resolvedSearchParams = await searchParams;
if (!(await isAdminAuthenticated())) {
@@ -58,7 +58,7 @@ export default async function RootPortfolioProjectsPage({
]);
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="portfolio"
portfolioChild="projects"
@@ -74,6 +74,6 @@ export default async function RootPortfolioProjectsPage({
selectedStatus={selectedStatus}
/>
</div>
</RootDashboardShell>
</AdminDashboardShell>
);
}
@@ -2,8 +2,8 @@ import { MediaKind } from "@prisma/client";
import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade";
import { SiteSettingsForm } from "@/components/root/site-settings-form";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
import { SiteSettingsForm } from "@/components/admin/site-settings-form";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import {
getSiteSettings,
@@ -29,7 +29,7 @@ const copy = {
backToSite: "Zur Website",
};
export default async function RootSiteSettingsPage() {
export default async function AdminSiteSettingsPage() {
if (!(await isAdminAuthenticated())) {
redirect("/");
}
@@ -48,7 +48,7 @@ export default async function RootSiteSettingsPage() {
]);
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="site-settings"
logoutAction={logoutAction}
@@ -65,6 +65,6 @@ export default async function RootSiteSettingsPage() {
/>
</MotionFade>
</div>
</RootDashboardShell>
</AdminDashboardShell>
);
}
@@ -1,8 +1,8 @@
import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade";
import { ContactProtectionForm } from "@/components/root/contact-protection-form";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
import { ContactProtectionForm } from "@/components/admin/contact-protection-form";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getContactProtectionFormValues } from "@/lib/app-config";
@@ -25,7 +25,7 @@ const copy = {
backToSite: "Zur Website",
};
export default async function RootSMTPProtectionPage() {
export default async function AdminSMTPProtectionPage() {
if (!(await isAdminAuthenticated())) {
redirect("/");
}
@@ -40,7 +40,7 @@ export default async function RootSMTPProtectionPage() {
const settings = await getContactProtectionFormValues();
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="smtp"
smtpChild="contact-protection"
@@ -56,6 +56,6 @@ export default async function RootSMTPProtectionPage() {
/>
</MotionFade>
</div>
</RootDashboardShell>
</AdminDashboardShell>
);
}
@@ -1,8 +1,8 @@
import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
import { SMTPSettingsForm } from "@/components/root/smtp-settings-form";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { SMTPSettingsForm } from "@/components/admin/smtp-settings-form";
import { Button } from "@/components/ui/button";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getMailSettingsFormValues } from "@/lib/app-config";
@@ -26,7 +26,7 @@ const copy = {
backToSite: "Zur Website",
};
export default async function RootSMTPPage() {
export default async function AdminSMTPPage() {
if (!(await isAdminAuthenticated())) {
redirect("/");
}
@@ -41,7 +41,7 @@ export default async function RootSMTPPage() {
const mailSettings = await getMailSettingsFormValues();
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="smtp"
smtpChild="settings"
@@ -64,6 +64,6 @@ export default async function RootSMTPPage() {
/>
</MotionFade>
</div>
</RootDashboardShell>
</AdminDashboardShell>
);
}
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
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 { UiKitShowcase } from "@/components/ui/ui-kit-showcase";
@@ -9,7 +9,7 @@ export const dynamic = "force-dynamic";
const copy = {
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",
overview: "Uebersicht",
uiKit: "UI Kit",
@@ -20,7 +20,7 @@ const copy = {
backToSite: "Zur Website",
};
export default async function RootUiKitPage() {
export default async function AdminUiKitPage() {
const authenticated = await isAdminAuthenticated();
if (!authenticated) {
@@ -35,7 +35,7 @@ export default async function RootUiKitPage() {
}
return (
<RootDashboardShell
<AdminDashboardShell
copy={copy}
active="ui-kit"
logoutAction={logoutAction}
@@ -45,6 +45,6 @@ export default async function RootUiKitPage() {
<MotionFade delay={0.1}>
<UiKitShowcase localeKey="de" />
</MotionFade>
</RootDashboardShell>
</AdminDashboardShell>
);
}
+2 -2
View File
@@ -1,2 +1,2 @@
export { metadata } from "../root/layout";
export { default } from "../root/layout";
export { metadata } from "../_admin/layout";
export { default } from "../_admin/layout";
+2 -2
View File
@@ -1,2 +1,2 @@
export { dynamic } from "../../root/maintenance/page";
export { default } from "../../root/maintenance/page";
export { dynamic } from "../../_admin/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 { default } from "../../root/media/page";
export { dynamic } from "../../_admin/media/page";
export { default } from "../../_admin/media/page";
+2 -2
View File
@@ -1,2 +1,2 @@
export { dynamic } from "../root/page";
export { default } from "../root/page";
export { dynamic } from "../_admin/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 Script from "next/script";
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 { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/toaster";
@@ -15,17 +15,17 @@ import Link from "next/link";
import { DashboardLayout } from "@/components/dashboard/dashboard-layout";
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 { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
import { buildSiteUrl } from "@/lib/admin-routing";
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;
subtitle: string;
overview: string;
@@ -41,8 +41,8 @@ type RootDashboardCopy = {
backToSite: string;
};
type RootDashboardShellProps = {
copy: RootDashboardCopy;
type AdminDashboardShellProps = {
copy: AdminDashboardCopy;
active: "overview" | "maintenance" | "ui-kit" | "portfolio" | "media" | "site-settings" | "smtp" | "marquee";
smtpChild?: "settings" | "contact-protection";
portfolioChild?: "overview" | "projects" | "new-project" | "categories";
@@ -55,7 +55,7 @@ type RootDashboardShellProps = {
children: ReactNode;
};
export async function RootDashboardShell({
export async function AdminDashboardShell({
copy,
active,
smtpChild,
@@ -67,12 +67,12 @@ export async function RootDashboardShell({
sidebarTopContent,
toolbar,
children,
}: RootDashboardShellProps) {
}: AdminDashboardShellProps) {
const [mediaBindings, maintenanceEnabled] = await Promise.all([
getSiteSettingsMediaBindings(),
getMaintenanceMode(),
]);
const sidebarItems = getRootNavigation(copy, active, smtpChild, portfolioChild);
const sidebarItems = getAdminNavigation(copy, active, smtpChild, portfolioChild);
const normalizedSidebarItems = sidebarItems.filter(
(item) =>
item.href !== "/maintenance" &&
@@ -28,7 +28,7 @@ import { Label } from "@/components/ui/label";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
import { cn } from "@/lib/utils";
import { createMediaAssetAction, deleteMediaAssetAction } from "@/app/root/media/actions";
import { createMediaAssetAction, deleteMediaAssetAction } from "@/app/_admin/media/actions";
const copy = {
addMedia: "Upload Media File",
@@ -13,7 +13,7 @@ import {
Trash2,
} 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 { AppCard } from "@/components/ui/app-card";
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 { useState } from "react";
import { deleteProjectAction } from "@/app/root/portfolio/actions";
import { deleteProjectAction } from "@/app/_admin/portfolio/actions";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -15,7 +15,7 @@ import {
} from "lucide-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 { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -3,7 +3,7 @@ import Link from "next/link";
import { StatsCard } from "@/components/dashboard/stats-card";
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 { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
+3 -3
View File
@@ -11,18 +11,18 @@ import {
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import type { RootNavItem } from "@/lib/root-navigation";
import type { AdminNavItem } from "@/lib/admin-navigation";
type DashboardLayoutProps = {
title: string;
description: string;
icon?: LucideIcon;
items: RootNavItem[];
items: AdminNavItem[];
sidebarIconSrc?: string;
sidebarBrandHref?: string;
sidebarBrandHoverLabel?: string;
sidebarTop?: ReactNode;
sidebarFooterItems?: RootNavItem[];
sidebarFooterItems?: AdminNavItem[];
sidebarFooter?: ReactNode;
headerActions?: 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">
<CardHeader className="pb-4">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-sidebar-foreground/56">
Root Access
Admin Access
</p>
<CardTitle className="text-base font-semibold">{title}</CardTitle>
<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 ? (
<a href={buildAdminUrl()} className="text-muted-foreground hover:text-foreground">
{tNav("root")}
{tNav("admin")}
</a>
) : null}
</nav>
+2 -2
View File
@@ -315,7 +315,7 @@ export function SiteHeader({
/>
{isAdmin ? (
<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" />
</Link>
</Button>
@@ -465,7 +465,7 @@ export function SiteHeader({
/>
{isAdmin ? (
<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" />
</Link>
</Button>
+2 -2
View File
@@ -13,8 +13,8 @@ services:
DATABASE_URL: postgresql://postgres:postgres@db:5432/moh_sass?schema=public
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-me}
ADMIN_AUTH_SECRET: ${ADMIN_AUTH_SECRET:-change-me-long-secret}
ROOT_BASIC_AUTH_USER: ${ROOT_BASIC_AUTH_USER:-root}
ROOT_BASIC_AUTH_PASS: ${ROOT_BASIC_AUTH_PASS:-change-me-root}
ADMIN_BASIC_AUTH_USER: ${ADMIN_BASIC_AUTH_USER:-root}
ADMIN_BASIC_AUTH_PASS: ${ADMIN_BASIC_AUTH_PASS:-change-me-root}
depends_on:
db:
condition: service_healthy
+71
View File
@@ -15,6 +15,75 @@ function getPassword(): string {
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 {
return createHmac("sha256", getSecret()).update(value).digest("hex");
}
@@ -71,6 +140,7 @@ export async function setAdminSessionCookie(): Promise<void> {
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
domain: getAdminCookieDomain(),
maxAge: 60 * 60 * 8,
});
}
@@ -82,6 +152,7 @@ export async function clearAdminSessionCookie(): Promise<void> {
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
domain: getAdminCookieDomain(),
maxAge: 0,
});
}
@@ -12,7 +12,7 @@ import {
type LucideIcon,
} from "lucide-react";
type RootNavigationCopy = {
type AdminNavigationCopy = {
overview: string;
maintenance: string;
uiKit: string;
@@ -24,21 +24,21 @@ type RootNavigationCopy = {
contactProtection?: string;
};
export type RootNavItem = {
export type AdminNavItem = {
label: string;
href: string;
icon: LucideIcon;
active?: boolean;
expanded?: boolean;
children?: RootNavItem[];
children?: AdminNavItem[];
};
export function getRootNavigation(
copy: RootNavigationCopy,
export function getAdminNavigation(
copy: AdminNavigationCopy,
active: "overview" | "maintenance" | "ui-kit" | "portfolio" | "media" | "site-settings" | "smtp" | "marquee",
smtpChild?: "settings" | "contact-protection",
portfolioChild?: "overview" | "projects" | "new-project" | "categories",
): RootNavItem[] {
): AdminNavItem[] {
return [
{
label: copy.overview,
+1 -1
View File
@@ -6,7 +6,7 @@
"about": "معلومات عني",
"contact": "تواصل معي",
"soon": "قــريــبــاً",
"root": "Root",
"admin": "Admin",
"openMenu": "فتح القائمة",
"closeMenu": "إغلاق القائمة",
"themeToggle": "تبديل المظهر",
+1 -1
View File
@@ -6,7 +6,7 @@
"about": "Über mich",
"contact": "Kontakt",
"soon": "Bald",
"root": "Root",
"admin": "Admin",
"openMenu": "Menü öffnen",
"closeMenu": "Menü schliessen",
"themeToggle": "Theme wechseln",
+1 -1
View File
@@ -6,7 +6,7 @@
"about": "About Me",
"contact": "Contact",
"soon": "Soon",
"root": "Root",
"admin": "Admin",
"openMenu": "Open menu",
"closeMenu": "Close menu",
"themeToggle": "Toggle theme",
+18 -13
View File
@@ -13,12 +13,20 @@ import {
const intlMiddleware = createMiddleware(routing);
function isRootBasicAuthConfigured(): boolean {
return Boolean(process.env.ROOT_BASIC_AUTH_USER && process.env.ROOT_BASIC_AUTH_PASS);
function getAdminBasicAuthUser(): string {
return process.env.ADMIN_BASIC_AUTH_USER ?? "";
}
function isRootBasicAuthValid(request: NextRequest): boolean {
if (!isRootBasicAuthConfigured()) {
function getAdminBasicAuthPass(): string {
return process.env.ADMIN_BASIC_AUTH_PASS ?? "";
}
function isAdminBasicAuthConfigured(): boolean {
return Boolean(getAdminBasicAuthUser() && getAdminBasicAuthPass());
}
function isAdminBasicAuthValid(request: NextRequest): boolean {
if (!isAdminBasicAuthConfigured()) {
return false;
}
@@ -37,10 +45,7 @@ function isRootBasicAuthValid(request: NextRequest): boolean {
const user = decoded.slice(0, index);
const pass = decoded.slice(index + 1);
return (
user === process.env.ROOT_BASIC_AUTH_USER &&
pass === process.env.ROOT_BASIC_AUTH_PASS
);
return user === getAdminBasicAuthUser() && pass === getAdminBasicAuthPass();
} catch {
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,
);
const isAdminRequest = isAdminHost(hostname);
const rootRobotsHeaders = {
const adminRobotsHeaders = {
"X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet, noimageindex",
};
@@ -63,12 +68,12 @@ export default async function middleware(request: NextRequest) {
}
if (isAdminRequest) {
if (isRootBasicAuthConfigured() && !isRootBasicAuthValid(request)) {
if (isAdminBasicAuthConfigured() && !isAdminBasicAuthValid(request)) {
return new NextResponse("Authentication required", {
status: 401,
headers: {
"WWW-Authenticate": 'Basic realm="Root Area", charset="UTF-8"',
...rootRobotsHeaders,
"WWW-Authenticate": 'Basic realm="Admin Area", charset="UTF-8"',
...adminRobotsHeaders,
},
});
}
@@ -77,7 +82,7 @@ export default async function middleware(request: NextRequest) {
rewriteUrl.pathname = toInternalAdminPath(pathname);
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;
}