Zum Inhalt

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" }
  );
}
// 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 /dashboard bei 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 });
}

admin_users-Tabelle

admin_users (
  id            SERIAL PRIMARY KEY,
  username      TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  role          TEXT NOT NULL,     -- z.B. 'superadmin'
  last_login    TIMESTAMP,
  created_at    TIMESTAMP DEFAULT NOW()
)