From 9a7191f4c1b3351f3f4c238623016dbe95512096 Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Wed, 13 May 2026 21:20:24 +0200 Subject: [PATCH 1/3] fix(proxy): merge middleware.ts into proxy.ts for Next.js 16 compat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next.js 16 staat alleen proxy.ts toe als de twee co-existeren; build faalt met "Both middleware file and proxy file are detected". De CSP- en CSRF-logica uit middleware.ts is samengevoegd in proxy.ts en de auth-redirect blijft. CSRF-validatie geldt nu alleen voor POST /api/*, auth-redirect alleen buiten /api — matcher uitgebreid om beide te dekken. Co-Authored-By: Claude Opus 4.7 (1M context) --- middleware.ts | 55 --------------------------------------------- proxy.ts | 62 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 64 deletions(-) delete mode 100644 middleware.ts diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index 99f0bae..0000000 --- a/middleware.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -const CSP = [ - "default-src 'self'", - "script-src 'self' 'unsafe-inline'", - "style-src 'self' 'unsafe-inline'", - "font-src 'self'", - "img-src 'self' data:", - "connect-src 'self'", - "frame-ancestors 'none'", - "base-uri 'self'", - "form-action 'self'", -].join('; ') - -const CSRF_COOKIE = 'csrf_token' -const CSRF_HEADER = 'x-csrf-token' - -export function middleware(request: NextRequest) { - const { method, nextUrl } = request - - // Validate CSRF token on all POST requests to API routes - if (method === 'POST' && nextUrl.pathname.startsWith('/api/')) { - const cookieToken = request.cookies.get(CSRF_COOKIE)?.value - const headerToken = request.headers.get(CSRF_HEADER) - if (!cookieToken || cookieToken !== headerToken) { - return new NextResponse( - JSON.stringify({ error: 'CSRF validation failed' }), - { status: 403, headers: { 'Content-Type': 'application/json' } }, - ) - } - } - - const response = NextResponse.next() - - response.headers.set('Content-Security-Policy', CSP) - response.headers.set('X-Frame-Options', 'DENY') - response.headers.set('X-Content-Type-Options', 'nosniff') - response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin') - - // Issue a CSRF token cookie on GET requests when not yet present - if (method === 'GET' && !request.cookies.get(CSRF_COOKIE)) { - response.cookies.set(CSRF_COOKIE, crypto.randomUUID(), { - httpOnly: false, // must be readable by client JS for the double-submit pattern - sameSite: 'strict', - secure: process.env.NODE_ENV === 'production', - path: '/', - }) - } - - return response -} - -export const config = { - matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], -} diff --git a/proxy.ts b/proxy.ts index e2b24d3..2f902de 100644 --- a/proxy.ts +++ b/proxy.ts @@ -2,22 +2,66 @@ import { NextRequest, NextResponse } from 'next/server' const PUBLIC_PATHS = ['/login'] +const CSP = [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline'", + "font-src 'self'", + "img-src 'self' data:", + "connect-src 'self'", + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self'", +].join('; ') + +const CSRF_COOKIE = 'csrf_token' +const CSRF_HEADER = 'x-csrf-token' + export default function proxy(request: NextRequest) { - const { pathname } = request.nextUrl - const isPublic = PUBLIC_PATHS.some((p) => pathname.startsWith(p)) - const hasSession = request.cookies.has('ops_session') + const { method, nextUrl } = request + const { pathname } = nextUrl - if (!isPublic && !hasSession) { - return NextResponse.redirect(new URL('/login', request.url)) + if (method === 'POST' && pathname.startsWith('/api/')) { + const cookieToken = request.cookies.get(CSRF_COOKIE)?.value + const headerToken = request.headers.get(CSRF_HEADER) + if (!cookieToken || cookieToken !== headerToken) { + return new NextResponse( + JSON.stringify({ error: 'CSRF validation failed' }), + { status: 403, headers: { 'Content-Type': 'application/json' } }, + ) + } } - if (isPublic && hasSession) { - return NextResponse.redirect(new URL('/', request.url)) + if (!pathname.startsWith('/api/')) { + const isPublic = PUBLIC_PATHS.some((p) => pathname.startsWith(p)) + const hasSession = request.cookies.has('ops_session') + if (!isPublic && !hasSession) { + return NextResponse.redirect(new URL('/login', request.url)) + } + if (isPublic && hasSession) { + return NextResponse.redirect(new URL('/', request.url)) + } } - return NextResponse.next() + const response = NextResponse.next() + + response.headers.set('Content-Security-Policy', CSP) + response.headers.set('X-Frame-Options', 'DENY') + response.headers.set('X-Content-Type-Options', 'nosniff') + response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin') + + if (method === 'GET' && !request.cookies.get(CSRF_COOKIE)) { + response.cookies.set(CSRF_COOKIE, crypto.randomUUID(), { + httpOnly: false, + sameSite: 'strict', + secure: process.env.NODE_ENV === 'production', + path: '/', + }) + } + + return response } export const config = { - matcher: ['/((?!api|_next/static|_next/image|.*\\.(?:png|ico|svg)$).*)'], + matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:png|ico|svg)$).*)'], } From 2812bc83e1fefb993c02ec087366a4097b3de55b Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Wed, 13 May 2026 21:20:24 +0200 Subject: [PATCH 2/3] fix(build): placeholder DATABASE_URL in builder stage Prisma v7 vereist DATABASE_URL bij prisma generate (config-load via prisma.config.ts) zelfs als generate niet daadwerkelijk verbindt. Container-builds zonder env-file faalden hierop. Echte URL wordt nog steeds runtime gezet via env_file in docker-compose. Co-Authored-By: Claude Opus 4.7 (1M context) --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 383599b..79da967 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . +ENV DATABASE_URL="postgresql://placeholder:placeholder@localhost:5432/placeholder" RUN npx prisma generate RUN npm run build From 9fbc5220bdef5ada27ff8170b4962800b9550e7b Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Wed, 13 May 2026 21:20:24 +0200 Subject: [PATCH 3/3] feat(home): vervang Next.js boilerplate door dashboard-index Sprint SP-1 maakte 7 module-routes (docker/git/systemd/caddy/flows/ audit/settings) maar liet app/page.tsx de create-next-app starter houden. Tijdelijke kaartjes-grid die auth-check doet en doorlinkt naar elke module. IDEA-060 is gelogd voor een rijke dashboard met live status per module. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/page.tsx | 96 ++++++++++++++++++++-------------------------------- 1 file changed, 37 insertions(+), 59 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 3f36f7c..99118b6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,65 +1,43 @@ -import Image from "next/image"; +import Link from 'next/link' +import { redirect } from 'next/navigation' +import { getCurrentUser } from '@/lib/session' + +export const dynamic = 'force-dynamic' + +const SECTIONS = [ + { href: '/docker', title: 'Docker', desc: 'Containers en status' }, + { href: '/git', title: 'Git', desc: 'Repo checkouts en diffs' }, + { href: '/systemd', title: 'systemd', desc: 'Services en journals' }, + { href: '/caddy', title: 'Caddy', desc: 'Config en certs' }, + { href: '/flows', title: 'Flows', desc: 'Multi-step deployments' }, + { href: '/audit', title: 'Audit', desc: 'Command-log en runs' }, + { href: '/settings', title: 'Settings', desc: 'Backups en config' }, +] + +export default async function Home() { + const user = await getCurrentUser() + if (!user) redirect('/login') -export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

+
+
+
+

Ops Dashboard

+

Welkom {user.email}

-
- - Vercel logomark - Deploy Now - - - Documentation - +
+ {SECTIONS.map((s) => ( + +

{s.title}

+

{s.desc}

+ + ))}
-
+
- ); + ) }