Move admin area to root subdomain
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-12 00:15:12 +01:00
parent e7dab6f0ef
commit 87a597c872
50 changed files with 256 additions and 140 deletions
+2
View File
@@ -1,5 +1,7 @@
DATABASE_URL="postgresql://postgres:postgres@db:5432/moh_sass?schema=public" DATABASE_URL="postgresql://postgres:postgres@db:5432/moh_sass?schema=public"
NEXT_PUBLIC_APP_URL="https://mohfarawati.de" NEXT_PUBLIC_APP_URL="https://mohfarawati.de"
NEXT_PUBLIC_SITE_URL="https://mohfarawati.de"
NEXT_PUBLIC_ADMIN_URL="https://root.mohfarawati.de"
NEXT_TELEMETRY_DISABLED="1" NEXT_TELEMETRY_DISABLED="1"
ADMIN_PASSWORD="123Yolo!321" ADMIN_PASSWORD="123Yolo!321"
ADMIN_AUTH_SECRET="Us0z76jwlTQLOeQWAGGxAxDcc0rHwp4q" ADMIN_AUTH_SECRET="Us0z76jwlTQLOeQWAGGxAxDcc0rHwp4q"
+2
View File
@@ -0,0 +1,2 @@
export { metadata } from "../root/layout";
export { default } from "../root/layout";
+2
View File
@@ -0,0 +1,2 @@
export { dynamic } from "../../root/maintenance/page";
export { default } from "../../root/maintenance/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../root/marquee/page";
+2
View File
@@ -0,0 +1,2 @@
export { dynamic } from "../../root/media/page";
export { default } from "../../root/media/page";
+2
View File
@@ -0,0 +1,2 @@
export { dynamic } from "../root/page";
export { default } from "../root/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../../root/portfolio/categories/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../../root/portfolio/media/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../root/portfolio/page";
@@ -0,0 +1 @@
export { default } from "../../../../root/portfolio/projects/[id]/page";
@@ -0,0 +1 @@
export { default } from "../../../../root/portfolio/projects/new/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../../root/portfolio/projects/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../root/site-settings/page";
@@ -0,0 +1 @@
export { default } from "../../../root/smtp/contact-protection/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../root/smtp/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "../../root/ui-kit/page";
+1 -1
View File
@@ -7,7 +7,7 @@ export default function robots(): MetadataRoute.Robots {
rules: [ rules: [
{ {
userAgent: "*", userAgent: "*",
disallow: ["/root"], disallow: ["/_admin"],
}, },
], ],
sitemap: new URL("/sitemap.xml", siteUrl).toString(), sitemap: new URL("/sitemap.xml", siteUrl).toString(),
+6 -5
View File
@@ -4,6 +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 { 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";
@@ -11,7 +12,7 @@ import { setMaintenanceMode } from "@/lib/app-config";
function ensureAdmin() { function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
} }
@@ -19,7 +20,7 @@ export async function updateMaintenanceModeAction(formData: FormData) {
ensureAdmin(); ensureAdmin();
const nextValue = formData.get("enabled") === "true"; const nextValue = formData.get("enabled") === "true";
const redirectPath = String(formData.get("redirectPath") ?? "/root"); const redirectPath = String(formData.get("redirectPath") ?? "/");
const redirectUrl = new URL(redirectPath, "http://localhost"); const redirectUrl = new URL(redirectPath, "http://localhost");
redirectUrl.searchParams.set( redirectUrl.searchParams.set(
"success", "success",
@@ -29,9 +30,9 @@ export async function updateMaintenanceModeAction(formData: FormData) {
await setMaintenanceMode(nextValue); await setMaintenanceMode(nextValue);
revalidatePath("/", "layout"); revalidatePath("/", "layout");
revalidatePath("/coming-soon"); revalidatePath("/coming-soon");
revalidatePath("/root"); revalidatePath(toInternalAdminPath("/"));
revalidatePath("/root/maintenance"); revalidatePath(toInternalAdminPath("/maintenance"));
revalidatePath(redirectPath); revalidatePath(toInternalAdminPath(redirectUrl.pathname));
for (const appLocale of routing.locales) { for (const appLocale of routing.locales) {
revalidatePath(getLocalizedPath(appLocale), "layout"); revalidatePath(getLocalizedPath(appLocale), "layout");
+2 -2
View File
@@ -31,7 +31,7 @@ export default async function RootMaintenancePage() {
const authenticated = isAdminAuthenticated(); const authenticated = isAdminAuthenticated();
if (!authenticated) { if (!authenticated) {
redirect("/root"); redirect("/");
} }
const maintenanceEnabled = await getMaintenanceMode(); const maintenanceEnabled = await getMaintenanceMode();
@@ -39,7 +39,7 @@ export default async function RootMaintenancePage() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
return ( return (
+6 -5
View File
@@ -4,6 +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"; import { isRedirectError } from "next/dist/client/components/redirect";
import { 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";
@@ -12,7 +13,7 @@ import { getLocalizedPath } from "@/lib/locale";
function ensureAdmin() { function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
} }
@@ -24,8 +25,8 @@ function withMessage(pathname: string, type: "success" | "error", message: strin
} }
async function revalidateMarqueePages() { async function revalidateMarqueePages() {
revalidatePath("/root"); revalidatePath(toInternalAdminPath("/"));
revalidatePath("/root/marquee"); revalidatePath(toInternalAdminPath("/marquee"));
for (const locale of routing.locales) { for (const locale of routing.locales) {
revalidatePath(getLocalizedPath(locale), "layout"); revalidatePath(getLocalizedPath(locale), "layout");
@@ -71,13 +72,13 @@ export async function saveMarqueeSettingsAction(formData: FormData) {
await updateMarqueeSettings(settings); await updateMarqueeSettings(settings);
await revalidateMarqueePages(); await revalidateMarqueePages();
redirect(withMessage("/root/marquee", "success", "Marquee gespeichert.")); redirect(withMessage("/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("/root/marquee", "error", message)); redirect(withMessage("/marquee", "error", message));
} }
} }
+2 -2
View File
@@ -27,14 +27,14 @@ const copy = {
export default async function RootMarqueePage() { export default async function RootMarqueePage() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
redirect("/root"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
const marqueeSettings = await getMarqueeSettings(); const marqueeSettings = await getMarqueeSettings();
+12 -11
View File
@@ -5,6 +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"; import { isRedirectError } from "next/dist/client/components/redirect";
import { 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";
@@ -14,7 +15,7 @@ import { prisma } from "@/lib/prisma";
function ensureAdmin() { function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
} }
@@ -26,10 +27,10 @@ function withMessage(pathname: string, type: "success" | "error", message: strin
} }
function revalidateMediaPages() { function revalidateMediaPages() {
revalidatePath("/root"); revalidatePath(toInternalAdminPath("/"));
revalidatePath("/root/media"); revalidatePath(toInternalAdminPath("/media"));
revalidatePath("/root/portfolio"); revalidatePath(toInternalAdminPath("/portfolio"));
revalidatePath("/root/portfolio/projects"); revalidatePath(toInternalAdminPath("/portfolio/projects"));
} }
export async function createMediaAssetAction(formData: FormData) { export async function createMediaAssetAction(formData: FormData) {
@@ -46,14 +47,14 @@ export async function createMediaAssetAction(formData: FormData) {
}); });
revalidateMediaPages(); revalidateMediaPages();
redirect(withMessage("/root/media", "success", "Datei gespeichert.")); redirect(withMessage("/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("/root/media", "error", message)); redirect(withMessage("/media", "error", message));
} }
} }
@@ -66,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("/root/media", "error", "Datei nicht gefunden.")); redirect(withMessage("/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("/root/media", "error", "Datei wird noch verwendet.")); redirect(withMessage("/media", "error", "Datei wird noch verwendet."));
} }
await prisma.mediaAsset.delete({ await prisma.mediaAsset.delete({
@@ -89,13 +90,13 @@ export async function deleteMediaAssetAction(formData: FormData) {
} }
revalidateMediaPages(); revalidateMediaPages();
redirect(withMessage("/root/media", "success", "Datei geloescht.")); redirect(withMessage("/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("/root/media", "error", message)); redirect(withMessage("/media", "error", message));
} }
} }
+2 -2
View File
@@ -22,14 +22,14 @@ const copy = {
export default async function RootMediaPage() { export default async function RootMediaPage() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
redirect("/root"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
const mediaAssets = await getAdminMediaAssets(); const mediaAssets = await getAdminMediaAssets();
+5 -5
View File
@@ -75,28 +75,28 @@ export default async function RootPage({ searchParams }: RootPageProps) {
const currentLockState = getAdminLockState(); const currentLockState = getAdminLockState();
if (currentLockState.locked) { if (currentLockState.locked) {
redirect("/root?error=locked"); redirect("/?error=locked");
} }
if (!isAdminAuthConfigured() || !isPasswordValid(password)) { if (!isAdminAuthConfigured() || !isPasswordValid(password)) {
const failState = registerFailedAdminAttempt(); const failState = registerFailedAdminAttempt();
if (failState.locked) { if (failState.locked) {
redirect("/root?error=locked"); redirect("/?error=locked");
} }
redirect("/root?error=invalid"); redirect("/?error=invalid");
} }
resetAdminFailedAttempts(); resetAdminFailedAttempts();
setAdminSessionCookie(); setAdminSessionCookie();
redirect("/root"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
if (!authenticated) { if (!authenticated) {
+16 -15
View File
@@ -7,6 +7,7 @@ import { isRedirectError } from "next/dist/client/components/redirect";
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 { 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";
@@ -25,7 +26,7 @@ import {
function ensureAdmin() { function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
} }
@@ -85,11 +86,11 @@ function parseZodError(error: ZodError) {
} }
async function revalidatePortfolioPages() { async function revalidatePortfolioPages() {
revalidatePath("/root"); revalidatePath(toInternalAdminPath("/"));
revalidatePath("/root/media"); revalidatePath(toInternalAdminPath("/media"));
revalidatePath("/root/portfolio"); revalidatePath(toInternalAdminPath("/portfolio"));
revalidatePath("/root/portfolio/categories"); revalidatePath(toInternalAdminPath("/portfolio/categories"));
revalidatePath("/root/portfolio/projects"); revalidatePath(toInternalAdminPath("/portfolio/projects"));
revalidatePath("/portfolio"); revalidatePath("/portfolio");
for (const locale of routing.locales) { for (const locale of routing.locales) {
@@ -106,7 +107,7 @@ async function removeManagedPaths(paths: string[]) {
export async function upsertCategoryAction(formData: FormData) { export async function upsertCategoryAction(formData: FormData) {
ensureAdmin(); ensureAdmin();
const redirectPath = getRedirectPath(formData, "/root/portfolio/categories"); const redirectPath = getRedirectPath(formData, "/portfolio/categories");
try { try {
const parsed = categoryInputSchema.parse({ const parsed = categoryInputSchema.parse({
@@ -156,7 +157,7 @@ export async function upsertCategoryAction(formData: FormData) {
export async function deleteCategoryAction(formData: FormData) { export async function deleteCategoryAction(formData: FormData) {
ensureAdmin(); ensureAdmin();
const redirectPath = getRedirectPath(formData, "/root/portfolio/categories"); const redirectPath = getRedirectPath(formData, "/portfolio/categories");
const id = String(formData.get("id") ?? ""); const id = String(formData.get("id") ?? "");
try { try {
@@ -191,8 +192,8 @@ export async function saveProjectAction(formData: FormData) {
ensureAdmin(); ensureAdmin();
const fallbackRedirect = String(formData.get("id") ?? "").trim() const fallbackRedirect = String(formData.get("id") ?? "").trim()
? `/root/portfolio/projects/${String(formData.get("id") ?? "").trim()}` ? `/portfolio/projects/${String(formData.get("id") ?? "").trim()}`
: "/root/portfolio/projects/new"; : "/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[] = [];
@@ -517,7 +518,7 @@ export async function saveProjectAction(formData: FormData) {
}); });
await revalidatePortfolioPages(); await revalidatePortfolioPages();
revalidatePath(`/root/portfolio/projects/${projectResult.project.id}`); revalidatePath(toInternalAdminPath(`/portfolio/projects/${projectResult.project.id}`));
revalidatePath(`/portfolio/${projectResult.project.slug}`); revalidatePath(`/portfolio/${projectResult.project.slug}`);
for (const locale of routing.locales) { for (const locale of routing.locales) {
@@ -525,7 +526,7 @@ export async function saveProjectAction(formData: FormData) {
} }
redirect( redirect(
withMessage(`/root/portfolio/projects/${projectResult.project.id}`, "success", "Projekt gespeichert."), withMessage(`/portfolio/projects/${projectResult.project.id}`, "success", "Projekt gespeichert."),
); );
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
@@ -578,7 +579,7 @@ export async function deleteProjectAction(formData: FormData) {
}); });
if (!project) { if (!project) {
redirect(withMessage("/root/portfolio", "error", "Project not found.")); redirect(withMessage("/portfolio", "error", "Project not found."));
} }
await prisma.portfolioProject.delete({ await prisma.portfolioProject.delete({
@@ -595,12 +596,12 @@ export async function deleteProjectAction(formData: FormData) {
revalidatePath(getLocalizedPath(locale, `/portfolio/${project.slug}`)); revalidatePath(getLocalizedPath(locale, `/portfolio/${project.slug}`));
} }
redirect(withMessage("/root/portfolio", "success", "Project deleted.")); redirect(withMessage("/portfolio", "success", "Project deleted."));
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
throw error; throw error;
} }
redirect(withMessage("/root/portfolio", "error", "Unable to delete project.")); redirect(withMessage("/portfolio", "error", "Unable to delete project."));
} }
} }
+2 -2
View File
@@ -24,14 +24,14 @@ const copy = {
export default async function RootPortfolioCategoriesPage() { export default async function RootPortfolioCategoriesPage() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
redirect("/root"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
const categories = await getAdminPortfolioCategories(); const categories = await getAdminPortfolioCategories();
+1 -1
View File
@@ -1,5 +1,5 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default function RootPortfolioMediaRedirectPage() { export default function RootPortfolioMediaRedirectPage() {
redirect("/root/media"); redirect("/media");
} }
+2 -2
View File
@@ -31,14 +31,14 @@ type RootPortfolioPageProps = {
export default async function RootPortfolioPage({ searchParams }: RootPortfolioPageProps) { export default async function RootPortfolioPage({ searchParams }: RootPortfolioPageProps) {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
redirect("/root"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
const selectedCategory = searchParams?.category && searchParams.category !== "__all__" const selectedCategory = searchParams?.category && searchParams.category !== "__all__"
+4 -4
View File
@@ -53,14 +53,14 @@ export default async function RootPortfolioProjectPage({
params, params,
}: RootPortfolioProjectPageProps) { }: RootPortfolioProjectPageProps) {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
redirect("/root"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
const [categories, mediaOptions, project] = await Promise.all([ const [categories, mediaOptions, project] = await Promise.all([
@@ -70,7 +70,7 @@ export default async function RootPortfolioProjectPage({
]); ]);
if (!project) { if (!project) {
redirect("/root/portfolio?error=Project+not+found."); redirect("/portfolio?error=Project+not+found.");
} }
return ( return (
@@ -90,7 +90,7 @@ export default async function RootPortfolioProjectPage({
mediaOptions={mediaOptions} mediaOptions={mediaOptions}
project={project} project={project}
formId="portfolio-project-form" formId="portfolio-project-form"
redirectPath={`/root/portfolio/projects/${project.id}`} redirectPath={`/portfolio/projects/${project.id}`}
/> />
</MotionFade> </MotionFade>
+3 -3
View File
@@ -26,14 +26,14 @@ const copy = {
export default async function RootNewPortfolioProjectPage() { export default async function RootNewPortfolioProjectPage() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
redirect("/root"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
const [categories, mediaOptions] = await Promise.all([ const [categories, mediaOptions] = await Promise.all([
@@ -57,7 +57,7 @@ export default async function RootNewPortfolioProjectPage() {
categories={categories} categories={categories}
mediaOptions={mediaOptions} mediaOptions={mediaOptions}
formId="portfolio-project-form" formId="portfolio-project-form"
redirectPath="/root/portfolio/projects/new" redirectPath="/portfolio/projects/new"
/> />
</MotionFade> </MotionFade>
</div> </div>
+2 -2
View File
@@ -31,14 +31,14 @@ export default async function RootPortfolioProjectsPage({
searchParams, searchParams,
}: RootPortfolioProjectsPageProps) { }: RootPortfolioProjectsPageProps) {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
redirect("/root"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
const selectedCategory = searchParams?.category && searchParams.category !== "__all__" const selectedCategory = searchParams?.category && searchParams.category !== "__all__"
+6 -5
View File
@@ -14,6 +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 { 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";
@@ -27,7 +28,7 @@ import { prisma } from "@/lib/prisma";
function ensureAdmin() { function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
} }
@@ -74,8 +75,8 @@ async function cleanupCreatedMedia(assetIds: string[], uploadedPaths: string[])
async function revalidateSiteSettingsPages() { async function revalidateSiteSettingsPages() {
revalidatePath("/", "layout"); revalidatePath("/", "layout");
revalidatePath("/root"); revalidatePath(toInternalAdminPath("/"));
revalidatePath("/root/site-settings"); revalidatePath(toInternalAdminPath("/site-settings"));
revalidatePath("/coming-soon"); revalidatePath("/coming-soon");
const publicPaths = ["/", "/about", "/portfolio", "/contact", "/success", "/coming-soon"]; const publicPaths = ["/", "/about", "/portfolio", "/contact", "/success", "/coming-soon"];
@@ -277,7 +278,7 @@ export async function saveSiteSettingsAction(formData: FormData) {
}); });
await revalidateSiteSettingsPages(); await revalidateSiteSettingsPages();
redirect(withMessage("/root/site-settings", "success", "Einstellungen gespeichert.")); redirect(withMessage("/site-settings", "success", "Einstellungen gespeichert."));
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
throw error; throw error;
@@ -290,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("/root/site-settings", "error", message)); redirect(withMessage("/site-settings", "error", message));
} }
} }
+2 -2
View File
@@ -31,14 +31,14 @@ const copy = {
export default async function RootSiteSettingsPage() { export default async function RootSiteSettingsPage() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
redirect("/root"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
const [siteSettings, mediaBindings, mediaOptions] = await Promise.all([ const [siteSettings, mediaBindings, mediaOptions] = await Promise.all([
+7 -6
View File
@@ -4,6 +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"; import { isRedirectError } from "next/dist/client/components/redirect";
import { 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 +17,7 @@ import type { MailSettings } from "@/lib/mail-settings";
function ensureAdmin() { function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
} }
@@ -77,8 +78,8 @@ export async function saveMailSettingsAction(formData: FormData) {
const nextMailSettings = parseMailSettingsFormData(formData, existingMailSettings); const nextMailSettings = parseMailSettingsFormData(formData, existingMailSettings);
await updateMailSettings(nextMailSettings); await updateMailSettings(nextMailSettings);
revalidatePath("/root/smtp"); revalidatePath(toInternalAdminPath("/smtp"));
redirect(withMessage("/root/smtp", "success", "SMTP Einstellungen gespeichert.")); redirect(withMessage("/smtp", "success", "SMTP Einstellungen gespeichert."));
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
throw error; throw error;
@@ -89,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("/root/smtp", "error", message)); redirect(withMessage("/smtp", "error", message));
} }
} }
@@ -98,7 +99,7 @@ export async function sendTestEmailAction() {
try { try {
await sendTestEmail(); await sendTestEmail();
redirect(withMessage("/root/smtp", "success", "Test-E-Mail gesendet.")); redirect(withMessage("/smtp", "success", "Test-E-Mail gesendet."));
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
throw error; throw error;
@@ -109,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("/root/smtp", "error", message)); redirect(withMessage("/smtp", "error", message));
} }
} }
+5 -4
View File
@@ -4,6 +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"; import { isRedirectError } from "next/dist/client/components/redirect";
import { 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 {
@@ -15,7 +16,7 @@ import type { ContactProtectionSettings } from "@/lib/contact-protection";
function ensureAdmin() { function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
} }
@@ -85,13 +86,13 @@ export async function saveContactProtectionSettingsAction(formData: FormData) {
const nextSettings = parseContactProtectionFormData(formData, existingSettings); const nextSettings = parseContactProtectionFormData(formData, existingSettings);
await updateContactProtectionSettings(nextSettings); await updateContactProtectionSettings(nextSettings);
revalidatePath("/root/smtp/contact-protection"); revalidatePath(toInternalAdminPath("/smtp/contact-protection"));
revalidatePath("/contact"); revalidatePath("/contact");
revalidatePath("/ar/contact"); revalidatePath("/ar/contact");
revalidatePath("/en/contact"); revalidatePath("/en/contact");
redirect( redirect(
withMessage( withMessage(
"/root/smtp/contact-protection", "/smtp/contact-protection",
"success", "success",
"Contact Protection gespeichert.", "Contact Protection gespeichert.",
), ),
@@ -106,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("/root/smtp/contact-protection", "error", message)); redirect(withMessage("/smtp/contact-protection", "error", message));
} }
} }
+2 -2
View File
@@ -27,14 +27,14 @@ const copy = {
export default async function RootSMTPProtectionPage() { export default async function RootSMTPProtectionPage() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
redirect("/root"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
const settings = await getContactProtectionFormValues(); const settings = await getContactProtectionFormValues();
+2 -2
View File
@@ -28,14 +28,14 @@ const copy = {
export default async function RootSMTPPage() { export default async function RootSMTPPage() {
if (!isAdminAuthenticated()) { if (!isAdminAuthenticated()) {
redirect("/root"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
const mailSettings = await getMailSettingsFormValues(); const mailSettings = await getMailSettingsFormValues();
+2 -2
View File
@@ -24,14 +24,14 @@ export default async function RootUiKitPage() {
const authenticated = isAdminAuthenticated(); const authenticated = isAdminAuthenticated();
if (!authenticated) { if (!authenticated) {
redirect("/root"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); clearAdminSessionCookie();
redirect("/root"); redirect("/");
} }
return ( return (
+3 -3
View File
@@ -27,10 +27,10 @@ export function AppSidebar({
items, items,
footer, footer,
}: AppSidebarProps) { }: AppSidebarProps) {
const portfolioItem = items.find((item) => item.href === "/root/portfolio"); const portfolioItem = items.find((item) => item.href === "/portfolio");
const systemOrder = ["/root", "/root/media", "/root/maintenance", "/root/ui-kit"]; const systemOrder = ["/", "/media", "/maintenance", "/ui-kit"];
const systemItems = items const systemItems = items
.filter((item) => item.href !== "/root/portfolio") .filter((item) => item.href !== "/portfolio")
.sort((left, right) => systemOrder.indexOf(left.href) - systemOrder.indexOf(right.href)); .sort((left, right) => systemOrder.indexOf(left.href) - systemOrder.indexOf(right.href));
function renderItem(item: SidebarItem, nested = false) { function renderItem(item: SidebarItem, nested = false) {
+2 -1
View File
@@ -2,6 +2,7 @@ import Link from "next/link";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import { Container } from "@/components/layout/container"; import { Container } from "@/components/layout/container";
import { buildAdminUrl } from "@/lib/admin-routing";
import { getLocalizedPath } from "@/lib/locale"; import { getLocalizedPath } from "@/lib/locale";
const navItems = [ const navItems = [
@@ -34,7 +35,7 @@ export function SiteFooter({ isAdmin = false }: SiteFooterProps) {
</Link> </Link>
))} ))}
{isAdmin ? ( {isAdmin ? (
<a href="/root" className="text-muted-foreground hover:text-foreground"> <a href={buildAdminUrl()} className="text-muted-foreground hover:text-foreground">
{tNav("root")} {tNav("root")}
</a> </a>
) : null} ) : null}
+3 -2
View File
@@ -13,6 +13,7 @@ import { SiteLogo } from "@/components/layout/site-logo";
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 { buildAdminUrl } from "@/lib/admin-routing";
import { getLocalizedPath, stripLocalePrefix } from "@/lib/locale"; import { getLocalizedPath, stripLocalePrefix } from "@/lib/locale";
import { toast } from "@/lib/toast"; import { toast } from "@/lib/toast";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -314,7 +315,7 @@ export function SiteHeader({
/> />
{isAdmin ? ( {isAdmin ? (
<Button asChild variant="ghost" size="icon" className={desktopControlButtonClassName}> <Button asChild variant="ghost" size="icon" className={desktopControlButtonClassName}>
<Link href="/root" aria-label={t("root")}> <Link href={buildAdminUrl()} aria-label={t("root")}>
<LayoutDashboard className="h-4 w-4 transition-transform duration-300 ease-out hover:scale-110" /> <LayoutDashboard className="h-4 w-4 transition-transform duration-300 ease-out hover:scale-110" />
</Link> </Link>
</Button> </Button>
@@ -464,7 +465,7 @@ export function SiteHeader({
/> />
{isAdmin ? ( {isAdmin ? (
<Button asChild variant="ghost" size="icon" className={mobileControlButtonClassName}> <Button asChild variant="ghost" size="icon" className={mobileControlButtonClassName}>
<Link href="/root" aria-label={t("root")} onClick={() => setIsOpen(false)}> <Link href={buildAdminUrl()} aria-label={t("root")} onClick={() => setIsOpen(false)}>
<LayoutDashboard className="h-4 w-4 transition-transform duration-300 ease-out hover:scale-110" /> <LayoutDashboard className="h-4 w-4 transition-transform duration-300 ease-out hover:scale-110" />
</Link> </Link>
</Button> </Button>
@@ -159,7 +159,7 @@ function CategoryForm({
return ( return (
<form id={formId} action={action} className="space-y-6"> <form id={formId} action={action} className="space-y-6">
{categoryId ? <input type="hidden" name="id" value={categoryId} /> : null} {categoryId ? <input type="hidden" name="id" value={categoryId} /> : null}
<input type="hidden" name="redirectPath" value="/root/portfolio/categories" /> <input type="hidden" name="redirectPath" value="/portfolio/categories" />
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
@@ -281,7 +281,7 @@ function EditCategoryDialog({
<div className="flex w-full flex-col-reverse gap-2 sm:w-auto sm:flex-row"> <div className="flex w-full flex-col-reverse gap-2 sm:w-auto sm:flex-row">
<form action={removeCategoryAction}> <form action={removeCategoryAction}>
<input type="hidden" name="id" value={category.id} /> <input type="hidden" name="id" value={category.id} />
<input type="hidden" name="redirectPath" value="/root/portfolio/categories" /> <input type="hidden" name="redirectPath" value="/portfolio/categories" />
<Button type="submit" variant="destructive" disabled={category.projectCount > 0}> <Button type="submit" variant="destructive" disabled={category.projectCount > 0}>
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
{copy.delete} {copy.delete}
@@ -40,7 +40,7 @@ export function PortfolioProjectActions({ projectId }: { projectId: string }) {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link href={`/root/portfolio/projects/${projectId}`}> <Link href={`/portfolio/projects/${projectId}`}>
<FolderKanban className="mr-2 h-4 w-4" /> <FolderKanban className="mr-2 h-4 w-4" />
{copy.editProject} {copy.editProject}
</Link> </Link>
@@ -46,13 +46,13 @@ export function PortfolioProjectsOverview({
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button asChild> <Button asChild>
<Link href="/root/portfolio/projects/new"> <Link href="/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="/root/portfolio/categories"> <Link href="/portfolio/categories">
<Tags className="h-4 w-4" /> <Tags className="h-4 w-4" />
{copy.newCategory} {copy.newCategory}
</Link> </Link>
@@ -62,7 +62,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="/root/portfolio">{copy.all}</Link> <Link href="/portfolio">{copy.all}</Link>
</Button> </Button>
{categories.map((category) => ( {categories.map((category) => (
<Button <Button
@@ -70,7 +70,7 @@ export function PortfolioProjectsOverview({
asChild asChild
variant={selectedCategory === category.id ? "default" : "outline"} variant={selectedCategory === category.id ? "default" : "outline"}
> >
<Link href={`/root/portfolio?category=${category.id}`}> <Link href={`/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>
+2 -2
View File
@@ -12,12 +12,12 @@ const items = [
{ {
key: "projects", key: "projects",
label: "Projekte", label: "Projekte",
href: "/root/portfolio", href: "/portfolio",
}, },
{ {
key: "categories", key: "categories",
label: "Kategorien", label: "Kategorien",
href: "/root/portfolio/categories", href: "/portfolio/categories",
}, },
] as const; ] as const;
+8 -8
View File
@@ -19,8 +19,8 @@ import { SidebarMaintenanceControl } from "@/components/root/sidebar-maintenance
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 { getMaintenanceMode, getSiteSettingsMediaBindings } from "@/lib/app-config"; import { getMaintenanceMode, getSiteSettingsMediaBindings } from "@/lib/app-config";
import { getLocalizedPath } from "@/lib/locale";
import { getRootNavigation } from "@/lib/root-navigation"; import { getRootNavigation } from "@/lib/root-navigation";
import { updateMaintenanceModeAction } from "@/app/root/maintenance/actions"; import { updateMaintenanceModeAction } from "@/app/root/maintenance/actions";
@@ -75,12 +75,12 @@ export async function RootDashboardShell({
const sidebarItems = getRootNavigation(copy, active, smtpChild, portfolioChild); const sidebarItems = getRootNavigation(copy, active, smtpChild, portfolioChild);
const normalizedSidebarItems = sidebarItems.filter( const normalizedSidebarItems = sidebarItems.filter(
(item) => (item) =>
item.href !== "/root/maintenance" && item.href !== "/maintenance" &&
item.href !== "/root/ui-kit" && item.href !== "/ui-kit" &&
item.href !== "/root/smtp" && item.href !== "/smtp" &&
item.href !== "/root/marquee", item.href !== "/marquee",
); );
const footerSidebarItems = ["/root/marquee", "/root/smtp"] const footerSidebarItems = ["/marquee", "/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 =
@@ -110,7 +110,7 @@ export async function RootDashboardShell({
icon={headerIcon} icon={headerIcon}
items={normalizedSidebarItems} items={normalizedSidebarItems}
sidebarIconSrc={mediaBindings.favicon?.url} sidebarIconSrc={mediaBindings.favicon?.url}
sidebarBrandHref={getLocalizedPath("de")} sidebarBrandHref={buildSiteUrl()}
sidebarBrandHoverLabel={copy.backToSite} sidebarBrandHoverLabel={copy.backToSite}
sidebarTop={sidebarTopContent} sidebarTop={sidebarTopContent}
sidebarFooterItems={footerSidebarItems} sidebarFooterItems={footerSidebarItems}
@@ -126,7 +126,7 @@ export async function RootDashboardShell({
variant={active === "ui-kit" ? "default" : "outline"} variant={active === "ui-kit" ? "default" : "outline"}
className="w-full justify-between" className="w-full justify-between"
> >
<Link href="/root/ui-kit"> <Link href="/ui-kit">
{copy.uiKit} {copy.uiKit}
<SwatchBook className="h-4 w-4" /> <SwatchBook className="h-4 w-4" />
</Link> </Link>
+2 -1
View File
@@ -9,6 +9,7 @@ services:
environment: environment:
NODE_ENV: production NODE_ENV: production
NEXT_TELEMETRY_DISABLED: "1" NEXT_TELEMETRY_DISABLED: "1"
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL:-https://root.mohfarawati.de}
DATABASE_URL: postgresql://postgres:postgres@db:5432/moh_sass?schema=public DATABASE_URL: postgresql://postgres:postgres@db:5432/moh_sass?schema=public
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-me} ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-me}
ADMIN_AUTH_SECRET: ${ADMIN_AUTH_SECRET:-change-me-long-secret} ADMIN_AUTH_SECRET: ${ADMIN_AUTH_SECRET:-change-me-long-secret}
@@ -26,7 +27,7 @@ services:
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.docker.network=proxy" - "traefik.docker.network=proxy"
- "traefik.http.routers.sass-mohfarawati.rule=Host(`mohfarawati.de`) || Host(`www.mohfarawati.de`)" - "traefik.http.routers.sass-mohfarawati.rule=Host(`mohfarawati.de`) || Host(`www.mohfarawati.de`) || Host(`root.mohfarawati.de`)"
- "traefik.http.routers.sass-mohfarawati.entrypoints=websecure" - "traefik.http.routers.sass-mohfarawati.entrypoints=websecure"
- "traefik.http.routers.sass-mohfarawati.tls=true" - "traefik.http.routers.sass-mohfarawati.tls=true"
- "traefik.http.routers.sass-mohfarawati.tls.certresolver=cf" - "traefik.http.routers.sass-mohfarawati.tls.certresolver=cf"
+67
View File
@@ -0,0 +1,67 @@
const DEFAULT_ADMIN_HOST = "root.mohfarawati.de";
const DEFAULT_ADMIN_URL = `https://${DEFAULT_ADMIN_HOST}`;
const DEFAULT_SITE_URL = "https://mohfarawati.de";
export const INTERNAL_ADMIN_PREFIX = "/_admin";
function normalizePathname(pathname: string): string {
if (!pathname || pathname === "/") {
return "/";
}
return pathname.startsWith("/") ? pathname : `/${pathname}`;
}
function normalizeBaseUrl(url: string): string {
return url.replace(/\/+$/, "");
}
export function getAdminHost(): string {
return (process.env.ADMIN_HOST ?? DEFAULT_ADMIN_HOST).trim().toLowerCase();
}
export function getRequestHostname(hostHeader?: string | null): string {
return (hostHeader ?? "")
.split(",")[0]
?.trim()
.toLowerCase()
.replace(/:\d+$/, "") ?? "";
}
export function isAdminHost(hostname: string): boolean {
return hostname === getAdminHost();
}
export function isLegacyAdminPath(pathname: string): boolean {
return pathname === "/root" || pathname.startsWith("/root/");
}
export function isInternalAdminPath(pathname: string): boolean {
return pathname === INTERNAL_ADMIN_PREFIX || pathname.startsWith(`${INTERNAL_ADMIN_PREFIX}/`);
}
export function toInternalAdminPath(pathname: string): string {
const normalizedPathname = normalizePathname(pathname);
if (normalizedPathname === "/") {
return INTERNAL_ADMIN_PREFIX;
}
return `${INTERNAL_ADMIN_PREFIX}${normalizedPathname}`;
}
export function getAdminBaseUrl(): string {
return normalizeBaseUrl(process.env.NEXT_PUBLIC_ADMIN_URL ?? DEFAULT_ADMIN_URL);
}
export function buildAdminUrl(pathname = "/"): string {
return `${getAdminBaseUrl()}${normalizePathname(pathname)}`;
}
export function getSiteBaseUrl(): string {
return normalizeBaseUrl(process.env.NEXT_PUBLIC_SITE_URL ?? DEFAULT_SITE_URL);
}
export function buildSiteUrl(pathname = "/"): string {
return `${getSiteBaseUrl()}${normalizePathname(pathname)}`;
}
+13 -13
View File
@@ -42,56 +42,56 @@ export function getRootNavigation(
return [ return [
{ {
label: copy.overview, label: copy.overview,
href: "/root", href: "/",
icon: LayoutDashboard, icon: LayoutDashboard,
active: active === "overview", active: active === "overview",
}, },
{ {
label: copy.maintenance, label: copy.maintenance,
href: "/root/maintenance", href: "/maintenance",
icon: ShieldAlert, icon: ShieldAlert,
active: active === "maintenance", active: active === "maintenance",
}, },
{ {
label: copy.uiKit, label: copy.uiKit,
href: "/root/ui-kit", href: "/ui-kit",
icon: SwatchBook, icon: SwatchBook,
active: active === "ui-kit", active: active === "ui-kit",
}, },
{ {
label: copy.media, label: copy.media,
href: "/root/media", href: "/media",
icon: ImageIcon, icon: ImageIcon,
active: active === "media", active: active === "media",
}, },
{ {
label: copy.siteSettings, label: copy.siteSettings,
href: "/root/site-settings", href: "/site-settings",
icon: Globe2, icon: Globe2,
active: active === "site-settings", active: active === "site-settings",
}, },
{ {
label: copy.marquee ?? "Marquee", label: copy.marquee ?? "Marquee",
href: "/root/marquee", href: "/marquee",
icon: Type, icon: Type,
active: active === "marquee", active: active === "marquee",
}, },
{ {
label: copy.smtp ?? "SMTP", label: copy.smtp ?? "SMTP",
href: "/root/smtp", href: "/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: "/root/smtp", href: "/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: "/root/smtp/contact-protection", href: "/smtp/contact-protection",
icon: ShieldAlert, icon: ShieldAlert,
active: smtpChild === "contact-protection", active: smtpChild === "contact-protection",
}, },
@@ -99,26 +99,26 @@ export function getRootNavigation(
}, },
{ {
label: copy.portfolio, label: copy.portfolio,
href: "/root/portfolio", href: "/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: "/root/portfolio", href: "/portfolio",
icon: FolderKanban, icon: FolderKanban,
active: portfolioChild === "overview" || portfolioChild === "projects", active: portfolioChild === "overview" || portfolioChild === "projects",
}, },
{ {
label: "Add Project", label: "Add Project",
href: "/root/portfolio/projects/new", href: "/portfolio/projects/new",
icon: PlusSquare, icon: PlusSquare,
active: portfolioChild === "new-project", active: portfolioChild === "new-project",
}, },
{ {
label: "Add Category", label: "Add Category",
href: "/root/portfolio/categories", href: "/portfolio/categories",
icon: Tags, icon: Tags,
active: portfolioChild === "categories", active: portfolioChild === "categories",
}, },
+28 -10
View File
@@ -3,6 +3,13 @@ import { NextResponse } from "next/server";
import type { NextRequest } from "next/server"; import type { NextRequest } from "next/server";
import { routing } from "./i18n/routing"; import { routing } from "./i18n/routing";
import {
getRequestHostname,
isAdminHost,
isInternalAdminPath,
isLegacyAdminPath,
toInternalAdminPath,
} from "./lib/admin-routing";
const intlMiddleware = createMiddleware(routing); const intlMiddleware = createMiddleware(routing);
@@ -41,20 +48,22 @@ function isRootBasicAuthValid(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 isRootBaseRoute = pathname === "/root" || pathname.startsWith("/root/"); const hostname = getRequestHostname(
const isRootRoute = isRootBaseRoute; request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? request.nextUrl.hostname,
);
const isAdminRequest = isAdminHost(hostname);
const rootRobotsHeaders = { const rootRobotsHeaders = {
"X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet, noimageindex", "X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet, noimageindex",
}; };
if (pathname === "/de" || pathname.startsWith("/de/")) { if (isInternalAdminPath(pathname) || isLegacyAdminPath(pathname)) {
const redirectUrl = request.nextUrl.clone(); return new NextResponse("Not Found", {
const nextPath = pathname.slice(3) || "/"; status: 404,
redirectUrl.pathname = nextPath; });
return NextResponse.redirect(redirectUrl, 308);
} }
if (isRootRoute && isRootBasicAuthConfigured() && !isRootBasicAuthValid(request)) { if (isAdminRequest) {
if (isRootBasicAuthConfigured() && !isRootBasicAuthValid(request)) {
return new NextResponse("Authentication required", { return new NextResponse("Authentication required", {
status: 401, status: 401,
headers: { headers: {
@@ -64,12 +73,21 @@ export default async function middleware(request: NextRequest) {
}); });
} }
if (isRootBaseRoute) { const rewriteUrl = request.nextUrl.clone();
const response = NextResponse.next(); rewriteUrl.pathname = toInternalAdminPath(pathname);
const response = NextResponse.rewrite(rewriteUrl);
response.headers.set("X-Robots-Tag", rootRobotsHeaders["X-Robots-Tag"]); response.headers.set("X-Robots-Tag", rootRobotsHeaders["X-Robots-Tag"]);
return response; return response;
} }
if (pathname === "/de" || pathname.startsWith("/de/")) {
const redirectUrl = request.nextUrl.clone();
const nextPath = pathname.slice(3) || "/";
redirectUrl.pathname = nextPath;
return NextResponse.redirect(redirectUrl, 308);
}
return intlMiddleware(request); return intlMiddleware(request);
} }