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:
Z User committed 2026-10-28 22:38:39 +00:00
1 parent e7185103f5
commit 17ceefe32a
8 files changed
+277 -100

No files matched your search

+102
View File
@@ -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).*)',
],
}