- Root Access
+ Admin Access
{title}
{description}
diff --git a/components/layout/site-footer.tsx b/components/layout/site-footer.tsx
index 9699176..48cef6d 100644
--- a/components/layout/site-footer.tsx
+++ b/components/layout/site-footer.tsx
@@ -36,7 +36,7 @@ export function SiteFooter({ isAdmin = false }: SiteFooterProps) {
))}
{isAdmin ? (
- {tNav("root")}
+ {tNav("admin")}
) : null}
diff --git a/components/layout/site-header.tsx b/components/layout/site-header.tsx
index d571a0d..d58ede6 100644
--- a/components/layout/site-header.tsx
+++ b/components/layout/site-header.tsx
@@ -315,7 +315,7 @@ export function SiteHeader({
/>
{isAdmin ? (
@@ -465,7 +465,7 @@ export function SiteHeader({
/>
{isAdmin ? (
diff --git a/docker-compose.yml b/docker-compose.yml
index 17efeb0..98b87cb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -13,8 +13,8 @@ services:
DATABASE_URL: postgresql://postgres:postgres@db:5432/moh_sass?schema=public
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-me}
ADMIN_AUTH_SECRET: ${ADMIN_AUTH_SECRET:-change-me-long-secret}
- ROOT_BASIC_AUTH_USER: ${ROOT_BASIC_AUTH_USER:-root}
- ROOT_BASIC_AUTH_PASS: ${ROOT_BASIC_AUTH_PASS:-change-me-root}
+ ADMIN_BASIC_AUTH_USER: ${ADMIN_BASIC_AUTH_USER:-root}
+ ADMIN_BASIC_AUTH_PASS: ${ADMIN_BASIC_AUTH_PASS:-change-me-root}
depends_on:
db:
condition: service_healthy
diff --git a/lib/admin-auth.ts b/lib/admin-auth.ts
index 840ac7e..0ba92e1 100644
--- a/lib/admin-auth.ts
+++ b/lib/admin-auth.ts
@@ -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 {
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
+ domain: getAdminCookieDomain(),
maxAge: 60 * 60 * 8,
});
}
@@ -82,6 +152,7 @@ export async function clearAdminSessionCookie(): Promise {
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
+ domain: getAdminCookieDomain(),
maxAge: 0,
});
}
diff --git a/lib/root-navigation.ts b/lib/admin-navigation.ts
similarity index 94%
rename from lib/root-navigation.ts
rename to lib/admin-navigation.ts
index f31487c..ba45c9a 100644
--- a/lib/root-navigation.ts
+++ b/lib/admin-navigation.ts
@@ -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,
diff --git a/messages/ar.json b/messages/ar.json
index bc4220b..28332c5 100644
--- a/messages/ar.json
+++ b/messages/ar.json
@@ -6,7 +6,7 @@
"about": "معلومات عني",
"contact": "تواصل معي",
"soon": "قــريــبــاً",
- "root": "Root",
+ "admin": "Admin",
"openMenu": "فتح القائمة",
"closeMenu": "إغلاق القائمة",
"themeToggle": "تبديل المظهر",
diff --git a/messages/de.json b/messages/de.json
index 46a6c2b..a2001e0 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -6,7 +6,7 @@
"about": "Über mich",
"contact": "Kontakt",
"soon": "Bald",
- "root": "Root",
+ "admin": "Admin",
"openMenu": "Menü öffnen",
"closeMenu": "Menü schliessen",
"themeToggle": "Theme wechseln",
diff --git a/messages/en.json b/messages/en.json
index 248c12e..569a78f 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -6,7 +6,7 @@
"about": "About Me",
"contact": "Contact",
"soon": "Soon",
- "root": "Root",
+ "admin": "Admin",
"openMenu": "Open menu",
"closeMenu": "Close menu",
"themeToggle": "Toggle theme",
diff --git a/middleware.ts b/middleware.ts
index c70006d..daa4ada 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -13,12 +13,20 @@ import {
const intlMiddleware = createMiddleware(routing);
-function isRootBasicAuthConfigured(): boolean {
- return Boolean(process.env.ROOT_BASIC_AUTH_USER && process.env.ROOT_BASIC_AUTH_PASS);
+function getAdminBasicAuthUser(): string {
+ return process.env.ADMIN_BASIC_AUTH_USER ?? "";
}
-function isRootBasicAuthValid(request: NextRequest): boolean {
- if (!isRootBasicAuthConfigured()) {
+function getAdminBasicAuthPass(): string {
+ return process.env.ADMIN_BASIC_AUTH_PASS ?? "";
+}
+
+function isAdminBasicAuthConfigured(): boolean {
+ return Boolean(getAdminBasicAuthUser() && getAdminBasicAuthPass());
+}
+
+function isAdminBasicAuthValid(request: NextRequest): boolean {
+ if (!isAdminBasicAuthConfigured()) {
return false;
}
@@ -37,10 +45,7 @@ function isRootBasicAuthValid(request: NextRequest): boolean {
const user = decoded.slice(0, index);
const pass = decoded.slice(index + 1);
- return (
- user === process.env.ROOT_BASIC_AUTH_USER &&
- pass === process.env.ROOT_BASIC_AUTH_PASS
- );
+ return user === getAdminBasicAuthUser() && pass === getAdminBasicAuthPass();
} catch {
return false;
}
@@ -52,7 +57,7 @@ export default async function middleware(request: NextRequest) {
request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? request.nextUrl.hostname,
);
const isAdminRequest = isAdminHost(hostname);
- const rootRobotsHeaders = {
+ const adminRobotsHeaders = {
"X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet, noimageindex",
};
@@ -63,12 +68,12 @@ export default async function middleware(request: NextRequest) {
}
if (isAdminRequest) {
- if (isRootBasicAuthConfigured() && !isRootBasicAuthValid(request)) {
+ if (isAdminBasicAuthConfigured() && !isAdminBasicAuthValid(request)) {
return new NextResponse("Authentication required", {
status: 401,
headers: {
- "WWW-Authenticate": 'Basic realm="Root Area", charset="UTF-8"',
- ...rootRobotsHeaders,
+ "WWW-Authenticate": 'Basic realm="Admin Area", charset="UTF-8"',
+ ...adminRobotsHeaders,
},
});
}
@@ -77,7 +82,7 @@ export default async function middleware(request: NextRequest) {
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", adminRobotsHeaders["X-Robots-Tag"]);
return response;
}