enforce admin role by reading user role instead of hardcoding

This commit is contained in:
fuomag9
2025-11-19 18:06:24 +01:00
parent bc3e28d5ab
commit 7ae51ad034
11 changed files with 63 additions and 5 deletions

View File

@@ -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 <AccessListsClient lists={lists} />;
}

View File

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

View File

@@ -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 <CertificatesClient certificates={certificates} />;
}

View File

@@ -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 <DeadHostsClient hosts={hosts} />;
}

View File

@@ -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 <DashboardLayoutClient user={session.user}>{children}</DashboardLayoutClient>;
}

View File

@@ -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<StatCard[]> {
}
export default async function OverviewPage() {
const session = await requireUser();
const session = await requireAdmin();
const stats = await loadStats();
const recentEvents = await db
.select({

View File

@@ -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(),

View File

@@ -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 <RedirectsClient redirects={redirects} />;
}

42
middleware.ts Normal file
View File

@@ -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)$).*)",
],
};

View File

@@ -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;
},

View File

@@ -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<void> {
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<void> {
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<void> {
email: adminEmail,
subject,
passwordHash,
role: "admin",
updatedAt: now
})
.where(eq(users.id, adminId));