mirror of
https://gitlab.w-hs.de/an14051/backerup-website.git
synced 2026-07-27 17:35:29 +00:00
feat: implement user authentication and admin management
- Add user management API endpoints for listing and creating users. - Implement login and logout functionality with rate limiting. - Create UI components for login form, logout button, and animated elements. - Add session management with JWT and secure cookie handling. - Seed initial admin user if no users exist. - Introduce utility functions for password hashing and user authentication. - Set up proxy middleware for route protection and security headers. - Create a JSON file for user data storage. - Add links configuration for external resources.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
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
|
||||
} 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/).*)'],
|
||||
}
|
||||
Reference in New Issue
Block a user