diff --git a/.env.docker.local.example b/.env.docker.local.example new file mode 100644 index 0000000..ef6b984 --- /dev/null +++ b/.env.docker.local.example @@ -0,0 +1,3 @@ +NEXT_PUBLIC_SITE_URL="http://mohfarawati.localhost:3000" +NEXT_PUBLIC_ADMIN_URL="http://rootmohfarawati.localhost:3000" +ADMIN_HOST="rootmohfarawati.localhost" diff --git a/Makefile b/Makefile index f64ed9e..d2f06c5 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,8 @@ ps: docker compose ps 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: docker compose up -d --remove-orphans diff --git a/app/_admin/maintenance/actions.ts b/app/_admin/maintenance/actions.ts index 0d8a8ac..a841a4f 100644 --- a/app/_admin/maintenance/actions.ts +++ b/app/_admin/maintenance/actions.ts @@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; 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 { getLocalizedPath } from "@/lib/locale"; import { setMaintenanceMode } from "@/lib/app-config"; @@ -12,7 +12,7 @@ import { setMaintenanceMode } from "@/lib/app-config"; async function ensureAdmin() { if (!(await isAdminAuthenticated())) { await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } } @@ -20,7 +20,7 @@ export async function updateMaintenanceModeAction(formData: FormData) { await ensureAdmin(); 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"); redirectUrl.searchParams.set( "success", diff --git a/app/_admin/maintenance/page.tsx b/app/_admin/maintenance/page.tsx index c88566f..393a6aa 100644 --- a/app/_admin/maintenance/page.tsx +++ b/app/_admin/maintenance/page.tsx @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { MotionFade } from "@/components/motion-fade"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getMaintenanceMode } from "@/lib/app-config"; import { AppCard } from "@/components/ui/app-card"; import { Badge } from "@/components/ui/badge"; @@ -31,7 +32,7 @@ export default async function AdminMaintenancePage() { const authenticated = await isAdminAuthenticated(); if (!authenticated) { - redirect("/"); + redirect(getAdminAppPath("/")); } const maintenanceEnabled = await getMaintenanceMode(); @@ -39,7 +40,7 @@ export default async function AdminMaintenancePage() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } return ( diff --git a/app/_admin/marquee/actions.ts b/app/_admin/marquee/actions.ts index 65042f5..4812140 100644 --- a/app/_admin/marquee/actions.ts +++ b/app/_admin/marquee/actions.ts @@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; 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 { updateMarqueeSettings } from "@/lib/app-config"; import { routing } from "@/i18n/routing"; @@ -13,7 +13,7 @@ import { getLocalizedPath } from "@/lib/locale"; async function ensureAdmin() { if (!(await isAdminAuthenticated())) { await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } } @@ -72,13 +72,13 @@ export async function saveMarqueeSettingsAction(formData: FormData) { await updateMarqueeSettings(settings); await revalidateMarqueePages(); - redirect(withMessage("/marquee", "success", "Marquee gespeichert.")); + redirect(withMessage(getAdminAppPath("/marquee"), "success", "Marquee gespeichert.")); } catch (error) { if (isRedirectError(error)) { throw error; } const message = error instanceof Error ? error.message : "Marquee konnte nicht gespeichert werden."; - redirect(withMessage("/marquee", "error", message)); + redirect(withMessage(getAdminAppPath("/marquee"), "error", message)); } } diff --git a/app/_admin/marquee/page.tsx b/app/_admin/marquee/page.tsx index 22eef58..a558cb7 100644 --- a/app/_admin/marquee/page.tsx +++ b/app/_admin/marquee/page.tsx @@ -4,6 +4,7 @@ import { MarqueeSettingsForm } from "@/components/admin/marquee-settings-form"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell"; import { MotionFade } from "@/components/motion-fade"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getMarqueeSettings } from "@/lib/app-config"; import { saveMarqueeSettingsAction } from "./actions"; @@ -27,14 +28,14 @@ const copy = { export default async function AdminMarqueePage() { if (!(await isAdminAuthenticated())) { - redirect("/"); + redirect(getAdminAppPath("/")); } async function logoutAction() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } const marqueeSettings = await getMarqueeSettings(); diff --git a/app/_admin/media/actions.ts b/app/_admin/media/actions.ts index b2af042..1cbe2f7 100644 --- a/app/_admin/media/actions.ts +++ b/app/_admin/media/actions.ts @@ -5,7 +5,7 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; 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 { countMediaUsageReferences, getMediaAssetById } from "@/lib/media"; import { createStandaloneMediaAsset, deleteMediaAssetAndFile } from "@/lib/media-service"; @@ -15,7 +15,7 @@ import { prisma } from "@/lib/prisma"; async function ensureAdmin() { if (!(await isAdminAuthenticated())) { await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } } @@ -47,14 +47,14 @@ export async function createMediaAssetAction(formData: FormData) { }); revalidateMediaPages(); - redirect(withMessage("/media", "success", "Datei gespeichert.")); + redirect(withMessage(getAdminAppPath("/media"), "success", "Datei gespeichert.")); } catch (error) { if (isRedirectError(error)) { throw error; } 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); if (!asset) { - redirect(withMessage("/media", "error", "Datei nicht gefunden.")); + redirect(withMessage(getAdminAppPath("/media"), "error", "Datei nicht gefunden.")); } const usageCount = await countMediaUsageReferences(asset.id); if (usageCount > 0) { - redirect(withMessage("/media", "error", "Datei wird noch verwendet.")); + redirect(withMessage(getAdminAppPath("/media"), "error", "Datei wird noch verwendet.")); } await prisma.mediaAsset.delete({ @@ -90,13 +90,13 @@ export async function deleteMediaAssetAction(formData: FormData) { } revalidateMediaPages(); - redirect(withMessage("/media", "success", "Datei geloescht.")); + redirect(withMessage(getAdminAppPath("/media"), "success", "Datei geloescht.")); } catch (error) { if (isRedirectError(error)) { throw error; } const message = error instanceof Error ? error.message : "Datei konnte nicht geloescht werden."; - redirect(withMessage("/media", "error", message)); + redirect(withMessage(getAdminAppPath("/media"), "error", message)); } } diff --git a/app/_admin/media/page.tsx b/app/_admin/media/page.tsx index e45129d..4eff542 100644 --- a/app/_admin/media/page.tsx +++ b/app/_admin/media/page.tsx @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { MediaLibraryManager } from "@/components/admin/media-library-manager"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getAdminMediaAssets } from "@/lib/media"; export const dynamic = "force-dynamic"; @@ -22,14 +23,14 @@ const copy = { export default async function AdminMediaPage() { if (!(await isAdminAuthenticated())) { - redirect("/"); + redirect(getAdminAppPath("/")); } async function logoutAction() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } const mediaAssets = await getAdminMediaAssets(); diff --git a/app/_admin/page.tsx b/app/_admin/page.tsx index 673e53e..4324cc5 100644 --- a/app/_admin/page.tsx +++ b/app/_admin/page.tsx @@ -20,6 +20,7 @@ import { resetAdminFailedAttempts, setAdminSessionCookie, } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getMaintenanceMode } from "@/lib/app-config"; import { getAdminMediaAssets } from "@/lib/media"; import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio"; @@ -76,28 +77,28 @@ export default async function AdminPage({ searchParams }: AdminPageProps) { const currentLockState = await getAdminLockState(); if (currentLockState.locked) { - redirect("/?error=locked"); + redirect(`${getAdminAppPath("/")}?error=locked`); } if (!isAdminAuthConfigured() || !isPasswordValid(password)) { const failState = await registerFailedAdminAttempt(); if (failState.locked) { - redirect("/?error=locked"); + redirect(`${getAdminAppPath("/")}?error=locked`); } - redirect("/?error=invalid"); + redirect(`${getAdminAppPath("/")}?error=invalid`); } await resetAdminFailedAttempts(); await setAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } async function logoutAction() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } if (!authenticated) { diff --git a/app/_admin/portfolio/actions.ts b/app/_admin/portfolio/actions.ts index 495afce..77f1123 100644 --- a/app/_admin/portfolio/actions.ts +++ b/app/_admin/portfolio/actions.ts @@ -7,7 +7,7 @@ import { isRedirectError } from "next/dist/client/components/redirect-error"; import { ZodError } from "zod"; 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 { deleteEntityMediaUsages, replaceEntityMediaUsages } from "@/lib/media"; import { resolveMediaSelection } from "@/lib/media-service"; @@ -26,7 +26,7 @@ import { async function ensureAdmin() { if (!(await isAdminAuthenticated())) { await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } } @@ -107,7 +107,7 @@ async function removeManagedPaths(paths: string[]) { export async function upsertCategoryAction(formData: FormData) { await ensureAdmin(); - const redirectPath = getRedirectPath(formData, "/portfolio/categories"); + const redirectPath = getRedirectPath(formData, getAdminAppPath("/portfolio/categories")); try { const parsed = categoryInputSchema.parse({ @@ -157,7 +157,7 @@ export async function upsertCategoryAction(formData: FormData) { export async function deleteCategoryAction(formData: FormData) { await ensureAdmin(); - const redirectPath = getRedirectPath(formData, "/portfolio/categories"); + const redirectPath = getRedirectPath(formData, getAdminAppPath("/portfolio/categories")); const id = String(formData.get("id") ?? ""); try { @@ -192,8 +192,8 @@ export async function saveProjectAction(formData: FormData) { await ensureAdmin(); const fallbackRedirect = String(formData.get("id") ?? "").trim() - ? `/portfolio/projects/${String(formData.get("id") ?? "").trim()}` - : "/portfolio/projects/new"; + ? getAdminAppPath(`/portfolio/projects/${String(formData.get("id") ?? "").trim()}`) + : getAdminAppPath("/portfolio/projects/new"); const redirectPath = getRedirectPath(formData, fallbackRedirect); const uploadedPaths: string[] = []; const createdMediaAssetIds: string[] = []; @@ -526,7 +526,7 @@ export async function saveProjectAction(formData: FormData) { } redirect( - withMessage(`/portfolio/projects/${projectResult.project.id}`, "success", "Projekt gespeichert."), + withMessage(getAdminAppPath(`/portfolio/projects/${projectResult.project.id}`), "success", "Projekt gespeichert."), ); } catch (error) { if (isRedirectError(error)) { @@ -579,7 +579,7 @@ export async function deleteProjectAction(formData: FormData) { }); if (!project) { - redirect(withMessage("/portfolio", "error", "Project not found.")); + redirect(withMessage(getAdminAppPath("/portfolio"), "error", "Project not found.")); } await prisma.portfolioProject.delete({ @@ -596,12 +596,12 @@ export async function deleteProjectAction(formData: FormData) { revalidatePath(getLocalizedPath(locale, `/portfolio/${project.slug}`)); } - redirect(withMessage("/portfolio", "success", "Project deleted.")); + redirect(withMessage(getAdminAppPath("/portfolio"), "success", "Project deleted.")); } catch (error) { if (isRedirectError(error)) { throw error; } - redirect(withMessage("/portfolio", "error", "Unable to delete project.")); + redirect(withMessage(getAdminAppPath("/portfolio"), "error", "Unable to delete project.")); } } diff --git a/app/_admin/portfolio/categories/page.tsx b/app/_admin/portfolio/categories/page.tsx index 73cdd9f..2f1fa98 100644 --- a/app/_admin/portfolio/categories/page.tsx +++ b/app/_admin/portfolio/categories/page.tsx @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { PortfolioCategoriesManager } from "@/components/admin/portfolio-categories-manager"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getAdminPortfolioCategories } from "@/lib/portfolio"; import { deleteCategoryAction, upsertCategoryAction } from "../actions"; @@ -24,14 +25,14 @@ const copy = { export default async function AdminPortfolioCategoriesPage() { if (!(await isAdminAuthenticated())) { - redirect("/"); + redirect(getAdminAppPath("/")); } async function logoutAction() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } const categories = await getAdminPortfolioCategories(); diff --git a/app/_admin/portfolio/media/page.tsx b/app/_admin/portfolio/media/page.tsx index dfde150..efb9640 100644 --- a/app/_admin/portfolio/media/page.tsx +++ b/app/_admin/portfolio/media/page.tsx @@ -1,5 +1,6 @@ import { redirect } from "next/navigation"; +import { getAdminAppPath } from "@/lib/admin-routing"; export default function AdminPortfolioMediaRedirectPage() { - redirect("/media"); + redirect(getAdminAppPath("/media")); } diff --git a/app/_admin/portfolio/page.tsx b/app/_admin/portfolio/page.tsx index 11b115c..a9f0031 100644 --- a/app/_admin/portfolio/page.tsx +++ b/app/_admin/portfolio/page.tsx @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell"; import { PortfolioProjectsOverview } from "@/components/admin/portfolio-projects-overview"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio"; export const dynamic = "force-dynamic"; @@ -33,14 +34,14 @@ export default async function AdminPortfolioPage({ searchParams }: AdminPortfoli const resolvedSearchParams = await searchParams; if (!(await isAdminAuthenticated())) { - redirect("/"); + redirect(getAdminAppPath("/")); } async function logoutAction() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } const selectedCategory = resolvedSearchParams?.category && resolvedSearchParams.category !== "__all__" diff --git a/app/_admin/portfolio/projects/[id]/page.tsx b/app/_admin/portfolio/projects/[id]/page.tsx index 15e0b12..0798a22 100644 --- a/app/_admin/portfolio/projects/[id]/page.tsx +++ b/app/_admin/portfolio/projects/[id]/page.tsx @@ -16,6 +16,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getMediaOptions } from "@/lib/media"; import { getActivePortfolioCategories, @@ -53,14 +54,14 @@ export default async function AdminPortfolioProjectPage({ params, }: AdminPortfolioProjectPageProps) { if (!(await isAdminAuthenticated())) { - redirect("/"); + redirect(getAdminAppPath("/")); } async function logoutAction() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } const { id } = await params; @@ -72,7 +73,7 @@ export default async function AdminPortfolioProjectPage({ ]); if (!project) { - redirect("/portfolio?error=Project+not+found."); + redirect(`${getAdminAppPath("/portfolio")}?error=Project+not+found.`); } return ( @@ -92,7 +93,7 @@ export default async function AdminPortfolioProjectPage({ mediaOptions={mediaOptions} project={project} formId="portfolio-project-form" - redirectPath={`/portfolio/projects/${project.id}`} + redirectPath={getAdminAppPath(`/portfolio/projects/${project.id}`)} /> diff --git a/app/_admin/portfolio/projects/new/page.tsx b/app/_admin/portfolio/projects/new/page.tsx index 5509d12..7142659 100644 --- a/app/_admin/portfolio/projects/new/page.tsx +++ b/app/_admin/portfolio/projects/new/page.tsx @@ -4,6 +4,7 @@ import { MotionFade } from "@/components/motion-fade"; import { PortfolioProjectForm } from "@/components/admin/portfolio-project-form"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getMediaOptions } from "@/lib/media"; import { getActivePortfolioCategories } from "@/lib/portfolio"; @@ -26,14 +27,14 @@ const copy = { export default async function AdminNewPortfolioProjectPage() { if (!(await isAdminAuthenticated())) { - redirect("/"); + redirect(getAdminAppPath("/")); } async function logoutAction() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } const [categories, mediaOptions] = await Promise.all([ @@ -57,7 +58,7 @@ export default async function AdminNewPortfolioProjectPage() { categories={categories} mediaOptions={mediaOptions} formId="portfolio-project-form" - redirectPath="/portfolio/projects/new" + redirectPath={getAdminAppPath("/portfolio/projects/new")} /> diff --git a/app/_admin/portfolio/projects/page.tsx b/app/_admin/portfolio/projects/page.tsx index 5831f62..471d440 100644 --- a/app/_admin/portfolio/projects/page.tsx +++ b/app/_admin/portfolio/projects/page.tsx @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { PortfolioProjectsOverview } from "@/components/admin/portfolio-projects-overview"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio"; export const dynamic = "force-dynamic"; @@ -33,14 +34,14 @@ export default async function AdminPortfolioProjectsPage({ const resolvedSearchParams = await searchParams; if (!(await isAdminAuthenticated())) { - redirect("/"); + redirect(getAdminAppPath("/")); } async function logoutAction() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } const selectedCategory = resolvedSearchParams?.category && resolvedSearchParams.category !== "__all__" diff --git a/app/_admin/site-settings/actions.ts b/app/_admin/site-settings/actions.ts index 3b9819a..8e7c3eb 100644 --- a/app/_admin/site-settings/actions.ts +++ b/app/_admin/site-settings/actions.ts @@ -14,7 +14,7 @@ import { SITE_SETTINGS_LOGO_LIGHT_FIELD_KEY, updateSiteSettings, } 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 { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { replaceEntityMediaUsages } from "@/lib/media"; @@ -28,7 +28,7 @@ import { prisma } from "@/lib/prisma"; async function ensureAdmin() { if (!(await isAdminAuthenticated())) { await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } } @@ -278,7 +278,7 @@ export async function saveSiteSettingsAction(formData: FormData) { }); await revalidateSiteSettingsPages(); - redirect(withMessage("/site-settings", "success", "Einstellungen gespeichert.")); + redirect(withMessage(getAdminAppPath("/site-settings"), "success", "Einstellungen gespeichert.")); } catch (error) { if (isRedirectError(error)) { throw error; @@ -291,6 +291,6 @@ export async function saveSiteSettingsAction(formData: FormData) { ? error.message : "Einstellungen konnten nicht gespeichert werden."; - redirect(withMessage("/site-settings", "error", message)); + redirect(withMessage(getAdminAppPath("/site-settings"), "error", message)); } } diff --git a/app/_admin/site-settings/page.tsx b/app/_admin/site-settings/page.tsx index 5ee9eb7..bd3a15b 100644 --- a/app/_admin/site-settings/page.tsx +++ b/app/_admin/site-settings/page.tsx @@ -5,6 +5,7 @@ import { MotionFade } from "@/components/motion-fade"; import { SiteSettingsForm } from "@/components/admin/site-settings-form"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getSiteSettings, getSiteSettingsMediaBindings, @@ -31,14 +32,14 @@ const copy = { export default async function AdminSiteSettingsPage() { if (!(await isAdminAuthenticated())) { - redirect("/"); + redirect(getAdminAppPath("/")); } async function logoutAction() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } const [siteSettings, mediaBindings, mediaOptions] = await Promise.all([ diff --git a/app/_admin/smtp/actions.ts b/app/_admin/smtp/actions.ts index be68ac0..0c055a6 100644 --- a/app/_admin/smtp/actions.ts +++ b/app/_admin/smtp/actions.ts @@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; 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 { isCheckedFormValue } from "@/lib/form-data"; import { @@ -17,7 +17,7 @@ import type { MailSettings } from "@/lib/mail-settings"; async function ensureAdmin() { if (!(await isAdminAuthenticated())) { await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } } @@ -79,7 +79,7 @@ export async function saveMailSettingsAction(formData: FormData) { await updateMailSettings(nextMailSettings); revalidatePath(toInternalAdminPath("/smtp")); - redirect(withMessage("/smtp", "success", "SMTP Einstellungen gespeichert.")); + redirect(withMessage(getAdminAppPath("/smtp"), "success", "SMTP Einstellungen gespeichert.")); } catch (error) { if (isRedirectError(error)) { throw error; @@ -90,7 +90,7 @@ export async function saveMailSettingsAction(formData: FormData) { ? error.message : "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 { await sendTestEmail(); - redirect(withMessage("/smtp", "success", "Test-E-Mail gesendet.")); + redirect(withMessage(getAdminAppPath("/smtp"), "success", "Test-E-Mail gesendet.")); } catch (error) { if (isRedirectError(error)) { throw error; @@ -110,6 +110,6 @@ export async function sendTestEmailAction() { ? error.message : "Test-E-Mail konnte nicht gesendet werden."; - redirect(withMessage("/smtp", "error", message)); + redirect(withMessage(getAdminAppPath("/smtp"), "error", message)); } } diff --git a/app/_admin/smtp/contact-protection/actions.ts b/app/_admin/smtp/contact-protection/actions.ts index 7e3e0a9..5af62af 100644 --- a/app/_admin/smtp/contact-protection/actions.ts +++ b/app/_admin/smtp/contact-protection/actions.ts @@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; 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 { isCheckedFormValue } from "@/lib/form-data"; import { @@ -16,7 +16,7 @@ import type { ContactProtectionSettings } from "@/lib/contact-protection"; async function ensureAdmin() { if (!(await isAdminAuthenticated())) { await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } } @@ -92,7 +92,7 @@ export async function saveContactProtectionSettingsAction(formData: FormData) { revalidatePath("/en/contact"); redirect( withMessage( - "/smtp/contact-protection", + getAdminAppPath("/smtp/contact-protection"), "success", "Contact Protection gespeichert.", ), @@ -107,6 +107,6 @@ export async function saveContactProtectionSettingsAction(formData: FormData) { ? error.message : "Contact Protection konnte nicht gespeichert werden."; - redirect(withMessage("/smtp/contact-protection", "error", message)); + redirect(withMessage(getAdminAppPath("/smtp/contact-protection"), "error", message)); } } diff --git a/app/_admin/smtp/contact-protection/page.tsx b/app/_admin/smtp/contact-protection/page.tsx index 3526f9c..49e3d86 100644 --- a/app/_admin/smtp/contact-protection/page.tsx +++ b/app/_admin/smtp/contact-protection/page.tsx @@ -4,6 +4,7 @@ import { MotionFade } from "@/components/motion-fade"; import { ContactProtectionForm } from "@/components/admin/contact-protection-form"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getContactProtectionFormValues } from "@/lib/app-config"; import { saveContactProtectionSettingsAction } from "./actions"; @@ -27,14 +28,14 @@ const copy = { export default async function AdminSMTPProtectionPage() { if (!(await isAdminAuthenticated())) { - redirect("/"); + redirect(getAdminAppPath("/")); } async function logoutAction() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } const settings = await getContactProtectionFormValues(); diff --git a/app/_admin/smtp/page.tsx b/app/_admin/smtp/page.tsx index 4d44693..bb6d559 100644 --- a/app/_admin/smtp/page.tsx +++ b/app/_admin/smtp/page.tsx @@ -5,6 +5,7 @@ import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell"; import { SMTPSettingsForm } from "@/components/admin/smtp-settings-form"; import { Button } from "@/components/ui/button"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getMailSettingsFormValues } from "@/lib/app-config"; import { saveMailSettingsAction, sendTestEmailAction } from "./actions"; @@ -28,14 +29,14 @@ const copy = { export default async function AdminSMTPPage() { if (!(await isAdminAuthenticated())) { - redirect("/"); + redirect(getAdminAppPath("/")); } async function logoutAction() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } const mailSettings = await getMailSettingsFormValues(); diff --git a/app/_admin/ui-kit/page.tsx b/app/_admin/ui-kit/page.tsx index 2a8c9ba..7de0faa 100644 --- a/app/_admin/ui-kit/page.tsx +++ b/app/_admin/ui-kit/page.tsx @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { MotionFade } from "@/components/motion-fade"; import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { UiKitShowcase } from "@/components/ui/ui-kit-showcase"; export const dynamic = "force-dynamic"; @@ -24,14 +25,14 @@ export default async function AdminUiKitPage() { const authenticated = await isAdminAuthenticated(); if (!authenticated) { - redirect("/"); + redirect(getAdminAppPath("/")); } async function logoutAction() { "use server"; await clearAdminSessionCookie(); - redirect("/"); + redirect(getAdminAppPath("/")); } return ( diff --git a/app/admin-internal/layout.tsx b/app/admin-internal/layout.tsx index f6508ec..647d969 100644 --- a/app/admin-internal/layout.tsx +++ b/app/admin-internal/layout.tsx @@ -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"; diff --git a/app/admin-internal/maintenance/page.tsx b/app/admin-internal/maintenance/page.tsx index 6aa01c4..a143fa5 100644 --- a/app/admin-internal/maintenance/page.tsx +++ b/app/admin-internal/maintenance/page.tsx @@ -1,2 +1,2 @@ -export { dynamic } from "../../_admin/maintenance/page"; +export const dynamic = "force-dynamic"; export { default } from "../../_admin/maintenance/page"; diff --git a/app/admin-internal/media/page.tsx b/app/admin-internal/media/page.tsx index b99431c..1101384 100644 --- a/app/admin-internal/media/page.tsx +++ b/app/admin-internal/media/page.tsx @@ -1,2 +1,2 @@ -export { dynamic } from "../../_admin/media/page"; +export const dynamic = "force-dynamic"; export { default } from "../../_admin/media/page"; diff --git a/app/admin-internal/page.tsx b/app/admin-internal/page.tsx index 98c7a67..36c47e5 100644 --- a/app/admin-internal/page.tsx +++ b/app/admin-internal/page.tsx @@ -1,2 +1,2 @@ -export { dynamic } from "../_admin/page"; +export const dynamic = "force-dynamic"; export { default } from "../_admin/page"; diff --git a/app/root/layout.tsx b/app/root/layout.tsx new file mode 100644 index 0000000..647d969 --- /dev/null +++ b/app/root/layout.tsx @@ -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"; diff --git a/app/root/maintenance/page.tsx b/app/root/maintenance/page.tsx new file mode 100644 index 0000000..a143fa5 --- /dev/null +++ b/app/root/maintenance/page.tsx @@ -0,0 +1,2 @@ +export const dynamic = "force-dynamic"; +export { default } from "../../_admin/maintenance/page"; diff --git a/app/root/marquee/page.tsx b/app/root/marquee/page.tsx new file mode 100644 index 0000000..1fb130b --- /dev/null +++ b/app/root/marquee/page.tsx @@ -0,0 +1 @@ +export { default } from "../../_admin/marquee/page"; diff --git a/app/root/media/page.tsx b/app/root/media/page.tsx new file mode 100644 index 0000000..1101384 --- /dev/null +++ b/app/root/media/page.tsx @@ -0,0 +1,2 @@ +export const dynamic = "force-dynamic"; +export { default } from "../../_admin/media/page"; diff --git a/app/root/page.tsx b/app/root/page.tsx new file mode 100644 index 0000000..36c47e5 --- /dev/null +++ b/app/root/page.tsx @@ -0,0 +1,2 @@ +export const dynamic = "force-dynamic"; +export { default } from "../_admin/page"; diff --git a/app/root/portfolio/categories/page.tsx b/app/root/portfolio/categories/page.tsx new file mode 100644 index 0000000..9018c4a --- /dev/null +++ b/app/root/portfolio/categories/page.tsx @@ -0,0 +1 @@ +export { default } from "../../../_admin/portfolio/categories/page"; diff --git a/app/root/portfolio/media/page.tsx b/app/root/portfolio/media/page.tsx new file mode 100644 index 0000000..b8f5f13 --- /dev/null +++ b/app/root/portfolio/media/page.tsx @@ -0,0 +1 @@ +export { default } from "../../../_admin/portfolio/media/page"; diff --git a/app/root/portfolio/page.tsx b/app/root/portfolio/page.tsx new file mode 100644 index 0000000..c1ded26 --- /dev/null +++ b/app/root/portfolio/page.tsx @@ -0,0 +1 @@ +export { default } from "../../_admin/portfolio/page"; diff --git a/app/root/portfolio/projects/[id]/page.tsx b/app/root/portfolio/projects/[id]/page.tsx new file mode 100644 index 0000000..bdb4e41 --- /dev/null +++ b/app/root/portfolio/projects/[id]/page.tsx @@ -0,0 +1 @@ +export { default } from "../../../../_admin/portfolio/projects/[id]/page"; diff --git a/app/root/portfolio/projects/new/page.tsx b/app/root/portfolio/projects/new/page.tsx new file mode 100644 index 0000000..dba419e --- /dev/null +++ b/app/root/portfolio/projects/new/page.tsx @@ -0,0 +1 @@ +export { default } from "../../../../_admin/portfolio/projects/new/page"; diff --git a/app/root/portfolio/projects/page.tsx b/app/root/portfolio/projects/page.tsx new file mode 100644 index 0000000..b85d3e9 --- /dev/null +++ b/app/root/portfolio/projects/page.tsx @@ -0,0 +1 @@ +export { default } from "../../../_admin/portfolio/projects/page"; diff --git a/app/root/site-settings/page.tsx b/app/root/site-settings/page.tsx new file mode 100644 index 0000000..b3884cd --- /dev/null +++ b/app/root/site-settings/page.tsx @@ -0,0 +1 @@ +export { default } from "../../_admin/site-settings/page"; diff --git a/app/root/smtp/contact-protection/page.tsx b/app/root/smtp/contact-protection/page.tsx new file mode 100644 index 0000000..60cd1ad --- /dev/null +++ b/app/root/smtp/contact-protection/page.tsx @@ -0,0 +1 @@ +export { default } from "../../../_admin/smtp/contact-protection/page"; diff --git a/app/root/smtp/page.tsx b/app/root/smtp/page.tsx new file mode 100644 index 0000000..5e58433 --- /dev/null +++ b/app/root/smtp/page.tsx @@ -0,0 +1 @@ +export { default } from "../../_admin/smtp/page"; diff --git a/app/root/ui-kit/page.tsx b/app/root/ui-kit/page.tsx new file mode 100644 index 0000000..8ea872c --- /dev/null +++ b/app/root/ui-kit/page.tsx @@ -0,0 +1 @@ +export { default } from "../../_admin/ui-kit/page"; diff --git a/components/admin/admin-dashboard-shell.tsx b/components/admin/admin-dashboard-shell.tsx index c695f01..c0ed0a8 100644 --- a/components/admin/admin-dashboard-shell.tsx +++ b/components/admin/admin-dashboard-shell.tsx @@ -19,7 +19,7 @@ import { SidebarMaintenanceControl } from "@/components/admin/sidebar-maintenanc import { SoundToggle } from "@/components/sound-toggle"; import { ThemeToggle } from "@/components/theme-toggle"; import { Button } from "@/components/ui/button"; -import { buildSiteUrl } from "@/lib/admin-routing"; +import { buildSiteUrl, getAdminAppPath } from "@/lib/admin-routing"; import { getMaintenanceMode, getSiteSettingsMediaBindings } from "@/lib/app-config"; import { getAdminNavigation } from "@/lib/admin-navigation"; @@ -75,12 +75,12 @@ export async function AdminDashboardShell({ const sidebarItems = getAdminNavigation(copy, active, smtpChild, portfolioChild); const normalizedSidebarItems = sidebarItems.filter( (item) => - item.href !== "/maintenance" && - item.href !== "/ui-kit" && - item.href !== "/smtp" && - item.href !== "/marquee", + item.href !== getAdminAppPath("/maintenance") && + item.href !== getAdminAppPath("/ui-kit") && + item.href !== getAdminAppPath("/smtp") && + item.href !== getAdminAppPath("/marquee"), ); - const footerSidebarItems = ["/marquee", "/smtp"] + const footerSidebarItems = [getAdminAppPath("/marquee"), getAdminAppPath("/smtp")] .map((href) => sidebarItems.find((item) => item.href === href)) .filter((item): item is NonNullable => Boolean(item)); const headerIcon = @@ -126,7 +126,7 @@ export async function AdminDashboardShell({ variant={active === "ui-kit" ? "default" : "outline"} className="w-full justify-between" > - + {copy.uiKit} diff --git a/components/admin/portfolio-projects-overview.tsx b/components/admin/portfolio-projects-overview.tsx index 782e432..419b837 100644 --- a/components/admin/portfolio-projects-overview.tsx +++ b/components/admin/portfolio-projects-overview.tsx @@ -8,6 +8,7 @@ import { AppCard } from "@/components/ui/app-card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { CardContent } from "@/components/ui/card"; +import { getAdminAppPath } from "@/lib/admin-routing"; import { getLocalizedPath } from "@/lib/locale"; import { getLocalizedValue, type PortfolioCategoryView, type PortfolioProjectView } from "@/lib/portfolio"; @@ -46,13 +47,13 @@ export function PortfolioProjectsOverview({
{categories.map((category) => ( diff --git a/lib/admin-navigation.ts b/lib/admin-navigation.ts index ba45c9a..2029ed0 100644 --- a/lib/admin-navigation.ts +++ b/lib/admin-navigation.ts @@ -11,6 +11,7 @@ import { Type, type LucideIcon, } from "lucide-react"; +import { getAdminAppPath } from "@/lib/admin-routing"; type AdminNavigationCopy = { overview: string; @@ -42,56 +43,56 @@ export function getAdminNavigation( return [ { label: copy.overview, - href: "/", + href: getAdminAppPath("/"), icon: LayoutDashboard, active: active === "overview", }, { label: copy.maintenance, - href: "/maintenance", + href: getAdminAppPath("/maintenance"), icon: ShieldAlert, active: active === "maintenance", }, { label: copy.uiKit, - href: "/ui-kit", + href: getAdminAppPath("/ui-kit"), icon: SwatchBook, active: active === "ui-kit", }, { label: copy.media, - href: "/media", + href: getAdminAppPath("/media"), icon: ImageIcon, active: active === "media", }, { label: copy.siteSettings, - href: "/site-settings", + href: getAdminAppPath("/site-settings"), icon: Globe2, active: active === "site-settings", }, { label: copy.marquee ?? "Marquee", - href: "/marquee", + href: getAdminAppPath("/marquee"), icon: Type, active: active === "marquee", }, { label: copy.smtp ?? "SMTP", - href: "/smtp", + href: getAdminAppPath("/smtp"), icon: Mail, active: active === "smtp" && !smtpChild, expanded: active === "smtp", children: [ { label: copy.smtp ?? "SMTP", - href: "/smtp", + href: getAdminAppPath("/smtp"), icon: Mail, active: smtpChild === "settings" || (!smtpChild && active === "smtp"), }, { label: copy.contactProtection ?? "Contact Protection", - href: "/smtp/contact-protection", + href: getAdminAppPath("/smtp/contact-protection"), icon: ShieldAlert, active: smtpChild === "contact-protection", }, @@ -99,26 +100,26 @@ export function getAdminNavigation( }, { label: copy.portfolio, - href: "/portfolio", + href: getAdminAppPath("/portfolio"), icon: FolderKanban, active: active === "portfolio" && !portfolioChild, expanded: active === "portfolio", children: [ { label: "Projects", - href: "/portfolio", + href: getAdminAppPath("/portfolio"), icon: FolderKanban, active: portfolioChild === "overview" || portfolioChild === "projects", }, { label: "Add Project", - href: "/portfolio/projects/new", + href: getAdminAppPath("/portfolio/projects/new"), icon: PlusSquare, active: portfolioChild === "new-project", }, { label: "Add Category", - href: "/portfolio/categories", + href: getAdminAppPath("/portfolio/categories"), icon: Tags, active: portfolioChild === "categories", }, diff --git a/lib/admin-routing.ts b/lib/admin-routing.ts index 53c89ec..1aab492 100644 --- a/lib/admin-routing.ts +++ b/lib/admin-routing.ts @@ -1,6 +1,7 @@ const DEFAULT_ADMIN_HOST = "root.mohfarawati.de"; const DEFAULT_ADMIN_URL = `https://${DEFAULT_ADMIN_HOST}`; const DEFAULT_SITE_URL = "https://mohfarawati.de"; +const DEV_ADMIN_PREFIX = "/root"; export const INTERNAL_ADMIN_PREFIX = "/admin-internal"; @@ -16,10 +17,32 @@ function normalizeBaseUrl(url: string): string { 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 { 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 { return (hostHeader ?? "") .split(",")[0] @@ -32,10 +55,18 @@ export function isAdminHost(hostname: string): boolean { return hostname === getAdminHost(); } +export function hasDedicatedAdminHost(): boolean { + return getAdminHost() !== getSiteHost(); +} + export function isLegacyAdminPath(pathname: string): boolean { 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 { 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}`; } +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 { 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 { - return `${getAdminBaseUrl()}${normalizePathname(pathname)}`; + return `${getAdminBaseUrl()}${getAdminAppPath(pathname)}`; } export function getSiteBaseUrl(): string { diff --git a/middleware.ts b/middleware.ts index daa4ada..c0e1a1a 100644 --- a/middleware.ts +++ b/middleware.ts @@ -4,8 +4,12 @@ import type { NextRequest } from "next/server"; import { routing } from "./i18n/routing"; import { + fromDevelopmentAdminPath, + getAdminBaseUrl, getRequestHostname, + isDevelopmentAdminPath, isAdminHost, + hasDedicatedAdminHost, isInternalAdminPath, isLegacyAdminPath, toInternalAdminPath, @@ -53,15 +57,35 @@ function isAdminBasicAuthValid(request: NextRequest): boolean { export default async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; + const isDevelopmentAdminRequest = + process.env.NODE_ENV !== "production" && isDevelopmentAdminPath(pathname); 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 = { "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", { status: 404, }); @@ -79,8 +103,9 @@ export default async function middleware(request: NextRequest) { } const rewriteUrl = request.nextUrl.clone(); - rewriteUrl.pathname = toInternalAdminPath(pathname); - + rewriteUrl.pathname = toInternalAdminPath( + isDevelopmentAdminRequest ? fromDevelopmentAdminPath(pathname) : pathname, + ); const response = NextResponse.rewrite(rewriteUrl); response.headers.set("X-Robots-Tag", adminRobotsHeaders["X-Robots-Tag"]); return response;