INSIGHT-MVP/packages/core-service/prisma/core.schema.prisma
Thomas Reitz 2348602fb0 feat: Erweiterte Profilfelder (analog O365) + Profilbild-Sync aus Microsoft 365
Neue Felder im Benutzerprofil (analog Microsoft 365 /me):
- Stellenbezeichnung (jobTitle), Abteilung (department)
- Firma (companyName), Standort (officeLocation)

Changes:
- Core: Prisma-Migration + neue Felder in User-Model, UpdateUserDto,
  findById/update/updateProfile
- CRM: M365UserProfile-Interface + getM365Profile um neue Felder erweitert;
  neue Methode getM365Photo() lädt 96x96 JPEG als Base64 Data-URL;
  neuer Endpoint GET /crm/office365/photo
- Frontend: AuthContext User-Interface, M365UserProfile-Typ, office365Api.getM365Photo()
  ProfilePage: Neues Formular-Fieldset "Organisation" mit 4 Feldern;
  manueller Sync-Button übernimmt auch Profilbild (immer überschreiben);
  useO365ProfileSync: Auto-Sync lädt Foto nur wenn noch kein INSIGHT-Avatar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 13:08:56 +01:00

359 lines
13 KiB
Text

// ============================================================
// INSIGHT MVP - Core Schema (platform_core Datenbank)
// ============================================================
// Zentrale Plattform-Tabellen: Users, Tenants, Auth, Modules
// Kein raw SQL - nur Prisma!
// ============================================================
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DATABASE_URL_DIRECT")
}
// --------------------------------------------------------
// User - Plattform-Benutzer
// --------------------------------------------------------
model User {
id String @id @default(uuid()) @db.Uuid
email String @unique @db.VarChar(255)
firstName String @map("first_name") @db.VarChar(100)
lastName String @map("last_name") @db.VarChar(100)
avatar String? @db.Text // Profilbild als Base64 Data-URL
// Kontaktdaten
phone String? @map("phone") @db.VarChar(30)
mobile String? @map("mobile") @db.VarChar(30)
// Adresse
street String? @map("street") @db.VarChar(200)
postalCode String? @map("postal_code") @db.VarChar(10)
city String? @map("city") @db.VarChar(100)
// Organisation (analog Microsoft 365 /me Profil)
jobTitle String? @map("job_title") @db.VarChar(100)
department String? @map("department") @db.VarChar(100)
companyName String? @map("company_name") @db.VarChar(200)
officeLocation String? @map("office_location") @db.VarChar(200)
role String @default("USER") @db.VarChar(50) // PLATFORM_ADMIN, TENANT_ADMIN, USER
isActive Boolean @default(true) @map("is_active")
// 2FA
twoFactorEnabled Boolean @default(false) @map("two_factor_enabled")
// Login-Tracking
lastLogin DateTime? @map("last_login")
failedLoginAttempts Int @default(0) @map("failed_login_attempts")
lastFailedLogin DateTime? @map("last_failed_login")
// Timestamps
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relationen
authProvider AuthProvider[]
tenantMemberships TenantMembership[]
auditLogs AuditLog[]
expertProfile ExpertProfile?
integrations UserIntegration[]
@@map("users")
}
// --------------------------------------------------------
// UserIntegration - Drittanbieter-OAuth-Tokens pro User
// --------------------------------------------------------
// Speichert Access- und Refresh-Tokens fuer externe Services
// (Microsoft 365 Graph API). Tokens werden AES-256-GCM verschluesselt.
model UserIntegration {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
provider String @db.VarChar(50) // MICROSOFT_365
accessToken String @map("access_token") @db.Text // AES-256-GCM verschluesselt
refreshToken String @map("refresh_token") @db.Text // AES-256-GCM verschluesselt
expiresAt DateTime @map("expires_at")
scopes String[] @default([])
msTenantId String? @map("ms_tenant_id") @db.VarChar(100)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, provider])
@@index([userId])
@@map("user_integrations")
}
// --------------------------------------------------------
// AuthProvider - Authentifizierungs-Provider pro User
// --------------------------------------------------------
// Unterstuetzt mehrere Auth-Methoden pro User:
// LOCAL (Passwort), MS_SSO (spaeter), M2M (Machine-to-Machine)
model AuthProvider {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
provider String @db.VarChar(50) // LOCAL, MS_SSO, M2M
providerId String? @map("provider_id") @db.VarChar(255) // Externe ID (z.B. MS Object ID)
passwordHash String? @map("password_hash") @db.VarChar(255)
totpSecret String? @map("totp_secret") @db.VarChar(255) // Verschluesselt
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relationen
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, provider])
@@map("auth_providers")
}
// --------------------------------------------------------
// Tenant - Mandant
// --------------------------------------------------------
model Tenant {
id String @id @default(uuid()) @db.Uuid
name String @db.VarChar(200)
slug String @unique @db.VarChar(50) // URL-freundlich, fuer DB-Name
isActive Boolean @default(true) @map("is_active")
// Mandant-Einstellungen (JSON)
settings Json @default("{}")
// Timestamps
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relationen
members TenantMembership[]
modules TenantModule[]
@@map("tenants")
}
// --------------------------------------------------------
// TenantMembership - User-Tenant-Zuordnung (M:N)
// --------------------------------------------------------
model TenantMembership {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
tenantId String @map("tenant_id") @db.Uuid
tenantRole String @default("MEMBER") @map("tenant_role") @db.VarChar(50) // ADMIN, MEMBER, VIEWER
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relationen
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@unique([userId, tenantId])
@@map("tenant_memberships")
}
// --------------------------------------------------------
// Module - Verfuegbare Plattform-Module
// --------------------------------------------------------
model Module {
id String @id @default(uuid()) @db.Uuid
key String @unique @db.VarChar(50) // z.B. "crm", "project", "docs"
name String @db.VarChar(100)
description String? @db.Text
version String @default("1.0.0") @db.VarChar(20)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relationen
tenantModules TenantModule[]
@@map("modules")
}
// --------------------------------------------------------
// TenantModule - Welcher Tenant welche Module nutzt
// --------------------------------------------------------
model TenantModule {
id String @id @default(uuid()) @db.Uuid
tenantId String @map("tenant_id") @db.Uuid
moduleId String @map("module_id") @db.Uuid
isActive Boolean @default(true) @map("is_active")
// Modul-spezifische Konfiguration pro Tenant
config Json @default("{}")
activatedAt DateTime @default(now()) @map("activated_at")
// Relationen
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
@@unique([tenantId, moduleId])
@@map("tenant_modules")
}
// --------------------------------------------------------
// AuditLog - Plattform-weites Audit-Log
// --------------------------------------------------------
model AuditLog {
id String @id @default(uuid()) @db.Uuid
userId String? @map("user_id") @db.Uuid
action String @db.VarChar(100) // z.B. "user.login", "tenant.create"
entity String @db.VarChar(100) // z.B. "User", "Tenant"
entityId String? @map("entity_id") @db.VarChar(255)
details Json? // Zusaetzliche Informationen
ipAddress String? @map("ip_address") @db.VarChar(45)
userAgent String? @map("user_agent") @db.Text
createdAt DateTime @default(now()) @map("created_at")
// Relationen
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([action])
@@index([entity, entityId])
@@index([createdAt])
@@map("audit_logs")
}
// --------------------------------------------------------
// ExpertProfile - Experten-Profil (1:1 mit User)
// --------------------------------------------------------
model ExpertProfile {
id String @id @default(uuid()) @db.Uuid
userId String @unique @map("user_id") @db.Uuid
// Skills als Tag-Array
skills String[] @default([])
// Timestamps
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relationen
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
experiences ExpertExperience[]
languages ExpertLanguage[]
projects ExpertProject[]
certifications ExpertCertification[]
attachments ExpertAttachment[]
@@map("expert_profiles")
}
// --------------------------------------------------------
// ExpertExperience - Erfahrung / Expertise-Bereiche
// --------------------------------------------------------
model ExpertExperience {
id String @id @default(uuid()) @db.Uuid
expertProfileId String @map("expert_profile_id") @db.Uuid
area String @db.VarChar(200) // z.B. "IT Infrastruktur"
years Int // Jahre Erfahrung
level String? @db.VarChar(50) // Experte, Fortgeschritten, Grundkenntnisse
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relationen
expertProfile ExpertProfile @relation(fields: [expertProfileId], references: [id], onDelete: Cascade)
@@map("expert_experiences")
}
// --------------------------------------------------------
// ExpertLanguage - Sprachen
// --------------------------------------------------------
model ExpertLanguage {
id String @id @default(uuid()) @db.Uuid
expertProfileId String @map("expert_profile_id") @db.Uuid
language String @db.VarChar(100) // z.B. "Deutsch"
level String @db.VarChar(20) // Muttersprache, C2, C1, B2, B1, A2, A1
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relationen
expertProfile ExpertProfile @relation(fields: [expertProfileId], references: [id], onDelete: Cascade)
@@map("expert_languages")
}
// --------------------------------------------------------
// ExpertProject - Projekthistorie
// --------------------------------------------------------
model ExpertProject {
id String @id @default(uuid()) @db.Uuid
expertProfileId String @map("expert_profile_id") @db.Uuid
// Zeitraum
fromMonth Int @map("from_month") // 1-12
fromYear Int @map("from_year") // z.B. 2023
toMonth Int? @map("to_month") // null wenn isCurrent
toYear Int? @map("to_year")
isCurrent Boolean @default(false) @map("is_current")
// Details
role String @db.VarChar(200) // Taetigkeit
tasks String? @db.Text // Aufgaben (max 1500 Zeichen im DTO)
company String? @db.VarChar(200) // Firma
companySize String? @map("company_size") @db.VarChar(20) // "1-10", "11-50", etc.
industry String? @db.VarChar(200) // Branche
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relationen
expertProfile ExpertProfile @relation(fields: [expertProfileId], references: [id], onDelete: Cascade)
@@index([expertProfileId, fromYear, fromMonth])
@@map("expert_projects")
}
// --------------------------------------------------------
// ExpertCertification - Zertifizierungen
// --------------------------------------------------------
model ExpertCertification {
id String @id @default(uuid()) @db.Uuid
expertProfileId String @map("expert_profile_id") @db.Uuid
title String @db.VarChar(300) // Titel
issuingBody String @map("issuing_body") @db.VarChar(300) // Zertifizierungsstelle
website String? @db.VarChar(500) // URL
issueYear Int @map("issue_year") // Ausstellungsjahr
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relationen
expertProfile ExpertProfile @relation(fields: [expertProfileId], references: [id], onDelete: Cascade)
@@map("expert_certifications")
}
// --------------------------------------------------------
// ExpertAttachment - Profilanlagen (Dateien als Base64)
// --------------------------------------------------------
model ExpertAttachment {
id String @id @default(uuid()) @db.Uuid
expertProfileId String @map("expert_profile_id") @db.Uuid
filename String @db.VarChar(255)
mimetype String @db.VarChar(100)
size Int // Groesse in Bytes
data String @db.Text // Base64-Daten
createdAt DateTime @default(now()) @map("created_at")
// Relationen
expertProfile ExpertProfile @relation(fields: [expertProfileId], references: [id], onDelete: Cascade)
@@map("expert_attachments")
}