Files
Lageplan/src/lib/auth.ts
Pepe Ziberi d08b96a42a
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
feat(admin): echtes Super-Admin-Benutzer-Management + letzte Anmeldung
- Super-Admin sieht jetzt ALLE Benutzer über alle Mandanten in einer Tabelle
  (statt nur Server-Admins). Neue Spalten: Mandant, Letzte Anmeldung, Status
- Einzelne Benutzer sperren/entsperren: User.isActive (Schema + Auto-Migration),
  Login-Sperre für deaktivierte Konten, PATCH /api/admin/users/[id] { isActive }
- GET /api/admin/users liefert jetzt lastLoginAt + isActive
- Selbstsperre verhindert; admin@lageplan.local geschützt
- Version 1.4.6

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 23:15:16 +02:00

135 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string> {
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<UserPayload | null> {
try {
const { payload } = await jwtVerify(token, JWT_SECRET)
return payload.user as UserPayload
} catch {
return null
}
}
export async function getSession(): Promise<UserPayload | null> {
const cookieStore = await cookies()
const token = cookieStore.get('auth-token')?.value
if (!token) return null
return await verifyToken(token)
}
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,
email: true,
name: true,
password: true,
role: true,
emailVerified: 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' }
}
// Gesperrte Benutzer dürfen sich nicht anmelden (isActive === false).
// Tolerant: nur blockieren, wenn explizit false (alte Datensätze ohne Wert bleiben aktiv).
if (user.isActive === false) {
return { success: false, error: 'Ihr Konto wurde deaktiviert. Bitte kontaktieren Sie den Administrator.' }
}
// Track email verification status (allow login regardless)
const emailVerified = (user as any).emailVerified !== false
// Get first tenant membership for non-server-admins
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) {
// Check if tenant is active
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 hashPassword(password: string): Promise<string> {
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'
}