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,64 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const schema = z.object({
|
||||
password: z.string().min(1),
|
||||
redirect: z.string().optional(),
|
||||
})
|
||||
|
||||
// POST /api/auth/login
|
||||
// Simple password-based auth. Password is set via ADMIN_PASSWORD env var.
|
||||
// Returns a session cookie (HMAC-signed, 7-day expiry).
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const parsed = schema.safeParse(await req.json())
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'invalid request' }, { status: 400 })
|
||||
}
|
||||
const { password, redirect } = parsed.data
|
||||
|
||||
const adminPassword = process.env.ADMIN_PASSWORD
|
||||
if (!adminPassword) {
|
||||
return NextResponse.json(
|
||||
{ error: 'ADMIN_PASSWORD env not configured. Set it in Dokploy environment variables.' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
|
||||
if (password !== adminPassword) {
|
||||
return NextResponse.json({ error: 'Falsches Passwort' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Create session token: base64(payload).hmac
|
||||
const crypto = await import('crypto')
|
||||
const payload = {
|
||||
sub: 'admin',
|
||||
iat: Date.now(),
|
||||
exp: Date.now() + 7 * 24 * 3600 * 1000, // 7 days
|
||||
}
|
||||
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
||||
const key = process.env.AUTH_SECRET || adminPassword
|
||||
const signature = crypto
|
||||
.createHmac('sha256', key)
|
||||
.update(payloadB64)
|
||||
.digest('base64url')
|
||||
const token = `${payloadB64}.${signature}`
|
||||
|
||||
const res = NextResponse.json({
|
||||
ok: true,
|
||||
redirect: redirect || '/',
|
||||
})
|
||||
res.cookies.set('aos-session', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 7 * 24 * 3600, // 7 days
|
||||
})
|
||||
return res
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'login failed' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user