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));