Add dedicated local admin domain routing

This commit is contained in:
MOH
2026-03-13 17:48:45 +01:00
parent bfd7adccac
commit fef44ded80
47 changed files with 260 additions and 107 deletions
+3
View File
@@ -0,0 +1,3 @@
NEXT_PUBLIC_SITE_URL="http://mohfarawati.localhost:3000"
NEXT_PUBLIC_ADMIN_URL="http://rootmohfarawati.localhost:3000"
ADMIN_HOST="rootmohfarawati.localhost"
+2 -1
View File
@@ -25,7 +25,8 @@ ps:
docker compose ps docker compose ps
port: port:
@echo "https://mohfarawati.de" @echo "Site: $${NEXT_PUBLIC_SITE_URL:-https://mohfarawati.de}"
@echo "Admin: $${NEXT_PUBLIC_ADMIN_URL:-https://root.mohfarawati.de}"
clean-orphans: clean-orphans:
docker compose up -d --remove-orphans docker compose up -d --remove-orphans
+3 -3
View File
@@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { routing } from "@/i18n/routing"; import { routing } from "@/i18n/routing";
import { toInternalAdminPath } from "@/lib/admin-routing"; import { getAdminAppPath, toInternalAdminPath } from "@/lib/admin-routing";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getLocalizedPath } from "@/lib/locale"; import { getLocalizedPath } from "@/lib/locale";
import { setMaintenanceMode } from "@/lib/app-config"; import { setMaintenanceMode } from "@/lib/app-config";
@@ -12,7 +12,7 @@ import { setMaintenanceMode } from "@/lib/app-config";
async function ensureAdmin() { async function ensureAdmin() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
} }
@@ -20,7 +20,7 @@ export async function updateMaintenanceModeAction(formData: FormData) {
await ensureAdmin(); await ensureAdmin();
const nextValue = formData.get("enabled") === "true"; const nextValue = formData.get("enabled") === "true";
const redirectPath = String(formData.get("redirectPath") ?? "/"); const redirectPath = String(formData.get("redirectPath") ?? getAdminAppPath("/"));
const redirectUrl = new URL(redirectPath, "http://localhost"); const redirectUrl = new URL(redirectPath, "http://localhost");
redirectUrl.searchParams.set( redirectUrl.searchParams.set(
"success", "success",
+3 -2
View File
@@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { AdminDashboardShell } from "@/components/admin/admin-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 { getAdminAppPath } from "@/lib/admin-routing";
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";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -31,7 +32,7 @@ export default async function AdminMaintenancePage() {
const authenticated = await isAdminAuthenticated(); const authenticated = await isAdminAuthenticated();
if (!authenticated) { if (!authenticated) {
redirect("/"); redirect(getAdminAppPath("/"));
} }
const maintenanceEnabled = await getMaintenanceMode(); const maintenanceEnabled = await getMaintenanceMode();
@@ -39,7 +40,7 @@ export default async function AdminMaintenancePage() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
return ( return (
+4 -4
View File
@@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { isRedirectError } from "next/dist/client/components/redirect-error"; import { isRedirectError } from "next/dist/client/components/redirect-error";
import { toInternalAdminPath } from "@/lib/admin-routing"; import { getAdminAppPath, toInternalAdminPath } from "@/lib/admin-routing";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { updateMarqueeSettings } from "@/lib/app-config"; import { updateMarqueeSettings } from "@/lib/app-config";
import { routing } from "@/i18n/routing"; import { routing } from "@/i18n/routing";
@@ -13,7 +13,7 @@ import { getLocalizedPath } from "@/lib/locale";
async function ensureAdmin() { async function ensureAdmin() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
} }
@@ -72,13 +72,13 @@ export async function saveMarqueeSettingsAction(formData: FormData) {
await updateMarqueeSettings(settings); await updateMarqueeSettings(settings);
await revalidateMarqueePages(); await revalidateMarqueePages();
redirect(withMessage("/marquee", "success", "Marquee gespeichert.")); redirect(withMessage(getAdminAppPath("/marquee"), "success", "Marquee gespeichert."));
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
throw error; throw error;
} }
const message = error instanceof Error ? error.message : "Marquee konnte nicht gespeichert werden."; const message = error instanceof Error ? error.message : "Marquee konnte nicht gespeichert werden.";
redirect(withMessage("/marquee", "error", message)); redirect(withMessage(getAdminAppPath("/marquee"), "error", message));
} }
} }
+3 -2
View File
@@ -4,6 +4,7 @@ import { MarqueeSettingsForm } from "@/components/admin/marquee-settings-form";
import { AdminDashboardShell } from "@/components/admin/admin-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 { getAdminAppPath } from "@/lib/admin-routing";
import { getMarqueeSettings } from "@/lib/app-config"; import { getMarqueeSettings } from "@/lib/app-config";
import { saveMarqueeSettingsAction } from "./actions"; import { saveMarqueeSettingsAction } from "./actions";
@@ -27,14 +28,14 @@ const copy = {
export default async function AdminMarqueePage() { export default async function AdminMarqueePage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect(getAdminAppPath("/"));
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
const marqueeSettings = await getMarqueeSettings(); const marqueeSettings = await getMarqueeSettings();
+8 -8
View File
@@ -5,7 +5,7 @@ import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { isRedirectError } from "next/dist/client/components/redirect-error"; import { isRedirectError } from "next/dist/client/components/redirect-error";
import { toInternalAdminPath } from "@/lib/admin-routing"; import { getAdminAppPath, toInternalAdminPath } from "@/lib/admin-routing";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { countMediaUsageReferences, getMediaAssetById } from "@/lib/media"; import { countMediaUsageReferences, getMediaAssetById } from "@/lib/media";
import { createStandaloneMediaAsset, deleteMediaAssetAndFile } from "@/lib/media-service"; import { createStandaloneMediaAsset, deleteMediaAssetAndFile } from "@/lib/media-service";
@@ -15,7 +15,7 @@ import { prisma } from "@/lib/prisma";
async function ensureAdmin() { async function ensureAdmin() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
} }
@@ -47,14 +47,14 @@ export async function createMediaAssetAction(formData: FormData) {
}); });
revalidateMediaPages(); revalidateMediaPages();
redirect(withMessage("/media", "success", "Datei gespeichert.")); redirect(withMessage(getAdminAppPath("/media"), "success", "Datei gespeichert."));
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
throw error; throw error;
} }
const message = error instanceof Error ? error.message : "Datei konnte nicht gespeichert werden."; const message = error instanceof Error ? error.message : "Datei konnte nicht gespeichert werden.";
redirect(withMessage("/media", "error", message)); redirect(withMessage(getAdminAppPath("/media"), "error", message));
} }
} }
@@ -67,13 +67,13 @@ export async function deleteMediaAssetAction(formData: FormData) {
const asset = await getMediaAssetById(assetId); const asset = await getMediaAssetById(assetId);
if (!asset) { if (!asset) {
redirect(withMessage("/media", "error", "Datei nicht gefunden.")); redirect(withMessage(getAdminAppPath("/media"), "error", "Datei nicht gefunden."));
} }
const usageCount = await countMediaUsageReferences(asset.id); const usageCount = await countMediaUsageReferences(asset.id);
if (usageCount > 0) { if (usageCount > 0) {
redirect(withMessage("/media", "error", "Datei wird noch verwendet.")); redirect(withMessage(getAdminAppPath("/media"), "error", "Datei wird noch verwendet."));
} }
await prisma.mediaAsset.delete({ await prisma.mediaAsset.delete({
@@ -90,13 +90,13 @@ export async function deleteMediaAssetAction(formData: FormData) {
} }
revalidateMediaPages(); revalidateMediaPages();
redirect(withMessage("/media", "success", "Datei geloescht.")); redirect(withMessage(getAdminAppPath("/media"), "success", "Datei geloescht."));
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
throw error; throw error;
} }
const message = error instanceof Error ? error.message : "Datei konnte nicht geloescht werden."; const message = error instanceof Error ? error.message : "Datei konnte nicht geloescht werden.";
redirect(withMessage("/media", "error", message)); redirect(withMessage(getAdminAppPath("/media"), "error", message));
} }
} }
+3 -2
View File
@@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
import { MediaLibraryManager } from "@/components/admin/media-library-manager"; import { MediaLibraryManager } from "@/components/admin/media-library-manager";
import { AdminDashboardShell } from "@/components/admin/admin-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 { getAdminAppPath } from "@/lib/admin-routing";
import { getAdminMediaAssets } from "@/lib/media"; import { getAdminMediaAssets } from "@/lib/media";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -22,14 +23,14 @@ const copy = {
export default async function AdminMediaPage() { export default async function AdminMediaPage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect(getAdminAppPath("/"));
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
const mediaAssets = await getAdminMediaAssets(); const mediaAssets = await getAdminMediaAssets();
+6 -5
View File
@@ -20,6 +20,7 @@ import {
resetAdminFailedAttempts, resetAdminFailedAttempts,
setAdminSessionCookie, setAdminSessionCookie,
} from "@/lib/admin-auth"; } from "@/lib/admin-auth";
import { getAdminAppPath } from "@/lib/admin-routing";
import { getMaintenanceMode } from "@/lib/app-config"; 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";
@@ -76,28 +77,28 @@ export default async function AdminPage({ searchParams }: AdminPageProps) {
const currentLockState = await getAdminLockState(); const currentLockState = await getAdminLockState();
if (currentLockState.locked) { if (currentLockState.locked) {
redirect("/?error=locked"); redirect(`${getAdminAppPath("/")}?error=locked`);
} }
if (!isAdminAuthConfigured() || !isPasswordValid(password)) { if (!isAdminAuthConfigured() || !isPasswordValid(password)) {
const failState = await registerFailedAdminAttempt(); const failState = await registerFailedAdminAttempt();
if (failState.locked) { if (failState.locked) {
redirect("/?error=locked"); redirect(`${getAdminAppPath("/")}?error=locked`);
} }
redirect("/?error=invalid"); redirect(`${getAdminAppPath("/")}?error=invalid`);
} }
await resetAdminFailedAttempts(); await resetAdminFailedAttempts();
await setAdminSessionCookie(); await setAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
if (!authenticated) { if (!authenticated) {
+10 -10
View File
@@ -7,7 +7,7 @@ import { isRedirectError } from "next/dist/client/components/redirect-error";
import { ZodError } from "zod"; import { ZodError } from "zod";
import { routing } from "@/i18n/routing"; import { routing } from "@/i18n/routing";
import { toInternalAdminPath } from "@/lib/admin-routing"; import { getAdminAppPath, toInternalAdminPath } from "@/lib/admin-routing";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { deleteEntityMediaUsages, replaceEntityMediaUsages } from "@/lib/media"; import { deleteEntityMediaUsages, replaceEntityMediaUsages } from "@/lib/media";
import { resolveMediaSelection } from "@/lib/media-service"; import { resolveMediaSelection } from "@/lib/media-service";
@@ -26,7 +26,7 @@ import {
async function ensureAdmin() { async function ensureAdmin() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
} }
@@ -107,7 +107,7 @@ async function removeManagedPaths(paths: string[]) {
export async function upsertCategoryAction(formData: FormData) { export async function upsertCategoryAction(formData: FormData) {
await ensureAdmin(); await ensureAdmin();
const redirectPath = getRedirectPath(formData, "/portfolio/categories"); const redirectPath = getRedirectPath(formData, getAdminAppPath("/portfolio/categories"));
try { try {
const parsed = categoryInputSchema.parse({ const parsed = categoryInputSchema.parse({
@@ -157,7 +157,7 @@ export async function upsertCategoryAction(formData: FormData) {
export async function deleteCategoryAction(formData: FormData) { export async function deleteCategoryAction(formData: FormData) {
await ensureAdmin(); await ensureAdmin();
const redirectPath = getRedirectPath(formData, "/portfolio/categories"); const redirectPath = getRedirectPath(formData, getAdminAppPath("/portfolio/categories"));
const id = String(formData.get("id") ?? ""); const id = String(formData.get("id") ?? "");
try { try {
@@ -192,8 +192,8 @@ export async function saveProjectAction(formData: FormData) {
await ensureAdmin(); await ensureAdmin();
const fallbackRedirect = String(formData.get("id") ?? "").trim() const fallbackRedirect = String(formData.get("id") ?? "").trim()
? `/portfolio/projects/${String(formData.get("id") ?? "").trim()}` ? getAdminAppPath(`/portfolio/projects/${String(formData.get("id") ?? "").trim()}`)
: "/portfolio/projects/new"; : getAdminAppPath("/portfolio/projects/new");
const redirectPath = getRedirectPath(formData, fallbackRedirect); const redirectPath = getRedirectPath(formData, fallbackRedirect);
const uploadedPaths: string[] = []; const uploadedPaths: string[] = [];
const createdMediaAssetIds: string[] = []; const createdMediaAssetIds: string[] = [];
@@ -526,7 +526,7 @@ export async function saveProjectAction(formData: FormData) {
} }
redirect( redirect(
withMessage(`/portfolio/projects/${projectResult.project.id}`, "success", "Projekt gespeichert."), withMessage(getAdminAppPath(`/portfolio/projects/${projectResult.project.id}`), "success", "Projekt gespeichert."),
); );
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
@@ -579,7 +579,7 @@ export async function deleteProjectAction(formData: FormData) {
}); });
if (!project) { if (!project) {
redirect(withMessage("/portfolio", "error", "Project not found.")); redirect(withMessage(getAdminAppPath("/portfolio"), "error", "Project not found."));
} }
await prisma.portfolioProject.delete({ await prisma.portfolioProject.delete({
@@ -596,12 +596,12 @@ export async function deleteProjectAction(formData: FormData) {
revalidatePath(getLocalizedPath(locale, `/portfolio/${project.slug}`)); revalidatePath(getLocalizedPath(locale, `/portfolio/${project.slug}`));
} }
redirect(withMessage("/portfolio", "success", "Project deleted.")); redirect(withMessage(getAdminAppPath("/portfolio"), "success", "Project deleted."));
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
throw error; throw error;
} }
redirect(withMessage("/portfolio", "error", "Unable to delete project.")); redirect(withMessage(getAdminAppPath("/portfolio"), "error", "Unable to delete project."));
} }
} }
+3 -2
View File
@@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
import { PortfolioCategoriesManager } from "@/components/admin/portfolio-categories-manager"; import { PortfolioCategoriesManager } from "@/components/admin/portfolio-categories-manager";
import { AdminDashboardShell } from "@/components/admin/admin-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 { getAdminAppPath } from "@/lib/admin-routing";
import { getAdminPortfolioCategories } from "@/lib/portfolio"; import { getAdminPortfolioCategories } from "@/lib/portfolio";
import { deleteCategoryAction, upsertCategoryAction } from "../actions"; import { deleteCategoryAction, upsertCategoryAction } from "../actions";
@@ -24,14 +25,14 @@ const copy = {
export default async function AdminPortfolioCategoriesPage() { export default async function AdminPortfolioCategoriesPage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect(getAdminAppPath("/"));
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
const categories = await getAdminPortfolioCategories(); const categories = await getAdminPortfolioCategories();
+2 -1
View File
@@ -1,5 +1,6 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getAdminAppPath } from "@/lib/admin-routing";
export default function AdminPortfolioMediaRedirectPage() { export default function AdminPortfolioMediaRedirectPage() {
redirect("/media"); redirect(getAdminAppPath("/media"));
} }
+3 -2
View File
@@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { PortfolioProjectsOverview } from "@/components/admin/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 { getAdminAppPath } from "@/lib/admin-routing";
import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio"; import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -33,14 +34,14 @@ export default async function AdminPortfolioPage({ searchParams }: AdminPortfoli
const resolvedSearchParams = await searchParams; const resolvedSearchParams = await searchParams;
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect(getAdminAppPath("/"));
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
const selectedCategory = resolvedSearchParams?.category && resolvedSearchParams.category !== "__all__" const selectedCategory = resolvedSearchParams?.category && resolvedSearchParams.category !== "__all__"
+5 -4
View File
@@ -16,6 +16,7 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminAppPath } from "@/lib/admin-routing";
import { getMediaOptions } from "@/lib/media"; import { getMediaOptions } from "@/lib/media";
import { import {
getActivePortfolioCategories, getActivePortfolioCategories,
@@ -53,14 +54,14 @@ export default async function AdminPortfolioProjectPage({
params, params,
}: AdminPortfolioProjectPageProps) { }: AdminPortfolioProjectPageProps) {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect(getAdminAppPath("/"));
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
const { id } = await params; const { id } = await params;
@@ -72,7 +73,7 @@ export default async function AdminPortfolioProjectPage({
]); ]);
if (!project) { if (!project) {
redirect("/portfolio?error=Project+not+found."); redirect(`${getAdminAppPath("/portfolio")}?error=Project+not+found.`);
} }
return ( return (
@@ -92,7 +93,7 @@ export default async function AdminPortfolioProjectPage({
mediaOptions={mediaOptions} mediaOptions={mediaOptions}
project={project} project={project}
formId="portfolio-project-form" formId="portfolio-project-form"
redirectPath={`/portfolio/projects/${project.id}`} redirectPath={getAdminAppPath(`/portfolio/projects/${project.id}`)}
/> />
</MotionFade> </MotionFade>
+4 -3
View File
@@ -4,6 +4,7 @@ import { MotionFade } from "@/components/motion-fade";
import { PortfolioProjectForm } from "@/components/admin/portfolio-project-form"; import { PortfolioProjectForm } from "@/components/admin/portfolio-project-form";
import { AdminDashboardShell } from "@/components/admin/admin-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 { getAdminAppPath } from "@/lib/admin-routing";
import { getMediaOptions } from "@/lib/media"; import { getMediaOptions } from "@/lib/media";
import { getActivePortfolioCategories } from "@/lib/portfolio"; import { getActivePortfolioCategories } from "@/lib/portfolio";
@@ -26,14 +27,14 @@ const copy = {
export default async function AdminNewPortfolioProjectPage() { export default async function AdminNewPortfolioProjectPage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect(getAdminAppPath("/"));
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
const [categories, mediaOptions] = await Promise.all([ const [categories, mediaOptions] = await Promise.all([
@@ -57,7 +58,7 @@ export default async function AdminNewPortfolioProjectPage() {
categories={categories} categories={categories}
mediaOptions={mediaOptions} mediaOptions={mediaOptions}
formId="portfolio-project-form" formId="portfolio-project-form"
redirectPath="/portfolio/projects/new" redirectPath={getAdminAppPath("/portfolio/projects/new")}
/> />
</MotionFade> </MotionFade>
</div> </div>
+3 -2
View File
@@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
import { PortfolioProjectsOverview } from "@/components/admin/portfolio-projects-overview"; import { PortfolioProjectsOverview } from "@/components/admin/portfolio-projects-overview";
import { AdminDashboardShell } from "@/components/admin/admin-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 { getAdminAppPath } from "@/lib/admin-routing";
import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio"; import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -33,14 +34,14 @@ export default async function AdminPortfolioProjectsPage({
const resolvedSearchParams = await searchParams; const resolvedSearchParams = await searchParams;
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect(getAdminAppPath("/"));
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
const selectedCategory = resolvedSearchParams?.category && resolvedSearchParams.category !== "__all__" const selectedCategory = resolvedSearchParams?.category && resolvedSearchParams.category !== "__all__"
+4 -4
View File
@@ -14,7 +14,7 @@ import {
SITE_SETTINGS_LOGO_LIGHT_FIELD_KEY, SITE_SETTINGS_LOGO_LIGHT_FIELD_KEY,
updateSiteSettings, updateSiteSettings,
} from "@/lib/app-config"; } from "@/lib/app-config";
import { toInternalAdminPath } from "@/lib/admin-routing"; import { getAdminAppPath, toInternalAdminPath } from "@/lib/admin-routing";
import { PAGE_TITLE_TOKEN, type SiteSettings } from "@/lib/site-settings"; import { PAGE_TITLE_TOKEN, type SiteSettings } from "@/lib/site-settings";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { replaceEntityMediaUsages } from "@/lib/media"; import { replaceEntityMediaUsages } from "@/lib/media";
@@ -28,7 +28,7 @@ import { prisma } from "@/lib/prisma";
async function ensureAdmin() { async function ensureAdmin() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
} }
@@ -278,7 +278,7 @@ export async function saveSiteSettingsAction(formData: FormData) {
}); });
await revalidateSiteSettingsPages(); await revalidateSiteSettingsPages();
redirect(withMessage("/site-settings", "success", "Einstellungen gespeichert.")); redirect(withMessage(getAdminAppPath("/site-settings"), "success", "Einstellungen gespeichert."));
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
throw error; throw error;
@@ -291,6 +291,6 @@ export async function saveSiteSettingsAction(formData: FormData) {
? error.message ? error.message
: "Einstellungen konnten nicht gespeichert werden."; : "Einstellungen konnten nicht gespeichert werden.";
redirect(withMessage("/site-settings", "error", message)); redirect(withMessage(getAdminAppPath("/site-settings"), "error", message));
} }
} }
+3 -2
View File
@@ -5,6 +5,7 @@ import { MotionFade } from "@/components/motion-fade";
import { SiteSettingsForm } from "@/components/admin/site-settings-form"; import { SiteSettingsForm } from "@/components/admin/site-settings-form";
import { AdminDashboardShell } from "@/components/admin/admin-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 { getAdminAppPath } from "@/lib/admin-routing";
import { import {
getSiteSettings, getSiteSettings,
getSiteSettingsMediaBindings, getSiteSettingsMediaBindings,
@@ -31,14 +32,14 @@ const copy = {
export default async function AdminSiteSettingsPage() { export default async function AdminSiteSettingsPage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect(getAdminAppPath("/"));
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
const [siteSettings, mediaBindings, mediaOptions] = await Promise.all([ const [siteSettings, mediaBindings, mediaOptions] = await Promise.all([
+6 -6
View File
@@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { isRedirectError } from "next/dist/client/components/redirect-error"; import { isRedirectError } from "next/dist/client/components/redirect-error";
import { toInternalAdminPath } from "@/lib/admin-routing"; import { getAdminAppPath, toInternalAdminPath } from "@/lib/admin-routing";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { isCheckedFormValue } from "@/lib/form-data"; import { isCheckedFormValue } from "@/lib/form-data";
import { import {
@@ -17,7 +17,7 @@ import type { MailSettings } from "@/lib/mail-settings";
async function ensureAdmin() { async function ensureAdmin() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
} }
@@ -79,7 +79,7 @@ export async function saveMailSettingsAction(formData: FormData) {
await updateMailSettings(nextMailSettings); await updateMailSettings(nextMailSettings);
revalidatePath(toInternalAdminPath("/smtp")); revalidatePath(toInternalAdminPath("/smtp"));
redirect(withMessage("/smtp", "success", "SMTP Einstellungen gespeichert.")); redirect(withMessage(getAdminAppPath("/smtp"), "success", "SMTP Einstellungen gespeichert."));
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
throw error; throw error;
@@ -90,7 +90,7 @@ export async function saveMailSettingsAction(formData: FormData) {
? error.message ? error.message
: "SMTP Einstellungen konnten nicht gespeichert werden."; : "SMTP Einstellungen konnten nicht gespeichert werden.";
redirect(withMessage("/smtp", "error", message)); redirect(withMessage(getAdminAppPath("/smtp"), "error", message));
} }
} }
@@ -99,7 +99,7 @@ export async function sendTestEmailAction() {
try { try {
await sendTestEmail(); await sendTestEmail();
redirect(withMessage("/smtp", "success", "Test-E-Mail gesendet.")); redirect(withMessage(getAdminAppPath("/smtp"), "success", "Test-E-Mail gesendet."));
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
throw error; throw error;
@@ -110,6 +110,6 @@ export async function sendTestEmailAction() {
? error.message ? error.message
: "Test-E-Mail konnte nicht gesendet werden."; : "Test-E-Mail konnte nicht gesendet werden.";
redirect(withMessage("/smtp", "error", message)); redirect(withMessage(getAdminAppPath("/smtp"), "error", message));
} }
} }
@@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { isRedirectError } from "next/dist/client/components/redirect-error"; import { isRedirectError } from "next/dist/client/components/redirect-error";
import { toInternalAdminPath } from "@/lib/admin-routing"; import { getAdminAppPath, toInternalAdminPath } from "@/lib/admin-routing";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { isCheckedFormValue } from "@/lib/form-data"; import { isCheckedFormValue } from "@/lib/form-data";
import { import {
@@ -16,7 +16,7 @@ import type { ContactProtectionSettings } from "@/lib/contact-protection";
async function ensureAdmin() { async function ensureAdmin() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
} }
@@ -92,7 +92,7 @@ export async function saveContactProtectionSettingsAction(formData: FormData) {
revalidatePath("/en/contact"); revalidatePath("/en/contact");
redirect( redirect(
withMessage( withMessage(
"/smtp/contact-protection", getAdminAppPath("/smtp/contact-protection"),
"success", "success",
"Contact Protection gespeichert.", "Contact Protection gespeichert.",
), ),
@@ -107,6 +107,6 @@ export async function saveContactProtectionSettingsAction(formData: FormData) {
? error.message ? error.message
: "Contact Protection konnte nicht gespeichert werden."; : "Contact Protection konnte nicht gespeichert werden.";
redirect(withMessage("/smtp/contact-protection", "error", message)); redirect(withMessage(getAdminAppPath("/smtp/contact-protection"), "error", message));
} }
} }
+3 -2
View File
@@ -4,6 +4,7 @@ import { MotionFade } from "@/components/motion-fade";
import { ContactProtectionForm } from "@/components/admin/contact-protection-form"; import { ContactProtectionForm } from "@/components/admin/contact-protection-form";
import { AdminDashboardShell } from "@/components/admin/admin-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 { getAdminAppPath } from "@/lib/admin-routing";
import { getContactProtectionFormValues } from "@/lib/app-config"; import { getContactProtectionFormValues } from "@/lib/app-config";
import { saveContactProtectionSettingsAction } from "./actions"; import { saveContactProtectionSettingsAction } from "./actions";
@@ -27,14 +28,14 @@ const copy = {
export default async function AdminSMTPProtectionPage() { export default async function AdminSMTPProtectionPage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect(getAdminAppPath("/"));
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
const settings = await getContactProtectionFormValues(); const settings = await getContactProtectionFormValues();
+3 -2
View File
@@ -5,6 +5,7 @@ import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { SMTPSettingsForm } from "@/components/admin/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 { getAdminAppPath } from "@/lib/admin-routing";
import { getMailSettingsFormValues } from "@/lib/app-config"; import { getMailSettingsFormValues } from "@/lib/app-config";
import { saveMailSettingsAction, sendTestEmailAction } from "./actions"; import { saveMailSettingsAction, sendTestEmailAction } from "./actions";
@@ -28,14 +29,14 @@ const copy = {
export default async function AdminSMTPPage() { export default async function AdminSMTPPage() {
if (!(await isAdminAuthenticated())) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect(getAdminAppPath("/"));
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
const mailSettings = await getMailSettingsFormValues(); const mailSettings = await getMailSettingsFormValues();
+3 -2
View File
@@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { AdminDashboardShell } from "@/components/admin/admin-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 { getAdminAppPath } from "@/lib/admin-routing";
import { UiKitShowcase } from "@/components/ui/ui-kit-showcase"; import { UiKitShowcase } from "@/components/ui/ui-kit-showcase";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -24,14 +25,14 @@ export default async function AdminUiKitPage() {
const authenticated = await isAdminAuthenticated(); const authenticated = await isAdminAuthenticated();
if (!authenticated) { if (!authenticated) {
redirect("/"); redirect(getAdminAppPath("/"));
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
await clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect(getAdminAppPath("/"));
} }
return ( return (
+18 -1
View File
@@ -1,2 +1,19 @@
export { metadata } from "../_admin/layout"; import type { Metadata } from "next";
export const metadata: Metadata = {
robots: {
index: false,
follow: false,
nocache: true,
googleBot: {
index: false,
follow: false,
noimageindex: true,
"max-image-preview": "none",
"max-snippet": -1,
"max-video-preview": -1,
},
},
};
export { default } from "../_admin/layout"; export { default } from "../_admin/layout";
+1 -1
View File
@@ -1,2 +1,2 @@
export { dynamic } from "../../_admin/maintenance/page"; export const dynamic = "force-dynamic";
export { default } from "../../_admin/maintenance/page"; export { default } from "../../_admin/maintenance/page";
+1 -1
View File
@@ -1,2 +1,2 @@
export { dynamic } from "../../_admin/media/page"; export const dynamic = "force-dynamic";
export { default } from "../../_admin/media/page"; export { default } from "../../_admin/media/page";
+1 -1
View File
@@ -1,2 +1,2 @@
export { dynamic } from "../_admin/page"; export const dynamic = "force-dynamic";
export { default } from "../_admin/page"; export { default } from "../_admin/page";
+19
View File
@@ -0,0 +1,19 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
robots: {
index: false,
follow: false,
nocache: true,
googleBot: {
index: false,
follow: false,
noimageindex: true,
"max-image-preview": "none",
"max-snippet": -1,
"max-video-preview": -1,
},
},
};
export { default } from "../_admin/layout";
+2
View File
@@ -0,0 +1,2 @@
export const dynamic = "force-dynamic";
export { default } from "../../_admin/maintenance/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../_admin/marquee/page";
+2
View File
@@ -0,0 +1,2 @@
export const dynamic = "force-dynamic";
export { default } from "../../_admin/media/page";
+2
View File
@@ -0,0 +1,2 @@
export const dynamic = "force-dynamic";
export { default } from "../_admin/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../../_admin/portfolio/categories/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../../_admin/portfolio/media/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../_admin/portfolio/page";
@@ -0,0 +1 @@
export { default } from "../../../../_admin/portfolio/projects/[id]/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../../../_admin/portfolio/projects/new/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../../_admin/portfolio/projects/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../_admin/site-settings/page";
@@ -0,0 +1 @@
export { default } from "../../../_admin/smtp/contact-protection/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../_admin/smtp/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../_admin/ui-kit/page";
+7 -7
View File
@@ -19,7 +19,7 @@ import { SidebarMaintenanceControl } from "@/components/admin/sidebar-maintenanc
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, getAdminAppPath } from "@/lib/admin-routing";
import { getMaintenanceMode, getSiteSettingsMediaBindings } from "@/lib/app-config"; import { getMaintenanceMode, getSiteSettingsMediaBindings } from "@/lib/app-config";
import { getAdminNavigation } from "@/lib/admin-navigation"; import { getAdminNavigation } from "@/lib/admin-navigation";
@@ -75,12 +75,12 @@ export async function AdminDashboardShell({
const sidebarItems = getAdminNavigation(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 !== getAdminAppPath("/maintenance") &&
item.href !== "/ui-kit" && item.href !== getAdminAppPath("/ui-kit") &&
item.href !== "/smtp" && item.href !== getAdminAppPath("/smtp") &&
item.href !== "/marquee", item.href !== getAdminAppPath("/marquee"),
); );
const footerSidebarItems = ["/marquee", "/smtp"] const footerSidebarItems = [getAdminAppPath("/marquee"), getAdminAppPath("/smtp")]
.map((href) => sidebarItems.find((item) => item.href === href)) .map((href) => sidebarItems.find((item) => item.href === href))
.filter((item): item is NonNullable<typeof item> => Boolean(item)); .filter((item): item is NonNullable<typeof item> => Boolean(item));
const headerIcon = const headerIcon =
@@ -126,7 +126,7 @@ export async function AdminDashboardShell({
variant={active === "ui-kit" ? "default" : "outline"} variant={active === "ui-kit" ? "default" : "outline"}
className="w-full justify-between" className="w-full justify-between"
> >
<Link href="/ui-kit"> <Link href={getAdminAppPath("/ui-kit")}>
{copy.uiKit} {copy.uiKit}
<SwatchBook className="h-4 w-4" /> <SwatchBook className="h-4 w-4" />
</Link> </Link>
@@ -8,6 +8,7 @@ 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";
import { CardContent } from "@/components/ui/card"; import { CardContent } from "@/components/ui/card";
import { getAdminAppPath } from "@/lib/admin-routing";
import { getLocalizedPath } from "@/lib/locale"; import { getLocalizedPath } from "@/lib/locale";
import { getLocalizedValue, type PortfolioCategoryView, type PortfolioProjectView } from "@/lib/portfolio"; import { getLocalizedValue, type PortfolioCategoryView, type PortfolioProjectView } from "@/lib/portfolio";
@@ -46,13 +47,13 @@ export function PortfolioProjectsOverview({
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button asChild> <Button asChild>
<Link href="/portfolio/projects/new"> <Link href={getAdminAppPath("/portfolio/projects/new")}>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
{copy.newProject} {copy.newProject}
</Link> </Link>
</Button> </Button>
<Button asChild variant="outline"> <Button asChild variant="outline">
<Link href="/portfolio/categories"> <Link href={getAdminAppPath("/portfolio/categories")}>
<Tags className="h-4 w-4" /> <Tags className="h-4 w-4" />
{copy.newCategory} {copy.newCategory}
</Link> </Link>
@@ -62,7 +63,7 @@ export function PortfolioProjectsOverview({
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button asChild variant={selectedCategory === "" ? "default" : "outline"}> <Button asChild variant={selectedCategory === "" ? "default" : "outline"}>
<Link href="/portfolio">{copy.all}</Link> <Link href={getAdminAppPath("/portfolio")}>{copy.all}</Link>
</Button> </Button>
{categories.map((category) => ( {categories.map((category) => (
<Button <Button
@@ -70,7 +71,7 @@ export function PortfolioProjectsOverview({
asChild asChild
variant={selectedCategory === category.id ? "default" : "outline"} variant={selectedCategory === category.id ? "default" : "outline"}
> >
<Link href={`/portfolio?category=${category.id}`}> <Link href={`${getAdminAppPath("/portfolio")}?category=${category.id}`}>
{category.name.de || category.name.en || category.name.ar} {category.name.de || category.name.en || category.name.ar}
</Link> </Link>
</Button> </Button>
+14 -13
View File
@@ -11,6 +11,7 @@ import {
Type, Type,
type LucideIcon, type LucideIcon,
} from "lucide-react"; } from "lucide-react";
import { getAdminAppPath } from "@/lib/admin-routing";
type AdminNavigationCopy = { type AdminNavigationCopy = {
overview: string; overview: string;
@@ -42,56 +43,56 @@ export function getAdminNavigation(
return [ return [
{ {
label: copy.overview, label: copy.overview,
href: "/", href: getAdminAppPath("/"),
icon: LayoutDashboard, icon: LayoutDashboard,
active: active === "overview", active: active === "overview",
}, },
{ {
label: copy.maintenance, label: copy.maintenance,
href: "/maintenance", href: getAdminAppPath("/maintenance"),
icon: ShieldAlert, icon: ShieldAlert,
active: active === "maintenance", active: active === "maintenance",
}, },
{ {
label: copy.uiKit, label: copy.uiKit,
href: "/ui-kit", href: getAdminAppPath("/ui-kit"),
icon: SwatchBook, icon: SwatchBook,
active: active === "ui-kit", active: active === "ui-kit",
}, },
{ {
label: copy.media, label: copy.media,
href: "/media", href: getAdminAppPath("/media"),
icon: ImageIcon, icon: ImageIcon,
active: active === "media", active: active === "media",
}, },
{ {
label: copy.siteSettings, label: copy.siteSettings,
href: "/site-settings", href: getAdminAppPath("/site-settings"),
icon: Globe2, icon: Globe2,
active: active === "site-settings", active: active === "site-settings",
}, },
{ {
label: copy.marquee ?? "Marquee", label: copy.marquee ?? "Marquee",
href: "/marquee", href: getAdminAppPath("/marquee"),
icon: Type, icon: Type,
active: active === "marquee", active: active === "marquee",
}, },
{ {
label: copy.smtp ?? "SMTP", label: copy.smtp ?? "SMTP",
href: "/smtp", href: getAdminAppPath("/smtp"),
icon: Mail, icon: Mail,
active: active === "smtp" && !smtpChild, active: active === "smtp" && !smtpChild,
expanded: active === "smtp", expanded: active === "smtp",
children: [ children: [
{ {
label: copy.smtp ?? "SMTP", label: copy.smtp ?? "SMTP",
href: "/smtp", href: getAdminAppPath("/smtp"),
icon: Mail, icon: Mail,
active: smtpChild === "settings" || (!smtpChild && active === "smtp"), active: smtpChild === "settings" || (!smtpChild && active === "smtp"),
}, },
{ {
label: copy.contactProtection ?? "Contact Protection", label: copy.contactProtection ?? "Contact Protection",
href: "/smtp/contact-protection", href: getAdminAppPath("/smtp/contact-protection"),
icon: ShieldAlert, icon: ShieldAlert,
active: smtpChild === "contact-protection", active: smtpChild === "contact-protection",
}, },
@@ -99,26 +100,26 @@ export function getAdminNavigation(
}, },
{ {
label: copy.portfolio, label: copy.portfolio,
href: "/portfolio", href: getAdminAppPath("/portfolio"),
icon: FolderKanban, icon: FolderKanban,
active: active === "portfolio" && !portfolioChild, active: active === "portfolio" && !portfolioChild,
expanded: active === "portfolio", expanded: active === "portfolio",
children: [ children: [
{ {
label: "Projects", label: "Projects",
href: "/portfolio", href: getAdminAppPath("/portfolio"),
icon: FolderKanban, icon: FolderKanban,
active: portfolioChild === "overview" || portfolioChild === "projects", active: portfolioChild === "overview" || portfolioChild === "projects",
}, },
{ {
label: "Add Project", label: "Add Project",
href: "/portfolio/projects/new", href: getAdminAppPath("/portfolio/projects/new"),
icon: PlusSquare, icon: PlusSquare,
active: portfolioChild === "new-project", active: portfolioChild === "new-project",
}, },
{ {
label: "Add Category", label: "Add Category",
href: "/portfolio/categories", href: getAdminAppPath("/portfolio/categories"),
icon: Tags, icon: Tags,
active: portfolioChild === "categories", active: portfolioChild === "categories",
}, },
+56 -1
View File
@@ -1,6 +1,7 @@
const DEFAULT_ADMIN_HOST = "root.mohfarawati.de"; const DEFAULT_ADMIN_HOST = "root.mohfarawati.de";
const DEFAULT_ADMIN_URL = `https://${DEFAULT_ADMIN_HOST}`; const DEFAULT_ADMIN_URL = `https://${DEFAULT_ADMIN_HOST}`;
const DEFAULT_SITE_URL = "https://mohfarawati.de"; const DEFAULT_SITE_URL = "https://mohfarawati.de";
const DEV_ADMIN_PREFIX = "/root";
export const INTERNAL_ADMIN_PREFIX = "/admin-internal"; export const INTERNAL_ADMIN_PREFIX = "/admin-internal";
@@ -16,10 +17,32 @@ function normalizeBaseUrl(url: string): string {
return url.replace(/\/+$/, ""); return url.replace(/\/+$/, "");
} }
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;
}
}
export function getAdminHost(): string { export function getAdminHost(): string {
return (process.env.ADMIN_HOST ?? DEFAULT_ADMIN_HOST).trim().toLowerCase(); return (process.env.ADMIN_HOST ?? DEFAULT_ADMIN_HOST).trim().toLowerCase();
} }
export function getSiteHost(): string {
return parseHostname(process.env.NEXT_PUBLIC_SITE_URL ?? DEFAULT_SITE_URL) ?? "localhost";
}
export function getRequestHostname(hostHeader?: string | null): string { export function getRequestHostname(hostHeader?: string | null): string {
return (hostHeader ?? "") return (hostHeader ?? "")
.split(",")[0] .split(",")[0]
@@ -32,10 +55,18 @@ export function isAdminHost(hostname: string): boolean {
return hostname === getAdminHost(); return hostname === getAdminHost();
} }
export function hasDedicatedAdminHost(): boolean {
return getAdminHost() !== getSiteHost();
}
export function isLegacyAdminPath(pathname: string): boolean { export function isLegacyAdminPath(pathname: string): boolean {
return pathname === "/root" || pathname.startsWith("/root/"); return pathname === "/root" || pathname.startsWith("/root/");
} }
export function isDevelopmentAdminPath(pathname: string): boolean {
return pathname === DEV_ADMIN_PREFIX || pathname.startsWith(`${DEV_ADMIN_PREFIX}/`);
}
export function isInternalAdminPath(pathname: string): boolean { export function isInternalAdminPath(pathname: string): boolean {
return pathname === INTERNAL_ADMIN_PREFIX || pathname.startsWith(`${INTERNAL_ADMIN_PREFIX}/`); return pathname === INTERNAL_ADMIN_PREFIX || pathname.startsWith(`${INTERNAL_ADMIN_PREFIX}/`);
} }
@@ -50,12 +81,36 @@ export function toInternalAdminPath(pathname: string): string {
return `${INTERNAL_ADMIN_PREFIX}${normalizedPathname}`; return `${INTERNAL_ADMIN_PREFIX}${normalizedPathname}`;
} }
export function fromDevelopmentAdminPath(pathname: string): string {
if (pathname === DEV_ADMIN_PREFIX) {
return "/";
}
if (pathname.startsWith(`${DEV_ADMIN_PREFIX}/`)) {
return pathname.slice(DEV_ADMIN_PREFIX.length);
}
return pathname;
}
export function getAdminBaseUrl(): string { export function getAdminBaseUrl(): string {
return normalizeBaseUrl(process.env.NEXT_PUBLIC_ADMIN_URL ?? DEFAULT_ADMIN_URL); return normalizeBaseUrl(process.env.NEXT_PUBLIC_ADMIN_URL ?? DEFAULT_ADMIN_URL);
} }
export function getAdminAppPath(pathname = "/"): string {
const normalizedPathname = normalizePathname(pathname);
if (process.env.NODE_ENV !== "production" && !hasDedicatedAdminHost()) {
return normalizedPathname === "/"
? DEV_ADMIN_PREFIX
: `${DEV_ADMIN_PREFIX}${normalizedPathname}`;
}
return normalizedPathname;
}
export function buildAdminUrl(pathname = "/"): string { export function buildAdminUrl(pathname = "/"): string {
return `${getAdminBaseUrl()}${normalizePathname(pathname)}`; return `${getAdminBaseUrl()}${getAdminAppPath(pathname)}`;
} }
export function getSiteBaseUrl(): string { export function getSiteBaseUrl(): string {
+30 -5
View File
@@ -4,8 +4,12 @@ import type { NextRequest } from "next/server";
import { routing } from "./i18n/routing"; import { routing } from "./i18n/routing";
import { import {
fromDevelopmentAdminPath,
getAdminBaseUrl,
getRequestHostname, getRequestHostname,
isDevelopmentAdminPath,
isAdminHost, isAdminHost,
hasDedicatedAdminHost,
isInternalAdminPath, isInternalAdminPath,
isLegacyAdminPath, isLegacyAdminPath,
toInternalAdminPath, toInternalAdminPath,
@@ -53,15 +57,35 @@ function isAdminBasicAuthValid(request: NextRequest): boolean {
export default async function middleware(request: NextRequest) { export default async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl; const { pathname } = request.nextUrl;
const isDevelopmentAdminRequest =
process.env.NODE_ENV !== "production" && isDevelopmentAdminPath(pathname);
const hostname = getRequestHostname( const hostname = getRequestHostname(
request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? request.nextUrl.hostname, request.headers.get("host") ?? request.headers.get("x-forwarded-host") ?? request.nextUrl.hostname,
); );
const isAdminRequest = isAdminHost(hostname); const isAdminRequest = isDevelopmentAdminRequest || isAdminHost(hostname);
const hasDedicatedAdminHostname = hasDedicatedAdminHost();
const adminRobotsHeaders = { const adminRobotsHeaders = {
"X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet, noimageindex", "X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet, noimageindex",
}; };
if (isInternalAdminPath(pathname) || isLegacyAdminPath(pathname)) { if (
isDevelopmentAdminRequest &&
hasDedicatedAdminHostname &&
!isAdminHost(hostname)
) {
const redirectUrl = new URL(getAdminBaseUrl());
redirectUrl.pathname = fromDevelopmentAdminPath(pathname);
redirectUrl.search = request.nextUrl.search;
return NextResponse.redirect(redirectUrl, 308);
}
if (isLegacyAdminPath(pathname) && process.env.NODE_ENV === "production") {
return new NextResponse("Not Found", {
status: 404,
});
}
if (isInternalAdminPath(pathname) && process.env.NODE_ENV === "production" && !isAdminRequest) {
return new NextResponse("Not Found", { return new NextResponse("Not Found", {
status: 404, status: 404,
}); });
@@ -79,8 +103,9 @@ export default async function middleware(request: NextRequest) {
} }
const rewriteUrl = request.nextUrl.clone(); const rewriteUrl = request.nextUrl.clone();
rewriteUrl.pathname = toInternalAdminPath(pathname); rewriteUrl.pathname = toInternalAdminPath(
isDevelopmentAdminRequest ? fromDevelopmentAdminPath(pathname) : pathname,
);
const response = NextResponse.rewrite(rewriteUrl); const response = NextResponse.rewrite(rewriteUrl);
response.headers.set("X-Robots-Tag", adminRobotsHeaders["X-Robots-Tag"]); response.headers.set("X-Robots-Tag", adminRobotsHeaders["X-Robots-Tag"]);
return response; return response;