Files
tessera-ctl/apps/api/prisma/schema.prisma
T
schalli 010aceb1ac
Tessera CI/CD / Lint & Type Check (push) Successful in 41s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m40s
feat(ldap): support anonymous bind (no bind DN/password required)
bindDn and bindPassword are now optional on LdapConfig (nullable
migration) and throughout the DTOs/service/client -- an admin can
leave both blank to connect to directories that permit anonymous
read access. LdapService.bind() falls back to an RFC 4513 anonymous
bind (empty DN + empty password) whenever either field is missing,
shared across testConnection, listGroups, and syncUsersForTenant.

Frontend: removed the required attribute from Bind-DN/Bind-Passwort,
added a placeholder hint ("leer = anonymous bind"), and the
"Verbindung testen" button now only needs a Server-URL to enable
(not bindDn+bindPassword). Config responses now return bindPassword
as null (not a misleading "********") when no password is set.

Verified locally: submitted only a Server-URL with both bind fields
empty and confirmed the request reached the anonymous-bind code path
(DNS failure for the unreachable test host, not a validation error).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 12:57:26 +02:00

258 lines
7.6 KiB
Plaintext

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Tenant {
id String @id @default(uuid())
name String
slug String @unique
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
ldapConfig LdapConfig?
}
enum Role {
SUPER_ADMIN
ADMIN
USER
}
model User {
id String @id @default(uuid())
username String @unique
email String @unique
passwordHash String?
displayName String?
role Role @default(USER)
isActive Boolean @default(true)
mustChangePassword Boolean @default(false)
ldapDn String?
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastLoginAt DateTime?
avatarPath String?
accentColor String?
passwordResetTokens PasswordResetToken[]
@@index([tenantId])
@@index([username])
@@index([email])
}
model PasswordResetToken {
id String @id @default(uuid())
token String @unique
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expiresAt DateTime
usedAt DateTime?
createdAt DateTime @default(now())
}
model LdapConfig {
id String @id @default(uuid())
tenantId String @unique
tenant Tenant @relation(fields: [tenantId], references: [id])
serverUrl String
baseDn String
bindDn String?
bindPassword String?
searchFilter String @default("(objectClass=person)")
syncIntervalMin Int @default(60)
isActive Boolean @default(true)
groupFilterDns String[] @default([])
lastSyncAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
fieldMappings LdapFieldMapping[]
}
model LdapFieldMapping {
id String @id @default(uuid())
ldapConfigId String
ldapConfig LdapConfig @relation(fields: [ldapConfigId], references: [id], onDelete: Cascade)
ldapField String
tesseraField String
isDefault Boolean @default(false)
createdAt DateTime @default(now())
@@unique([ldapConfigId, ldapField])
}
model Module {
id String @id @default(uuid())
slug String @unique
name String
version String
category String
description Json
icon String?
isSystem Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
activations TenantModuleActivation[]
}
model TenantModuleActivation {
id String @id @default(uuid())
tenantId String
moduleId String
isActive Boolean @default(true)
activatedAt DateTime @default(now())
module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
@@unique([tenantId, moduleId])
@@index([tenantId])
}
model DashboardLayout {
id String @id @default(uuid())
userId String @unique
tenantId String
layouts Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([tenantId])
}
model WidgetInstance {
id String @id @default(uuid())
userId String
tenantId String
widgetType String
config Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
favoriteLinks FavoriteLink[]
@@index([userId])
@@index([tenantId])
}
model SearchProvider {
id String @id @default(uuid())
userId String?
tenantId String?
name String
urlTemplate String
isDefault Boolean @default(false)
createdAt DateTime @default(now())
@@index([userId])
}
model CalendarSource {
id String @id @default(uuid())
userId String
tenantId String
name String
type String // 'caldav' | 'ics' | 'exchange'
exchangeMode String? // 'ews' | 'graph' — only for exchange type
domain String? // Exchange EWS only: Windows domain (e.g. COMPANY)
url String
username String?
encryptedPassword String? // AES-256-GCM ciphertext (iv:authTag:ciphertext hex)
color String? @default("#3B82F6")
isVisible Boolean @default(true)
syncIntervalMin Int @default(15)
lastSyncAt DateTime?
lastSyncError String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([tenantId])
}
model DkvModuleConfig {
id String @id @default(uuid())
tenantId String @unique
protocol String @default("imap") // 'imap' | 'exchange'
host String?
port Int?
encryption String @default("ssl-tls") // 'none' | 'starttls' | 'ssl-tls'
folder String @default("INBOX")
senderFilter String?
pollIntervalMin Int @default(60)
isActive Boolean @default(false)
exportRecipient String?
vehicleFormatString String @default("{Marke}/{Modell}/{Kennzeichen}")
domain String? // Exchange only: Windows domain (optional)
encryptedInboxCreds String? // AES-256-GCM: JSON { username, password } encrypted
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([tenantId])
}
model DkvVehicleMaster {
id String @id @default(uuid())
tenantId String
kennzeichen String
marke String
modell String
fahrer String // "Vorname Nachname"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([tenantId, kennzeichen])
@@index([tenantId])
}
model DkvInvoiceHistory {
id String @id @default(uuid())
tenantId String
datumZeit DateTime @default(now())
rechnungsnummer String
anzahlFahrzeuge Int @default(0)
anzahlTransaktionen Int @default(0)
status String // 'Verarbeitet' | 'Fehler' | 'Versand fehlgeschlagen'
errorMessage String?
exportFilename String?
createdAt DateTime @default(now())
@@index([tenantId])
@@index([datumZeit])
}
model SmtpConfig {
id String @id @default(uuid())
tenantId String @unique
host String
port Int @default(587)
encryption String @default("starttls") // 'none' | 'starttls' | 'ssl-tls'
username String?
encryptedPassword String? // AES-256-GCM via CalendarCryptoService
fromAddress String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model FavoriteLink {
id String @id @default(uuid())
userId String
tenantId String
widgetId String
title String
url String
iconUrl String?
position Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
widgetInstance WidgetInstance @relation(fields: [widgetId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([tenantId])
@@index([widgetId])
}