diff --git a/app/(dashboard)/access-lists/page.tsx b/app/(dashboard)/access-lists/page.tsx
index 9c913d89..2507efbe 100644
--- a/app/(dashboard)/access-lists/page.tsx
+++ b/app/(dashboard)/access-lists/page.tsx
@@ -1,7 +1,9 @@
import AccessListsClient from "./AccessListsClient";
import { listAccessLists } from "@/src/lib/models/access-lists";
+import { requireAdmin } from "@/src/lib/auth";
export default async function AccessListsPage() {
+ await requireAdmin();
const lists = await listAccessLists();
return ;
}
diff --git a/app/(dashboard)/audit-log/page.tsx b/app/(dashboard)/audit-log/page.tsx
index 16ec157f..2ca51a01 100644
--- a/app/(dashboard)/audit-log/page.tsx
+++ b/app/(dashboard)/audit-log/page.tsx
@@ -1,8 +1,10 @@
import AuditLogClient from "./AuditLogClient";
import { listAuditEvents } from "@/src/lib/models/audit";
import { listUsers } from "@/src/lib/models/user";
+import { requireAdmin } from "@/src/lib/auth";
export default async function AuditLogPage() {
+ await requireAdmin();
const events = await listAuditEvents(200);
const users = await listUsers();
const userMap = new Map(users.map((user) => [user.id, user]));
diff --git a/app/(dashboard)/certificates/page.tsx b/app/(dashboard)/certificates/page.tsx
index 8578d2df..fa03a2d6 100644
--- a/app/(dashboard)/certificates/page.tsx
+++ b/app/(dashboard)/certificates/page.tsx
@@ -1,7 +1,9 @@
import CertificatesClient from "./CertificatesClient";
import { listCertificates } from "@/src/lib/models/certificates";
+import { requireAdmin } from "@/src/lib/auth";
export default async function CertificatesPage() {
+ await requireAdmin();
const certificates = await listCertificates();
return ;
}
diff --git a/app/(dashboard)/dead-hosts/page.tsx b/app/(dashboard)/dead-hosts/page.tsx
index 2fdf8e31..b608679d 100644
--- a/app/(dashboard)/dead-hosts/page.tsx
+++ b/app/(dashboard)/dead-hosts/page.tsx
@@ -1,7 +1,9 @@
import DeadHostsClient from "./DeadHostsClient";
import { listDeadHosts } from "@/src/lib/models/dead-hosts";
+import { requireAdmin } from "@/src/lib/auth";
export default async function DeadHostsPage() {
+ await requireAdmin();
const hosts = await listDeadHosts();
return ;
}
diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx
index bde3fb40..498f2dd0 100644
--- a/app/(dashboard)/layout.tsx
+++ b/app/(dashboard)/layout.tsx
@@ -1,8 +1,8 @@
import type { ReactNode } from "react";
-import { requireUser } from "@/src/lib/auth";
+import { requireAdmin } from "@/src/lib/auth";
import DashboardLayoutClient from "./DashboardLayoutClient";
export default async function DashboardLayout({ children }: { children: ReactNode }) {
- const session = await requireUser();
+ const session = await requireAdmin();
return {children};
}
diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx
index 67990643..7d465b08 100644
--- a/app/(dashboard)/page.tsx
+++ b/app/(dashboard)/page.tsx
@@ -1,5 +1,5 @@
import db, { toIso } from "@/src/lib/db";
-import { requireUser } from "@/src/lib/auth";
+import { requireAdmin } from "@/src/lib/auth";
import OverviewClient from "./OverviewClient";
import {
accessLists,
@@ -43,7 +43,7 @@ async function loadStats(): Promise {
}
export default async function OverviewPage() {
- const session = await requireUser();
+ const session = await requireAdmin();
const stats = await loadStats();
const recentEvents = await db
.select({
diff --git a/app/(dashboard)/proxy-hosts/page.tsx b/app/(dashboard)/proxy-hosts/page.tsx
index 8430606b..078a3ad1 100644
--- a/app/(dashboard)/proxy-hosts/page.tsx
+++ b/app/(dashboard)/proxy-hosts/page.tsx
@@ -3,8 +3,10 @@ import { listProxyHosts } from "@/src/lib/models/proxy-hosts";
import { listCertificates } from "@/src/lib/models/certificates";
import { listAccessLists } from "@/src/lib/models/access-lists";
import { getAuthentikSettings } from "@/src/lib/settings";
+import { requireAdmin } from "@/src/lib/auth";
export default async function ProxyHostsPage() {
+ await requireAdmin();
const [hosts, certificates, accessLists, authentikDefaults] = await Promise.all([
listProxyHosts(),
listCertificates(),
diff --git a/app/(dashboard)/redirects/page.tsx b/app/(dashboard)/redirects/page.tsx
index 71f6a8f7..973ee93c 100644
--- a/app/(dashboard)/redirects/page.tsx
+++ b/app/(dashboard)/redirects/page.tsx
@@ -1,7 +1,9 @@
import RedirectsClient from "./RedirectsClient";
import { listRedirectHosts } from "@/src/lib/models/redirect-hosts";
+import { requireAdmin } from "@/src/lib/auth";
export default async function RedirectsPage() {
+ await requireAdmin();
const redirects = await listRedirectHosts();
return ;
}
diff --git a/middleware.ts b/middleware.ts
new file mode 100644
index 00000000..5ae63ab8
--- /dev/null
+++ b/middleware.ts
@@ -0,0 +1,42 @@
+import { auth } from "@/src/lib/auth";
+import { NextResponse } from "next/server";
+
+/**
+ * Next.js Middleware for route protection.
+ * Provides defense-in-depth by checking authentication at the edge
+ * before requests reach page components.
+ *
+ * Note: Uses Node.js runtime because auth requires database access.
+ */
+export const runtime = 'nodejs';
+
+export default auth((req) => {
+ const isAuthenticated = !!req.auth;
+ const pathname = req.nextUrl.pathname;
+
+ // Allow public routes
+ if (pathname === "/login" || pathname.startsWith("/api/auth") || pathname === "/api/health") {
+ return NextResponse.next();
+ }
+
+ // Redirect unauthenticated users to login
+ if (!isAuthenticated && !pathname.startsWith("/login")) {
+ const loginUrl = new URL("/login", req.url);
+ return NextResponse.redirect(loginUrl);
+ }
+
+ return NextResponse.next();
+});
+
+export const config = {
+ matcher: [
+ /*
+ * Match all request paths except for the ones starting with:
+ * - _next/static (static files)
+ * - _next/image (image optimization files)
+ * - favicon.ico (favicon file)
+ * - public folder
+ */
+ "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
+ ],
+};
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index 6444655b..79b990ef 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -78,7 +78,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
if (user) {
token.id = user.id;
token.email = user.email;
- token.role = "admin";
+ token.role = user.role ?? "user";
}
return token;
},
diff --git a/src/lib/init-db.ts b/src/lib/init-db.ts
index 33eb5f50..de5cfab9 100644
--- a/src/lib/init-db.ts
+++ b/src/lib/init-db.ts
@@ -9,6 +9,8 @@ import { eq } from "drizzle-orm";
* This is called during application startup.
* The password from environment variables is hashed and stored securely.
*/
+
+//Todo: this could probably be handled better, especially for the adminid.
export async function ensureAdminUser(): Promise {
const adminId = 1; // Must match the hardcoded ID in auth.ts
const adminEmail = `${config.adminUsername}@localhost`;
@@ -26,6 +28,7 @@ export async function ensureAdminUser(): Promise {
if (existingUser) {
// Admin user exists, update credentials if needed
// Always update password hash to handle password changes in env vars
+ // Also ensure role is always "admin" for the primary admin user
const now = nowIso();
await db
.update(users)
@@ -33,6 +36,7 @@ export async function ensureAdminUser(): Promise {
email: adminEmail,
subject,
passwordHash,
+ role: "admin",
updatedAt: now
})
.where(eq(users.id, adminId));