feat: auth system (middleware + login) + remove .env from git + fix hardcoded token
CRITICAL SECURITY FIXES: - src/middleware.ts: protects all routes except public ones (health, license check, seed-prod) Redirects unauthenticated users to /login, returns 401 for API routes - src/app/login/page.tsx: login page with password input - src/app/api/auth/login: HMAC-signed session cookie (7-day expiry) - src/app/api/auth/logout: clears session cookie - Top bar: Abmelden (logout) button now functional - .env removed from git tracking (secrets no longer in repo) - seed-prod: hardcoded admin token replaced with env MCP_API_TOKEN or random UUID - ADMIN_PASSWORD env var required for login (set in Dokploy env) Public routes (no auth needed): /api/health, /api/aos/seed-prod, /api/aos/licenses/check, /api/aos/licenses/validate /api/v1/* (token-auth'd), /api/mcp (token-auth'd), /login
This commit is contained in:
1 parent
e7185103f5
commit
17ceefe32a
8 files changed
+277
-100
No files matched your search
@@ -0,0 +1,102 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
// Middleware: protects all routes except public ones.
|
||||
// Uses a simple session cookie (HMAC-signed) for auth.
|
||||
// Password is set via ADMIN_PASSWORD env var.
|
||||
|
||||
const PUBLIC_PATHS = [
|
||||
'/api/health',
|
||||
'/api/aos/seed-prod',
|
||||
'/api/aos/licenses/check',
|
||||
'/api/aos/licenses/validate',
|
||||
'/login',
|
||||
'/_next',
|
||||
'/favicon.ico',
|
||||
'/manifest.json',
|
||||
'/icons/',
|
||||
'/apple-touch-icon',
|
||||
]
|
||||
|
||||
const PUBLIC_API_PREFIXES = [
|
||||
'/api/health',
|
||||
'/api/aos/seed-prod',
|
||||
'/api/aos/licenses/check',
|
||||
'/api/aos/licenses/validate',
|
||||
'/api/v1/', // public REST API (token-auth'd separately)
|
||||
'/api/mcp', // MCP (token-auth'd)
|
||||
]
|
||||
|
||||
function isPublic(pathname: string): boolean {
|
||||
// Exact matches
|
||||
if (PUBLIC_PATHS.includes(pathname)) return true
|
||||
// Prefix matches
|
||||
for (const prefix of PUBLIC_API_PREFIXES) {
|
||||
if (pathname.startsWith(prefix)) return true
|
||||
}
|
||||
// Static assets
|
||||
if (pathname.startsWith('/_next/') || pathname.startsWith('/icons/')) return true
|
||||
return false
|
||||
}
|
||||
|
||||
async function verifySession(token: string | undefined): Promise<boolean> {
|
||||
if (!token) return false
|
||||
const adminPassword = process.env.ADMIN_PASSWORD
|
||||
if (!adminPassword) return false
|
||||
try {
|
||||
// Session token format: base64(payload).hmac
|
||||
const [payloadB64, signature] = token.split('.')
|
||||
if (!payloadB64 || !signature) return false
|
||||
const payload = JSON.parse(Buffer.from(payloadB64, 'base64').toString())
|
||||
if (payload.exp && Date.now() > payload.exp) return false
|
||||
// Verify HMAC
|
||||
const crypto = await import('crypto')
|
||||
const key = process.env.AUTH_SECRET || adminPassword
|
||||
const expectedSig = crypto
|
||||
.createHmac('sha256', key)
|
||||
.update(payloadB64)
|
||||
.digest('base64url')
|
||||
return signature === expectedSig
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
const { pathname } = req.nextUrl
|
||||
|
||||
// Allow public paths
|
||||
if (isPublic(pathname)) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
// Check session cookie
|
||||
const sessionToken = req.cookies.get('aos-session')?.value
|
||||
const isAuthenticated = await verifySession(sessionToken)
|
||||
|
||||
if (!isAuthenticated) {
|
||||
// For API routes, return 401
|
||||
if (pathname.startsWith('/api/')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'unauthorized', message: 'Authentication required' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
// For pages, redirect to login
|
||||
const loginUrl = new URL('/login', req.url)
|
||||
loginUrl.searchParams.set('redirect', pathname)
|
||||
return NextResponse.redirect(loginUrl)
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except:
|
||||
* - _next/static, _next/image (static files)
|
||||
* - favicon.ico, icons, manifest (public assets)
|
||||
*/
|
||||
'/((?!_next/static|_next/image|favicon.ico|icons|manifest.json|apple-touch-icon).*)',
|
||||
],
|
||||
}
|
||||
Reference in new issue
Block a user