import { SignJWT, jwtVerify } from 'jose' import { cookies } from 'next/headers' import { prisma } from './db' import bcrypt from 'bcryptjs' import { JWT_SECRET } from './jwt-secret' export interface UserPayload { id: string email: string name: string role: 'SERVER_ADMIN' | 'TENANT_ADMIN' | 'OPERATOR' | 'VIEWER' tenantId?: string tenantSlug?: string emailVerified?: boolean } // Zentrale Session-Laufzeiten (Sekunden) – EINE Quelle für Token-Ablauf UND Cookie-maxAge, // damit beide nie auseinanderdriften. export const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 // 24 Stunden (Standard) export const SESSION_MAX_AGE_REMEMBER_SECONDS = 60 * 60 * 24 * 14 // 14 Tage ("angemeldet bleiben") export async function createToken(user: UserPayload, rememberMe = false): Promise { const maxAge = rememberMe ? SESSION_MAX_AGE_REMEMBER_SECONDS : SESSION_MAX_AGE_SECONDS return await new SignJWT({ user }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime(`${maxAge}s`) .sign(JWT_SECRET) } export async function verifyToken(token: string): Promise { try { const { payload } = await jwtVerify(token, JWT_SECRET) return payload.user as UserPayload } catch { return null } } export async function getSession(): Promise { const cookieStore = await cookies() const token = cookieStore.get('auth-token')?.value if (!token) return null return await verifyToken(token) } /** * Baut die Session-Payload für einen (bereits authentifizierten) Benutzer per ID. * Wird sowohl beim normalen Login (nach Passwortprüfung) als auch nach erfolgreicher * MFA-Verifikation verwendet, damit die Logik nur an einer Stelle lebt. */ export async function buildSessionForUser( userId: string ): Promise<{ success: boolean; user?: UserPayload; error?: string }> { const user = await (prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true, name: true, role: true, emailVerified: true, isActive: true }, }) as any) if (!user) return { success: false, error: 'Benutzer nicht gefunden' } if (user.isActive === false) { return { success: false, error: 'Ihr Konto wurde deaktiviert. Bitte kontaktieren Sie den Administrator.' } } const emailVerified = user.emailVerified !== false let tenantId: string | undefined let tenantSlug: string | undefined if ((user.role as string) !== 'SERVER_ADMIN') { const membership = await (prisma as any).tenantMembership.findFirst({ where: { userId: user.id }, include: { tenant: true }, orderBy: { createdAt: 'asc' }, }) if (membership) { if (!membership.tenant.isActive) { return { success: false, error: 'Ihr Mandant wurde gesperrt. Bitte kontaktieren Sie den Administrator.' } } tenantId = membership.tenantId tenantSlug = membership.tenant.slug } } const userPayload: UserPayload = { id: user.id, email: user.email, name: user.name, role: (user.role === 'ADMIN' ? 'SERVER_ADMIN' : user.role) as UserPayload['role'], tenantId, tenantSlug, emailVerified, } return { success: true, user: userPayload } } export async function login( email: string, password: string ): Promise<{ success: boolean; user?: UserPayload; error?: string }> { const user = await (prisma.user.findUnique({ where: { email }, select: { id: true, password: true, isActive: true }, }) as any) if (!user) { return { success: false, error: 'E-Mail oder Passwort falsch' } } const isValidPassword = await bcrypt.compare(password, user.password) if (!isValidPassword) { return { success: false, error: 'E-Mail oder Passwort falsch' } } if (user.isActive === false) { return { success: false, error: 'Ihr Konto wurde deaktiviert. Bitte kontaktieren Sie den Administrator.' } } return buildSessionForUser(user.id) } // ─── MFA: Zwischen-Token nach Passwort, vor zweitem Faktor ─── /** Kurzlebiges Token (10 Min) zwischen Passwort und zweitem Faktor. Ist KEINE Session. */ export async function createMfaToken(userId: string, rememberMe: boolean): Promise { return await new SignJWT({ uid: userId, rememberMe, purpose: 'mfa' }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('10m') .sign(JWT_SECRET) } export async function verifyMfaToken(token: string): Promise<{ userId: string; rememberMe: boolean } | null> { try { const { payload } = await jwtVerify(token, JWT_SECRET) if (payload.purpose !== 'mfa' || typeof payload.uid !== 'string') return null return { userId: payload.uid, rememberMe: !!payload.rememberMe } } catch { return null } } /** True, wenn der Benutzer einen zweiten Faktor aktiv hat (TOTP bestätigt oder WebAuthn registriert). */ export async function userHasMfa(userId: string): Promise { const user = await (prisma as any).user.findUnique({ where: { id: userId }, select: { totpConfirmedAt: true, _count: { select: { webauthnCredentials: true } } }, }) if (!user) return false return !!user.totpConfirmedAt || (user._count?.webauthnCredentials ?? 0) > 0 } export async function hashPassword(password: string): Promise { return await bcrypt.hash(password, 12) } export function canEdit(role: string): boolean { return role === 'SERVER_ADMIN' || role === 'TENANT_ADMIN' || role === 'OPERATOR' } export function isAdmin(role: string): boolean { return role === 'SERVER_ADMIN' || role === 'TENANT_ADMIN' } export function isServerAdmin(role: string): boolean { return role === 'SERVER_ADMIN' } export function isTenantAdmin(role: string): boolean { return role === 'TENANT_ADMIN' }