This commit is contained in:
+2
-2
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+7
-7
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+5
-5
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+5
-5
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
export { metadata } from "../root/layout";
|
export { metadata } from "../_admin/layout";
|
||||||
export { default } from "../root/layout";
|
export { default } from "../_admin/layout";
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
export { default } from "../../root/marquee/page";
|
export { default } from "../../_admin/marquee/page";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
export { default } from "../../../root/portfolio/media/page";
|
export { default } from "../../../_admin/portfolio/media/page";
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
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 @@
|
|||||||
export { default } from "../../root/smtp/page";
|
export { default } from "../../_admin/smtp/page";
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default } from "../../root/ui-kit/page";
|
export { default } from "../../_admin/ui-kit/page";
|
||||||
|
|||||||
+1
-1
@@ -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";
|
||||||
|
|||||||
+9
-9
@@ -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" &&
|
||||||
+1
-1
@@ -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",
|
||||||
+1
-1
@@ -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";
|
||||||
+1
-1
@@ -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,
|
||||||
+1
-1
@@ -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";
|
||||||
+1
-1
@@ -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";
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -6,7 +6,7 @@
|
|||||||
"about": "معلومات عني",
|
"about": "معلومات عني",
|
||||||
"contact": "تواصل معي",
|
"contact": "تواصل معي",
|
||||||
"soon": "قــريــبــاً",
|
"soon": "قــريــبــاً",
|
||||||
"root": "Root",
|
"admin": "Admin",
|
||||||
"openMenu": "فتح القائمة",
|
"openMenu": "فتح القائمة",
|
||||||
"closeMenu": "إغلاق القائمة",
|
"closeMenu": "إغلاق القائمة",
|
||||||
"themeToggle": "تبديل المظهر",
|
"themeToggle": "تبديل المظهر",
|
||||||
|
|||||||
+1
-1
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user