Refactor admin area and remove legacy root paths
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-13 16:14:04 +01:00
parent e32ac4cacb
commit db897e22bf
65 changed files with 216 additions and 140 deletions
+71
View File
@@ -15,6 +15,75 @@ function getPassword(): string {
return process.env.ADMIN_PASSWORD ?? "";
}
function parseHostname(value: string | undefined): string | undefined {
if (!value) {
return undefined;
}
const trimmed = value.trim().toLowerCase();
if (!trimmed) {
return undefined;
}
try {
return new URL(trimmed).hostname.toLowerCase();
} catch {
return trimmed.replace(/^https?:\/\//, "").split("/")[0]?.replace(/:\d+$/, "") || undefined;
}
}
function isCookieDomainCandidate(hostname: string | undefined): hostname is string {
return Boolean(
hostname &&
hostname !== "localhost" &&
!hostname.endsWith(".localhost") &&
!/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) &&
!hostname.includes(":"),
);
}
function getSharedCookieHostname(hostnames: string[]): string | undefined {
const validHostnames = hostnames.filter(isCookieDomainCandidate);
if (validHostnames.length === 0) {
return undefined;
}
if (validHostnames.length === 1) {
return validHostnames[0];
}
const reversedSegments = validHostnames.map((hostname) => hostname.split(".").reverse());
const sharedSegments: string[] = [];
for (let index = 0; index < reversedSegments[0].length; index += 1) {
const segment = reversedSegments[0][index];
if (!segment || reversedSegments.some((parts) => parts[index] !== segment)) {
break;
}
sharedSegments.push(segment);
}
if (sharedSegments.length < 2) {
return validHostnames[0];
}
return sharedSegments.reverse().join(".");
}
function getAdminCookieDomain(): string | undefined {
const hostname = getSharedCookieHostname([
parseHostname(process.env.NEXT_PUBLIC_SITE_URL),
parseHostname(process.env.NEXT_PUBLIC_ADMIN_URL),
parseHostname(process.env.ADMIN_HOST),
].filter((value): value is string => Boolean(value)));
return hostname ? `.${hostname}` : undefined;
}
function signValue(value: string): string {
return createHmac("sha256", getSecret()).update(value).digest("hex");
}
@@ -71,6 +140,7 @@ export async function setAdminSessionCookie(): Promise<void> {
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
domain: getAdminCookieDomain(),
maxAge: 60 * 60 * 8,
});
}
@@ -82,6 +152,7 @@ export async function clearAdminSessionCookie(): Promise<void> {
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
domain: getAdminCookieDomain(),
maxAge: 0,
});
}
@@ -12,7 +12,7 @@ import {
type LucideIcon,
} from "lucide-react";
type RootNavigationCopy = {
type AdminNavigationCopy = {
overview: string;
maintenance: string;
uiKit: string;
@@ -24,21 +24,21 @@ type RootNavigationCopy = {
contactProtection?: string;
};
export type RootNavItem = {
export type AdminNavItem = {
label: string;
href: string;
icon: LucideIcon;
active?: boolean;
expanded?: boolean;
children?: RootNavItem[];
children?: AdminNavItem[];
};
export function getRootNavigation(
copy: RootNavigationCopy,
export function getAdminNavigation(
copy: AdminNavigationCopy,
active: "overview" | "maintenance" | "ui-kit" | "portfolio" | "media" | "site-settings" | "smtp" | "marquee",
smtpChild?: "settings" | "contact-protection",
portfolioChild?: "overview" | "projects" | "new-project" | "categories",
): RootNavItem[] {
): AdminNavItem[] {
return [
{
label: copy.overview,