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 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}

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