diff --git a/.env.example b/.env.example index 5bd2437..2d041a2 100644 --- a/.env.example +++ b/.env.example @@ -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" diff --git a/app/root/layout.tsx b/app/_admin/layout.tsx similarity index 100% rename from app/root/layout.tsx rename to app/_admin/layout.tsx diff --git a/app/root/maintenance/actions.ts b/app/_admin/maintenance/actions.ts similarity index 100% rename from app/root/maintenance/actions.ts rename to app/_admin/maintenance/actions.ts diff --git a/app/root/maintenance/page.tsx b/app/_admin/maintenance/page.tsx similarity index 91% rename from app/root/maintenance/page.tsx rename to app/_admin/maintenance/page.tsx index efb773d..c88566f 100644 --- a/app/root/maintenance/page.tsx +++ b/app/_admin/maintenance/page.tsx @@ -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 ( - - + ); } diff --git a/app/root/marquee/actions.ts b/app/_admin/marquee/actions.ts similarity index 100% rename from app/root/marquee/actions.ts rename to app/_admin/marquee/actions.ts diff --git a/app/root/marquee/page.tsx b/app/_admin/marquee/page.tsx similarity index 83% rename from app/root/marquee/page.tsx rename to app/_admin/marquee/page.tsx index 5bf406b..22eef58 100644 --- a/app/root/marquee/page.tsx +++ b/app/_admin/marquee/page.tsx @@ -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 ( - - + ); } diff --git a/app/root/media/actions.ts b/app/_admin/media/actions.ts similarity index 100% rename from app/root/media/actions.ts rename to app/_admin/media/actions.ts diff --git a/app/root/media/page.tsx b/app/_admin/media/page.tsx similarity index 80% rename from app/root/media/page.tsx rename to app/_admin/media/page.tsx index 9a336c9..e45129d 100644 --- a/app/root/media/page.tsx +++ b/app/_admin/media/page.tsx @@ -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 ( - - + ); } diff --git a/app/root/page.tsx b/app/_admin/page.tsx similarity index 95% rename from app/root/page.tsx rename to app/_admin/page.tsx index 41126b7..673e53e 100644 --- a/app/root/page.tsx +++ b/app/_admin/page.tsx @@ -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 ( - - + ); } diff --git a/app/root/portfolio/actions.ts b/app/_admin/portfolio/actions.ts similarity index 100% rename from app/root/portfolio/actions.ts rename to app/_admin/portfolio/actions.ts diff --git a/app/root/portfolio/categories/page.tsx b/app/_admin/portfolio/categories/page.tsx similarity index 84% rename from app/root/portfolio/categories/page.tsx rename to app/_admin/portfolio/categories/page.tsx index 9c90bc3..73cdd9f 100644 --- a/app/root/portfolio/categories/page.tsx +++ b/app/_admin/portfolio/categories/page.tsx @@ -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 ( - - + ); } diff --git a/app/root/portfolio/media/page.tsx b/app/_admin/portfolio/media/page.tsx similarity index 53% rename from app/root/portfolio/media/page.tsx rename to app/_admin/portfolio/media/page.tsx index 3d84c73..dfde150 100644 --- a/app/root/portfolio/media/page.tsx +++ b/app/_admin/portfolio/media/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; -export default function RootPortfolioMediaRedirectPage() { +export default function AdminPortfolioMediaRedirectPage() { redirect("/media"); } diff --git a/app/root/portfolio/page.tsx b/app/_admin/portfolio/page.tsx similarity index 84% rename from app/root/portfolio/page.tsx rename to app/_admin/portfolio/page.tsx index bd2afa2..11b115c 100644 --- a/app/root/portfolio/page.tsx +++ b/app/_admin/portfolio/page.tsx @@ -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 ( - - + ); } diff --git a/app/root/portfolio/projects/[id]/page.tsx b/app/_admin/portfolio/projects/[id]/page.tsx similarity index 91% rename from app/root/portfolio/projects/[id]/page.tsx rename to app/_admin/portfolio/projects/[id]/page.tsx index 3df2644..15e0b12 100644 --- a/app/root/portfolio/projects/[id]/page.tsx +++ b/app/_admin/portfolio/projects/[id]/page.tsx @@ -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 ( - - + ); } diff --git a/app/root/portfolio/projects/new/page.tsx b/app/_admin/portfolio/projects/new/page.tsx similarity index 85% rename from app/root/portfolio/projects/new/page.tsx rename to app/_admin/portfolio/projects/new/page.tsx index 26d061e..5509d12 100644 --- a/app/root/portfolio/projects/new/page.tsx +++ b/app/_admin/portfolio/projects/new/page.tsx @@ -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 ( - - + ); } diff --git a/app/root/portfolio/projects/page.tsx b/app/_admin/portfolio/projects/page.tsx similarity index 84% rename from app/root/portfolio/projects/page.tsx rename to app/_admin/portfolio/projects/page.tsx index 79319a1..5831f62 100644 --- a/app/root/portfolio/projects/page.tsx +++ b/app/_admin/portfolio/projects/page.tsx @@ -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 ( - - + ); } diff --git a/app/root/site-settings/actions.ts b/app/_admin/site-settings/actions.ts similarity index 100% rename from app/root/site-settings/actions.ts rename to app/_admin/site-settings/actions.ts diff --git a/app/root/site-settings/page.tsx b/app/_admin/site-settings/page.tsx similarity index 86% rename from app/root/site-settings/page.tsx rename to app/_admin/site-settings/page.tsx index beded2e..5ee9eb7 100644 --- a/app/root/site-settings/page.tsx +++ b/app/_admin/site-settings/page.tsx @@ -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 ( - - + ); } diff --git a/app/root/smtp/actions.ts b/app/_admin/smtp/actions.ts similarity index 100% rename from app/root/smtp/actions.ts rename to app/_admin/smtp/actions.ts diff --git a/app/root/smtp/contact-protection/actions.ts b/app/_admin/smtp/contact-protection/actions.ts similarity index 100% rename from app/root/smtp/contact-protection/actions.ts rename to app/_admin/smtp/contact-protection/actions.ts diff --git a/app/root/smtp/contact-protection/page.tsx b/app/_admin/smtp/contact-protection/page.tsx similarity index 83% rename from app/root/smtp/contact-protection/page.tsx rename to app/_admin/smtp/contact-protection/page.tsx index 1e13f5b..3526f9c 100644 --- a/app/root/smtp/contact-protection/page.tsx +++ b/app/_admin/smtp/contact-protection/page.tsx @@ -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 ( - - + ); } diff --git a/app/root/smtp/page.tsx b/app/_admin/smtp/page.tsx similarity index 86% rename from app/root/smtp/page.tsx rename to app/_admin/smtp/page.tsx index 8dd2563..4d44693 100644 --- a/app/root/smtp/page.tsx +++ b/app/_admin/smtp/page.tsx @@ -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 ( - - + ); } diff --git a/app/root/ui-kit/page.tsx b/app/_admin/ui-kit/page.tsx similarity index 79% rename from app/root/ui-kit/page.tsx rename to app/_admin/ui-kit/page.tsx index 8fac469..2a8c9ba 100644 --- a/app/root/ui-kit/page.tsx +++ b/app/_admin/ui-kit/page.tsx @@ -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 ( - - + ); } diff --git a/app/admin-internal/layout.tsx b/app/admin-internal/layout.tsx index aaa652e..f6508ec 100644 --- a/app/admin-internal/layout.tsx +++ b/app/admin-internal/layout.tsx @@ -1,2 +1,2 @@ -export { metadata } from "../root/layout"; -export { default } from "../root/layout"; +export { metadata } from "../_admin/layout"; +export { default } from "../_admin/layout"; diff --git a/app/admin-internal/maintenance/page.tsx b/app/admin-internal/maintenance/page.tsx index b100301..6aa01c4 100644 --- a/app/admin-internal/maintenance/page.tsx +++ b/app/admin-internal/maintenance/page.tsx @@ -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"; diff --git a/app/admin-internal/marquee/page.tsx b/app/admin-internal/marquee/page.tsx index 292ef2f..1fb130b 100644 --- a/app/admin-internal/marquee/page.tsx +++ b/app/admin-internal/marquee/page.tsx @@ -1 +1 @@ -export { default } from "../../root/marquee/page"; +export { default } from "../../_admin/marquee/page"; diff --git a/app/admin-internal/media/page.tsx b/app/admin-internal/media/page.tsx index 966e62f..b99431c 100644 --- a/app/admin-internal/media/page.tsx +++ b/app/admin-internal/media/page.tsx @@ -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"; diff --git a/app/admin-internal/page.tsx b/app/admin-internal/page.tsx index d087c07..98c7a67 100644 --- a/app/admin-internal/page.tsx +++ b/app/admin-internal/page.tsx @@ -1,2 +1,2 @@ -export { dynamic } from "../root/page"; -export { default } from "../root/page"; +export { dynamic } from "../_admin/page"; +export { default } from "../_admin/page"; diff --git a/app/admin-internal/portfolio/categories/page.tsx b/app/admin-internal/portfolio/categories/page.tsx index 9b60cd3..9018c4a 100644 --- a/app/admin-internal/portfolio/categories/page.tsx +++ b/app/admin-internal/portfolio/categories/page.tsx @@ -1 +1 @@ -export { default } from "../../../root/portfolio/categories/page"; +export { default } from "../../../_admin/portfolio/categories/page"; diff --git a/app/admin-internal/portfolio/media/page.tsx b/app/admin-internal/portfolio/media/page.tsx index 732fa29..b8f5f13 100644 --- a/app/admin-internal/portfolio/media/page.tsx +++ b/app/admin-internal/portfolio/media/page.tsx @@ -1 +1 @@ -export { default } from "../../../root/portfolio/media/page"; +export { default } from "../../../_admin/portfolio/media/page"; diff --git a/app/admin-internal/portfolio/page.tsx b/app/admin-internal/portfolio/page.tsx index 9b44930..c1ded26 100644 --- a/app/admin-internal/portfolio/page.tsx +++ b/app/admin-internal/portfolio/page.tsx @@ -1 +1 @@ -export { default } from "../../root/portfolio/page"; +export { default } from "../../_admin/portfolio/page"; diff --git a/app/admin-internal/portfolio/projects/[id]/page.tsx b/app/admin-internal/portfolio/projects/[id]/page.tsx index 37a898d..bdb4e41 100644 --- a/app/admin-internal/portfolio/projects/[id]/page.tsx +++ b/app/admin-internal/portfolio/projects/[id]/page.tsx @@ -1 +1 @@ -export { default } from "../../../../root/portfolio/projects/[id]/page"; +export { default } from "../../../../_admin/portfolio/projects/[id]/page"; diff --git a/app/admin-internal/portfolio/projects/new/page.tsx b/app/admin-internal/portfolio/projects/new/page.tsx index 60a3a51..dba419e 100644 --- a/app/admin-internal/portfolio/projects/new/page.tsx +++ b/app/admin-internal/portfolio/projects/new/page.tsx @@ -1 +1 @@ -export { default } from "../../../../root/portfolio/projects/new/page"; +export { default } from "../../../../_admin/portfolio/projects/new/page"; diff --git a/app/admin-internal/portfolio/projects/page.tsx b/app/admin-internal/portfolio/projects/page.tsx index 88752dd..b85d3e9 100644 --- a/app/admin-internal/portfolio/projects/page.tsx +++ b/app/admin-internal/portfolio/projects/page.tsx @@ -1 +1 @@ -export { default } from "../../../root/portfolio/projects/page"; +export { default } from "../../../_admin/portfolio/projects/page"; diff --git a/app/admin-internal/site-settings/page.tsx b/app/admin-internal/site-settings/page.tsx index 01d04ea..b3884cd 100644 --- a/app/admin-internal/site-settings/page.tsx +++ b/app/admin-internal/site-settings/page.tsx @@ -1 +1 @@ -export { default } from "../../root/site-settings/page"; +export { default } from "../../_admin/site-settings/page"; diff --git a/app/admin-internal/smtp/contact-protection/page.tsx b/app/admin-internal/smtp/contact-protection/page.tsx index 34e80a5..60cd1ad 100644 --- a/app/admin-internal/smtp/contact-protection/page.tsx +++ b/app/admin-internal/smtp/contact-protection/page.tsx @@ -1 +1 @@ -export { default } from "../../../root/smtp/contact-protection/page"; +export { default } from "../../../_admin/smtp/contact-protection/page"; diff --git a/app/admin-internal/smtp/page.tsx b/app/admin-internal/smtp/page.tsx index 8c935aa..5e58433 100644 --- a/app/admin-internal/smtp/page.tsx +++ b/app/admin-internal/smtp/page.tsx @@ -1 +1 @@ -export { default } from "../../root/smtp/page"; +export { default } from "../../_admin/smtp/page"; diff --git a/app/admin-internal/ui-kit/page.tsx b/app/admin-internal/ui-kit/page.tsx index a9d5ab1..8ea872c 100644 --- a/app/admin-internal/ui-kit/page.tsx +++ b/app/admin-internal/ui-kit/page.tsx @@ -1 +1 @@ -export { default } from "../../root/ui-kit/page"; +export { default } from "../../_admin/ui-kit/page"; diff --git a/app/layout.tsx b/app/layout.tsx index 6285085..f88ce41 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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"; diff --git a/components/root/root-dashboard-shell.tsx b/components/admin/admin-dashboard-shell.tsx similarity index 90% rename from components/root/root-dashboard-shell.tsx rename to components/admin/admin-dashboard-shell.tsx index 757ee87..c695f01 100644 --- a/components/root/root-dashboard-shell.tsx +++ b/components/admin/admin-dashboard-shell.tsx @@ -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" && diff --git a/components/root/contact-protection-form.tsx b/components/admin/contact-protection-form.tsx similarity index 100% rename from components/root/contact-protection-form.tsx rename to components/admin/contact-protection-form.tsx diff --git a/components/root/marquee-settings-form.tsx b/components/admin/marquee-settings-form.tsx similarity index 100% rename from components/root/marquee-settings-form.tsx rename to components/admin/marquee-settings-form.tsx diff --git a/components/root/media-field-picker.tsx b/components/admin/media-field-picker.tsx similarity index 100% rename from components/root/media-field-picker.tsx rename to components/admin/media-field-picker.tsx diff --git a/components/root/media-library-manager.tsx b/components/admin/media-library-manager.tsx similarity index 99% rename from components/root/media-library-manager.tsx rename to components/admin/media-library-manager.tsx index 6f5b0b1..4737685 100644 --- a/components/root/media-library-manager.tsx +++ b/components/admin/media-library-manager.tsx @@ -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", diff --git a/components/root/portfolio-categories-manager.tsx b/components/admin/portfolio-categories-manager.tsx similarity index 99% rename from components/root/portfolio-categories-manager.tsx rename to components/admin/portfolio-categories-manager.tsx index d510532..f0b6112 100644 --- a/components/root/portfolio-categories-manager.tsx +++ b/components/admin/portfolio-categories-manager.tsx @@ -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"; diff --git a/components/root/portfolio-project-actions.tsx b/components/admin/portfolio-project-actions.tsx similarity index 97% rename from components/root/portfolio-project-actions.tsx rename to components/admin/portfolio-project-actions.tsx index 3a5d9f9..d69cfd9 100644 --- a/components/root/portfolio-project-actions.tsx +++ b/components/admin/portfolio-project-actions.tsx @@ -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, diff --git a/components/root/portfolio-project-form.tsx b/components/admin/portfolio-project-form.tsx similarity index 99% rename from components/root/portfolio-project-form.tsx rename to components/admin/portfolio-project-form.tsx index 1b49eaf..51fdbf7 100644 --- a/components/root/portfolio-project-form.tsx +++ b/components/admin/portfolio-project-form.tsx @@ -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"; diff --git a/components/root/portfolio-projects-overview.tsx b/components/admin/portfolio-projects-overview.tsx similarity index 98% rename from components/root/portfolio-projects-overview.tsx rename to components/admin/portfolio-projects-overview.tsx index 96dde76..782e432 100644 --- a/components/root/portfolio-projects-overview.tsx +++ b/components/admin/portfolio-projects-overview.tsx @@ -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"; diff --git a/components/root/portfolio-subnav.tsx b/components/admin/portfolio-subnav.tsx similarity index 100% rename from components/root/portfolio-subnav.tsx rename to components/admin/portfolio-subnav.tsx diff --git a/components/root/query-toast-bridge.tsx b/components/admin/query-toast-bridge.tsx similarity index 100% rename from components/root/query-toast-bridge.tsx rename to components/admin/query-toast-bridge.tsx diff --git a/components/root/sidebar-maintenance-control.tsx b/components/admin/sidebar-maintenance-control.tsx similarity index 100% rename from components/root/sidebar-maintenance-control.tsx rename to components/admin/sidebar-maintenance-control.tsx diff --git a/components/root/site-settings-form.tsx b/components/admin/site-settings-form.tsx similarity index 100% rename from components/root/site-settings-form.tsx rename to components/admin/site-settings-form.tsx diff --git a/components/root/smtp-settings-form.tsx b/components/admin/smtp-settings-form.tsx similarity index 100% rename from components/root/smtp-settings-form.tsx rename to components/admin/smtp-settings-form.tsx diff --git a/components/root/workspace-patterns.tsx b/components/admin/workspace-patterns.tsx similarity index 100% rename from components/root/workspace-patterns.tsx rename to components/admin/workspace-patterns.tsx diff --git a/components/dashboard/dashboard-layout.tsx b/components/dashboard/dashboard-layout.tsx index 9ce5388..babb13d 100644 --- a/components/dashboard/dashboard-layout.tsx +++ b/components/dashboard/dashboard-layout.tsx @@ -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; diff --git a/components/layout/app-sidebar.tsx b/components/layout/app-sidebar.tsx index 5a33e4d..f3f8754 100644 --- a/components/layout/app-sidebar.tsx +++ b/components/layout/app-sidebar.tsx @@ -66,7 +66,7 @@ export function AppSidebar({

- Root Access + Admin Access

{title}

{description}

diff --git a/components/layout/site-footer.tsx b/components/layout/site-footer.tsx index 9699176..48cef6d 100644 --- a/components/layout/site-footer.tsx +++ b/components/layout/site-footer.tsx @@ -36,7 +36,7 @@ export function SiteFooter({ isAdmin = false }: SiteFooterProps) { ))} {isAdmin ? ( - {tNav("root")} + {tNav("admin")} ) : null} diff --git a/components/layout/site-header.tsx b/components/layout/site-header.tsx index d571a0d..d58ede6 100644 --- a/components/layout/site-header.tsx +++ b/components/layout/site-header.tsx @@ -315,7 +315,7 @@ export function SiteHeader({ /> {isAdmin ? ( @@ -465,7 +465,7 @@ export function SiteHeader({ /> {isAdmin ? ( diff --git a/docker-compose.yml b/docker-compose.yml index 17efeb0..98b87cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/lib/admin-auth.ts b/lib/admin-auth.ts index 840ac7e..0ba92e1 100644 --- a/lib/admin-auth.ts +++ b/lib/admin-auth.ts @@ -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 { sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", + domain: getAdminCookieDomain(), maxAge: 60 * 60 * 8, }); } @@ -82,6 +152,7 @@ export async function clearAdminSessionCookie(): Promise { sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", + domain: getAdminCookieDomain(), maxAge: 0, }); } diff --git a/lib/root-navigation.ts b/lib/admin-navigation.ts similarity index 94% rename from lib/root-navigation.ts rename to lib/admin-navigation.ts index f31487c..ba45c9a 100644 --- a/lib/root-navigation.ts +++ b/lib/admin-navigation.ts @@ -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, diff --git a/messages/ar.json b/messages/ar.json index bc4220b..28332c5 100644 --- a/messages/ar.json +++ b/messages/ar.json @@ -6,7 +6,7 @@ "about": "معلومات عني", "contact": "تواصل معي", "soon": "قــريــبــاً", - "root": "Root", + "admin": "Admin", "openMenu": "فتح القائمة", "closeMenu": "إغلاق القائمة", "themeToggle": "تبديل المظهر", diff --git a/messages/de.json b/messages/de.json index 46a6c2b..a2001e0 100644 --- a/messages/de.json +++ b/messages/de.json @@ -6,7 +6,7 @@ "about": "Über mich", "contact": "Kontakt", "soon": "Bald", - "root": "Root", + "admin": "Admin", "openMenu": "Menü öffnen", "closeMenu": "Menü schliessen", "themeToggle": "Theme wechseln", diff --git a/messages/en.json b/messages/en.json index 248c12e..569a78f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -6,7 +6,7 @@ "about": "About Me", "contact": "Contact", "soon": "Soon", - "root": "Root", + "admin": "Admin", "openMenu": "Open menu", "closeMenu": "Close menu", "themeToggle": "Toggle theme", diff --git a/middleware.ts b/middleware.ts index c70006d..daa4ada 100644 --- a/middleware.ts +++ b/middleware.ts @@ -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; }