Authentifizierung & Security¶
Übersicht¶
Login-Seite
│
▼
POST /api/auth/login
│
├── admin_users abfragen
├── bcrypt-Vergleich
├── JWT erstellen (24h)
└── httpOnly Cookie setzen
│
▼
Alle weiteren Requests
│
▼
proxy.ts (Middleware)
│
├── Route erlaubt? (Whitelist)
├── Öffentliche Route? (/login)
└── JWT gültig? (Cookie prüfen)
├── Ja → Weiter
└── Nein → 401 / Redirect /login
JWT-Authentifizierung¶
Datei:
app/lib/auth.ts(63 Zeilen)
Konfiguration¶
| Parameter | Wert |
|---|---|
| Secret | ADMIN_JWT_SECRET (Env-Variable) |
| Cookie-Name | admin_session |
| Gültigkeit | 24 Stunden |
| Hash-Algorithmus | bcrypt (bcryptjs) |
Login-Ablauf¶
// auth.ts:16-36
async function verifyCredentials(username: string, password: string) {
const result = await db.query(
"SELECT id, username, password_hash, role FROM admin_users WHERE username = $1",
[username]
);
if (result.rows.length === 0) return null;
const valid = await bcrypt.compare(password, result.rows[0].password_hash);
if (!valid) return null;
// Last-Login aktualisieren
await db.query("UPDATE admin_users SET last_login = now() WHERE id = $1", [user.id]);
return { id: user.id, username: user.username, role: user.role };
}
Token-Erstellung¶
// auth.ts:38-44
function createToken(user: AdminUser): string {
return jwt.sign(
{ id: user.id, username: user.username, role: user.role },
JWT_SECRET,
{ expiresIn: "24h" }
);
}
Cookie-Konfiguration¶
// api/auth/login/route.ts
cookies().set("admin_session", token, {
httpOnly: true, // Kein JavaScript-Zugriff
secure: NODE_ENV === "production", // HTTPS in Produktion
sameSite: "lax", // CSRF-Schutz
path: "/", // Seitenweit verfügbar
maxAge: 86400 // 24 Stunden
});
Middleware (Route-Proxy)¶
Datei:
proxy.ts(128 Zeilen)
Route-Whitelist¶
// proxy.ts:11-60
const VALID_PAGES = new Set([
"/", "/login", "/dashboard", "/users", "/settlements",
"/intents", "/vouchers", "/affiliates", "/winners",
"/charity", "/config", "/contracts", "/errors",
"/workers", "/seeds", "/watchdog", "/testplayers"
]);
const VALID_API_ROUTES = new Set([
"/api/auth/login", "/api/auth/logout",
"/api/dashboard/stats", "/api/users", "/api/settlements",
"/api/intents", "/api/vouchers", "/api/vouchers/stats",
"/api/affiliates", "/api/winners", "/api/config",
"/api/contracts", "/api/errors", "/api/workers",
"/api/watchdog", "/api/testplayers", "/api/seeds",
"/api/maintenance"
]);
// Dynamische Routen
const VALID_PAGE_PREFIXES = ["/users/"];
const VALID_API_PREFIXES = ["/api/users/", "/api/charity/"];
Middleware-Logik¶
// proxy.ts:65-121
export function proxy(request: NextRequest) {
const path = request.nextUrl.pathname;
// 1. Next.js Internals durchlassen
if (path.startsWith("/_next/") || path === "/favicon.ico") {
return NextResponse.next();
}
// 2. API-Routen prüfen
if (path.startsWith("/api/")) {
if (!isValidApiRoute(path)) return new Response("Not Found", { status: 404 });
if (!isPublicRoute(path) && !hasValidToken(request)) {
return new Response("Unauthorized", { status: 401 });
}
return NextResponse.next();
}
// 3. Seiten-Routen prüfen
if (!isValidPage(path)) {
return NextResponse.redirect(new URL("/login", request.url));
}
// 4. Auth-Check für geschützte Seiten
if (!isPublicRoute(path) && !hasValidToken(request)) {
return NextResponse.redirect(new URL("/login", request.url));
}
// 5. Root → Dashboard Redirect
if (path === "/" && hasValidToken(request)) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
return NextResponse.next();
}
Öffentliche Routen (keine Auth nötig)¶
/login/api/auth/login
Sicherheitsmaßnahmen¶
| Maßnahme | Implementierung |
|---|---|
| Passwort-Hashing | bcrypt (bcryptjs) |
| Token-Speicherung | httpOnly Cookie (kein localStorage) |
| CSRF-Schutz | SameSite=lax |
| Route-Schutz | Whitelist-basierte Middleware |
| SQL-Injection | Parametrisierte Queries |
| XSS-Prävention | httpOnly Cookie, kein Token in JS |
Login-Seite¶
Datei:
app/login/page.tsx(180 Zeilen)
- Zentriertes Login-Formular im Dark Theme
- Username + Passwort-Felder
- Fehlermeldung bei ungültigen Credentials
- Redirect nach
/dashboardbei Erfolg - ChainBETs-Branding
Logout¶
// api/auth/logout/route.ts
export async function POST() {
cookies().set("admin_session", "", { maxAge: 0 }); // Cookie löschen
return NextResponse.json({ ok: true });
}