mirror of
https://gitlab.w-hs.de/an14051/backerup-website.git
synced 2026-07-27 17:35:29 +00:00
115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import type { NextRequest } from 'next/server'
|
|
import { jwtVerify } from 'jose'
|
|
|
|
// Dev fallback secret — must match lib/session.ts
|
|
const DEV_FALLBACK_SECRET = 'dev-secret-NOT-for-production-change-me-32chars!!'
|
|
|
|
function getSecretKey(): Uint8Array {
|
|
const secret = process.env.AUTH_SECRET ?? DEV_FALLBACK_SECRET
|
|
return new TextEncoder().encode(secret)
|
|
}
|
|
|
|
/** Paths (exact) that never require authentication. */
|
|
const PUBLIC_EXACT = new Set(['/', '/login'])
|
|
|
|
/** Path prefixes that never require authentication. */
|
|
const PUBLIC_PREFIXES = [
|
|
'/files/', // share link pages — public by design
|
|
'/api/files/', // individual file info + download — share links must work
|
|
'/api/auth/', // auth endpoints themselves
|
|
'/_next/',
|
|
]
|
|
|
|
/** Path prefixes that require admin role in addition to being authenticated. */
|
|
const ADMIN_PREFIXES = ['/admin', '/api/admin']
|
|
|
|
function isPublic(pathname: string): boolean {
|
|
if (PUBLIC_EXACT.has(pathname)) return true
|
|
return PUBLIC_PREFIXES.some((p) => pathname.startsWith(p))
|
|
}
|
|
|
|
function isAdminRoute(pathname: string): boolean {
|
|
return ADMIN_PREFIXES.some((p) => pathname.startsWith(p))
|
|
}
|
|
|
|
const SECURITY_HEADERS: Record<string, string> = {
|
|
'X-Frame-Options': 'DENY',
|
|
'X-Content-Type-Options': 'nosniff',
|
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
|
|
'X-DNS-Prefetch-Control': 'off',
|
|
}
|
|
|
|
export async function proxy(request: NextRequest) {
|
|
const { pathname } = request.nextUrl
|
|
|
|
// Build the base response with security headers applied to every response
|
|
function withHeaders(res: NextResponse): NextResponse {
|
|
for (const [k, v] of Object.entries(SECURITY_HEADERS)) {
|
|
res.headers.set(k, v)
|
|
}
|
|
return res
|
|
}
|
|
|
|
// Static assets — just add security headers
|
|
if (pathname.startsWith('/_next/') || pathname === '/favicon.ico') {
|
|
return withHeaders(NextResponse.next())
|
|
}
|
|
|
|
// Auth disabled (no secret configured) — pass all traffic through
|
|
if (!process.env.AUTH_SECRET && process.env.NODE_ENV !== 'production') {
|
|
// Still add security headers in dev mode
|
|
return withHeaders(NextResponse.next())
|
|
}
|
|
|
|
// Public routes — no auth needed
|
|
if (isPublic(pathname)) {
|
|
return withHeaders(NextResponse.next())
|
|
}
|
|
|
|
// Attempt to validate session
|
|
const token = request.cookies.get('backerup_session')?.value
|
|
let payload: { userId?: string; username?: string; role?: string } | null = null
|
|
|
|
if (token) {
|
|
try {
|
|
const { payload: p } = await jwtVerify(token, getSecretKey(), { algorithms: ['HS256'] })
|
|
payload = p as { userId?: string; username?: string; role?: string }
|
|
} catch {
|
|
// expired or tampered — treat as unauthenticated
|
|
}
|
|
}
|
|
|
|
// Not authenticated
|
|
if (!payload) {
|
|
if (pathname.startsWith('/api/')) {
|
|
return withHeaders(NextResponse.json({ error: 'Unauthorized' }, { status: 401 }))
|
|
}
|
|
const loginUrl = request.nextUrl.clone()
|
|
loginUrl.pathname = '/login'
|
|
loginUrl.searchParams.set('from', pathname)
|
|
return NextResponse.redirect(loginUrl)
|
|
}
|
|
|
|
// Admin-only routes
|
|
if (isAdminRoute(pathname) && payload.role !== 'admin') {
|
|
if (pathname.startsWith('/api/')) {
|
|
return withHeaders(NextResponse.json({ error: 'Forbidden' }, { status: 403 }))
|
|
}
|
|
return withHeaders(NextResponse.redirect(new URL('/', request.url)))
|
|
}
|
|
|
|
// Authenticated — pass through
|
|
const response = NextResponse.next()
|
|
// Forward user info to server components/API routes via headers
|
|
response.headers.set('x-user-id', payload.userId ?? '')
|
|
response.headers.set('x-user-name', payload.username ?? '')
|
|
response.headers.set('x-user-role', payload.role ?? '')
|
|
return withHeaders(response)
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ['/((?!_next/static|_next/image|public/).*)'],
|
|
}
|