generator client { provider = "prisma-client-js" output = "../server/generated/prisma" binaryTargets = ["native", "linux-musl-openssl-3.0.x"] } datasource db { provider = "postgresql" schemas = ["rebreak"] } model Profile { id String @id @db.Uuid username String? nickname String? avatar String? plan String @default("legend") streak Int @default(0) followersCount Int @default(0) @map("followers_count") stripeCustomerId String? @map("stripe_customer_id") stripeSubId String? @map("stripe_subscription_id") premiumUntil DateTime? @map("premium_until") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at") // ─── DiGA-Demographie (optional, user-initiated only) ─────────────────── // Diese Felder werden ausschließlich vom User über die Profile-Form // gesetzt — niemals durch Lyra-Extraction oder Memory-Inference. // Siehe memory/feedback_demographics_user_initiated.md birthYear Int? @map("birth_year") gender String? maritalStatus String? @map("marital_status") profession String? // legacy — deprecated, nicht mehr im Frontend, DB-Spalte bleibt employmentStatus String? @map("employment_status") shiftWork Boolean? @map("shift_work") industry String? jobTenure String? @map("job_tenure") bundesland String? city String? demographicsConsentAt DateTime? @map("demographics_consent_at") demographicsWithdrawnAt DateTime? @map("demographics_withdrawn_at") // ─── Lyra Voice-Picker (Legend-only Premium) ──────────────────────────── lyraVoiceId String? @map("lyra_voice_id") // ─── Pro-Trial-Reward (für Demographics-completion) ───────────────────── proTrialStartedAt DateTime? @map("pro_trial_started_at") proTrialExpiresAt DateTime? @map("pro_trial_expires_at") proTrialSource String? @map("pro_trial_source") proTrialUsedAt DateTime? @map("pro_trial_used_at") // ─── DiGA-Banner Persistence (server-side, überlebt Re-Install) ───────── digaBannerDismissedAt DateTime? @map("diga_banner_dismissed_at") // ─── Interaktives Onboarding (Welcome → Nickname → Block → Done) ──────── // Werte: "welcome" (Default für neue Profile) | "nickname" | "block" | "done" // Frontend nutzt diesen Wert um den Onboarding-Spotlight-Tour-Stand // wiederherzustellen — auch nach App-Reinstall (DB-State statt AsyncStorage). onboardingStep String @default("welcome") @map("onboarding_step") // ─── Protection-Disable-State (post-cooldown anti-auto-reactivation) ─── // Wird gesetzt wenn der User per Cooldown-Resolve den Schutz explizit // abschaltet. Solange `!= null`, gibt protection/state.protectionShouldBeActive // false zurück → Frontend macht KEINE Auto-Reactivation. User muss explizit // im UI re-aktivieren (POST /api/protection/mark-active setzt das Feld zurück). // Anti-Pattern: ohne dieses Feld würde der Schutz nach Cooldown sofort wieder // anspringen, was den Sinn des Cooldowns aushebelt. protectionDisabledAt DateTime? @map("protection_disabled_at") // ─── DiGA Rezept-Code-Audit ───────────────────────────────────────────── // Wenn ein User per Krankenkassen-Rezept reinkommt (DiGA-Pfad), wird der // Einlöse-Zeitpunkt hier persistiert. Reverse-Lookup auf den Code selbst // läuft über diga_codes.used_by_profile_id (1:n). digaCodeRedeemedAt DateTime? @map("diga_code_redeemed_at") digaCodes DigaCode[] // ─── Install-Tracking (für Streak: max(last_resolved_cooldown, last_install)) ── lastInstallAt DateTime? @map("last_install_at") // ─── Voice-Quota (tages-basiert, UTC-Reset) ───────────────────────────── // Tracked per plan: Free=60s/day, Pro=300s/day, Legend=unlimited (no tracking). // voiceQuotaResetAt wird auf UTC-Mitternacht des aktuellen Tages gesetzt. voiceSecondsUsedToday Int @default(0) @map("voice_seconds_used_today") voiceQuotaResetAt DateTime? @map("voice_quota_reset_at") // ─── Founding Members (erste 100 Signups → automatisch Pro, lifetime) ──── // foundingMember=true → exempt von Downgrade-Reconciliation (ihr Pro ist Geschenk). // Wird beim Signup gesetzt wenn profile-count < 100. // Manuell setzbar via /api/dev/set-plan (für Testing). foundingMember Boolean @default(false) @map("founding_member") // ─── Globale Blocklist Grace-Period (nach Downgrade auf free) ──────────── // Wenn gesetzt: User sieht noch die volle Blocklist bis zu diesem Datum. // Nach Ablauf: nur noch kuratierte Kernliste. 14-Tage-Grace. globalBlocklistGraceUntil DateTime? @map("global_blocklist_grace_until") // ─── Admin-Management (Phase E, Migration 20260509) ───────────────────── // banned: User wird auf API-Ebene blockiert (kein Login-Block — Supabase // bleibt unberührt). Soft-Delete scrubbt PII statt Hard-Delete (DSGVO). banned Boolean @default(false) bannedAt DateTime? @map("banned_at") bannedReason String? @map("banned_reason") deletedAt DateTime? @map("deleted_at") communityPosts CommunityPost[] communityReplies CommunityReply[] domainSubmissions DomainSubmission[] @@index([deletedAt]) @@index([plan]) @@map("profiles") @@schema("rebreak") } // ─── DiGA-Codes (Rezept-Einlösung für Krankenkassen-Pfad) ───────────────────── // // Codes werden vom Backend ausgegeben (später per Krankenkassen-API erstellt // + an Patient per E-Mail/Brief geschickt) und im Onboarding eingelöst. Bei // Einlösung wird der User auf den DiGA-Vollzugang (`grants_plan`, Default // 'legend') hochgestuft und das Trial-Modell übersprungen. // // Test-Codes (label='test_*') werden vom Seed angelegt. Wiederverwendung // nur per SQL-Reset: UPDATE diga_codes SET used_at=NULL, used_by_profile_id=NULL. model DigaCode { id String @id @default(uuid()) @db.Uuid code String @unique label String? // z.B. 'test_batch_2026-05' oder Krankenkassen-Code expiresAt DateTime? @map("expires_at") usedAt DateTime? @map("used_at") usedByProfileId String? @map("used_by_profile_id") @db.Uuid usedByProfile Profile? @relation(fields: [usedByProfileId], references: [id], onDelete: SetNull) grantsPlan String @default("legend") @map("grants_plan") notes String? createdAt DateTime @default(now()) @map("created_at") @@index([usedByProfileId]) @@map("diga_codes") @@schema("rebreak") } model Streak { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid startDate DateTime @map("start_date") @db.Date currentDays Int @default(0) @map("current_days") longestDays Int @default(0) @map("longest_days") avgMonthlySavings Float? @map("avg_monthly_savings") isActive Boolean @default(true) @map("is_active") @@map("streaks") @@schema("rebreak") } model StreakEvent { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid type String // "started" | "reset" | "milestone" | "relapse" meta Json? // z.B. { days: 30 } für Meilensteine createdAt DateTime @default(now()) @map("created_at") @@map("streak_events") @@schema("rebreak") } model UrgeLog { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid timestamp DateTime @default(now()) emotion String wasOvercome Boolean @default(false) @map("was_overcome") breathingDone Boolean @default(false) @map("breathing_done") @@map("urge_logs") @@schema("rebreak") } /// SOS-Session für DiGA-Auswertung — kompletter Verlauf einer Notfall-Session model SosSession { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid startedAt DateTime @default(now()) @map("started_at") endedAt DateTime? @map("ended_at") durationSec Int? @map("duration_sec") /// Voller Chat-Verlauf [{role, content, timestamp}] messages Json @default("[]") /// [{game, score, durationSec}] gamesPlayed Json @default("[]") breathingCount Int @default(0) @map("breathing_count") wasOvercome Boolean @default(false) @map("was_overcome") feedbackBetter Boolean? @map("feedback_better") feedbackRating Int? @map("feedback_rating") // 1-5 feedbackText String? @map("feedback_text") locale String? @@index([userId, startedAt]) @@map("sos_sessions") @@schema("rebreak") } model CommunityPost { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid category String content String imageUrl String? @map("image_url") upvotes Int @default(0) likesCount Int @default(0) @map("likes_count") dislikesCount Int @default(0) @map("dislikes_count") commentsCount Int @default(0) @map("comments_count") repostsCount Int @default(0) @map("reposts_count") isAnonymous Boolean @default(false) @map("is_anonymous") /// Reported-Marker: true wenn ein User den Post gemeldet hat. Admin-Queue /// listet alle Posts mit isModerated=true. Dismiss → false, Delete → bleibt /// true zusammen mit isDeleted=true (für audit/spätere Re-Review). isModerated Boolean @default(false) @map("is_moderated") /// Soft-Delete durch Moderation. content → "" damit Public-API nichts mehr /// rendert; Audit-Log behält Original (siehe ModerationAction.contentSnapshot). isDeleted Boolean @default(false) @map("is_deleted") deletedAt DateTime? @map("deleted_at") /// Wann der Post zum ersten Mal gemeldet wurde (queue-Sortierung). reportedAt DateTime? @map("reported_at") gameName String? @map("game_name") repostOfId String? @map("repost_of_id") @db.Uuid challengeId String? @map("challenge_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") author Profile? @relation(fields: [userId], references: [id]) repostOf CommunityPost? @relation("Reposts", fields: [repostOfId], references: [id], onDelete: SetNull) reposts CommunityPost[] @relation("Reposts") PostLike PostLike[] CommunityReply CommunityReply[] @@index([isModerated, reportedAt]) @@map("community_posts") @@schema("rebreak") } model PostLike { userId String @map("user_id") @db.Uuid postId String @map("post_id") @db.Uuid type String // "like" | "dislike" post CommunityPost @relation(fields: [postId], references: [id], onDelete: Cascade) @@id([userId, postId]) @@map("post_likes") @@schema("rebreak") } model CommunityReply { id String @id @default(uuid()) @db.Uuid postId String @map("post_id") @db.Uuid userId String @map("user_id") @db.Uuid content String parentReplyId String? @map("parent_reply_id") @db.Uuid isAnonymous Boolean @default(false) @map("is_anonymous") likesCount Int @default(0) @map("likes_count") /// Reported-Marker analog CommunityPost.isModerated. isModerated Boolean @default(false) @map("is_moderated") isDeleted Boolean @default(false) @map("is_deleted") deletedAt DateTime? @map("deleted_at") reportedAt DateTime? @map("reported_at") createdAt DateTime @default(now()) @map("created_at") post CommunityPost @relation(fields: [postId], references: [id], onDelete: Cascade) author Profile? @relation(fields: [userId], references: [id]) CommentLike CommentLike[] @@index([isModerated, reportedAt]) @@map("community_replies") @@schema("rebreak") } model CommentLike { userId String @map("user_id") @db.Uuid commentId String @map("comment_id") @db.Uuid reply CommunityReply @relation(fields: [commentId], references: [id], onDelete: Cascade) @@id([userId, commentId]) @@map("comment_likes") @@schema("rebreak") } model ChatRoom { id String @id @default(uuid()) @db.Uuid name String description String? isPublic Boolean @default(false) @map("is_public") avatarUrl String? @map("avatar_url") createdBy String @map("created_by") @db.Uuid joinMode String @default("open") @map("join_mode") // "open" | "approval" | "invite_only" inviteCode String? @unique @map("invite_code") memberCount Int @default(0) @map("member_count") isDefault Boolean @default(false) @map("is_default") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") members ChatRoomMember[] messages ChatMessage[] @@map("chat_rooms") @@schema("rebreak") } model ChatRoomMember { roomId String @map("room_id") @db.Uuid userId String @map("user_id") @db.Uuid role String @default("member") // "owner" | "admin" | "member" status String @default("active") // "active" | "pending" joinedAt DateTime @default(now()) @map("joined_at") room ChatRoom @relation(fields: [roomId], references: [id], onDelete: Cascade) @@id([roomId, userId]) @@map("chat_room_members") @@schema("rebreak") } model ChatMessage { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid content String roomId String? @map("room_id") @db.Uuid replyToId String? @map("reply_to_id") @db.Uuid attachmentUrl String? @map("attachment_url") attachmentType String? @map("attachment_type") attachmentName String? @map("attachment_name") likesCount Int @default(0) @map("likes_count") createdAt DateTime @default(now()) @map("created_at") room ChatRoom? @relation(fields: [roomId], references: [id], onDelete: Cascade) replyTo ChatMessage? @relation("ChatReplies", fields: [replyToId], references: [id], onDelete: SetNull) replies ChatMessage[] @relation("ChatReplies") likes ChatMessageLike[] @@map("chat_messages") @@schema("rebreak") } model ChatMessageLike { userId String @map("user_id") @db.Uuid messageId String @map("message_id") @db.Uuid message ChatMessage @relation(fields: [messageId], references: [id], onDelete: Cascade) @@id([userId, messageId]) @@map("chat_message_likes") @@schema("rebreak") } model DirectMessage { id String @id @default(uuid()) @db.Uuid senderId String @map("sender_id") @db.Uuid receiverId String @map("receiver_id") @db.Uuid content String replyToId String? @map("reply_to_id") @db.Uuid attachmentUrl String? @map("attachment_url") attachmentType String? @map("attachment_type") attachmentName String? @map("attachment_name") likesCount Int @default(0) @map("likes_count") createdAt DateTime @default(now()) @map("created_at") readAt DateTime? @map("read_at") replyTo DirectMessage? @relation("DmReplies", fields: [replyToId], references: [id], onDelete: SetNull) replies DirectMessage[] @relation("DmReplies") likes DirectMessageLike[] @@map("direct_messages") @@schema("rebreak") } model DirectMessageLike { userId String @map("user_id") @db.Uuid messageId String @map("message_id") @db.Uuid message DirectMessage @relation(fields: [messageId], references: [id], onDelete: Cascade) @@id([userId, messageId]) @@map("direct_message_likes") @@schema("rebreak") } model UserFollow { followerId String @map("follower_id") @db.Uuid followingId String @map("following_id") @db.Uuid @@id([followerId, followingId]) @@map("user_follows") @@schema("rebreak") } model UserScore { userId String @id @map("user_id") @db.Uuid totalPoints Int @default(0) @map("total_points") tier String @default("beginner") updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @@map("user_scores") @@schema("rebreak") } model ScoreEvent { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid eventType String @map("event_type") points Int createdAt DateTime @default(now()) @map("created_at") meta Json? @@map("score_events") @@schema("rebreak") } model UserCustomDomain { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid domain String source String @default("manual") // "active" | "submitted" | "approved" | "rejected" status String @default("active") // "web" | "mail_domain" | "mail_display_name" // Alle Types teilen den gleichen Slot-Pool (countActiveCustomDomains() zählt total). // mail_display_name: Substring-Heuristik gegen Sender-Display-Name (kein PII, Art. 4 DSGVO) type String @default("web") postId String? @map("post_id") @db.Uuid addedAt DateTime @default(now()) @map("added_at") submission DomainSubmission? @@unique([userId, domain]) @@index([userId, type]) @@map("user_custom_domains") @@schema("rebreak") } model DomainSubmission { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid domain String customDomainId String @unique @map("custom_domain_id") @db.Uuid postId String? @map("post_id") @db.Uuid // "pending" | "approved" | "rejected" status String @default("pending") // "web" | "mail_domain" — spiegelt den Type der zugehörigen UserCustomDomain-Row type String @default("web") yesVotes Int @default(0) @map("yes_votes") noVotes Int @default(0) @map("no_votes") reviewNote String? @map("review_note") createdAt DateTime @default(now()) @map("created_at") reviewedAt DateTime? @map("reviewed_at") customDomain UserCustomDomain @relation(fields: [customDomainId], references: [id], onDelete: Cascade) user Profile @relation(fields: [userId], references: [id]) votes DomainVote[] @@index([status, createdAt]) @@index([type, status]) @@map("domain_submissions") @@schema("rebreak") } model DomainVote { userId String @map("user_id") @db.Uuid submissionId String @map("submission_id") @db.Uuid vote String // "yes" | "no" createdAt DateTime @default(now()) @map("created_at") submission DomainSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade) @@id([userId, submissionId]) @@map("domain_votes") @@schema("rebreak") } enum FeedbackStatus { PENDING REVIEWING PLANNED SHIPPED REJECTED @@schema("rebreak") } model FeedbackItem { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid content String category String? status FeedbackStatus @default(PENDING) adminNote String? @map("admin_note") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("feedback_items") @@schema("rebreak") } model BlocklistDomain { id String @id @default(uuid()) @db.Uuid domain String @unique source String isActive Boolean @default(true) @map("is_active") reportCount Int @default(0) @map("report_count") @@map("blocklist_domains") @@schema("rebreak") } model TrustedContact { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid name String phone String? email String? createdAt DateTime @default(now()) @map("created_at") @@map("trusted_contacts") @@schema("rebreak") } model CoachSession { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid content Json createdAt DateTime @default(now()) @map("created_at") @@map("coach_sessions") @@schema("rebreak") } model MailConnection { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid email String provider String @default("imap") providerName String? @map("provider_name") imapHost String @map("imap_host") imapPort Int @map("imap_port") rejectUnauthorized Boolean @default(true) @map("reject_unauthorized") useStarttls Boolean @default(false) @map("use_starttls") passwordEncrypted String @map("password_encrypted") isActive Boolean @default(true) @map("is_active") /// Wenn gesetzt: Account wurde durch Downgrade-Reconciliation pausiert (nicht gelöscht). /// Re-Upgrade setzt pausedAt=null + pausedReason=null + isActive=true zurück. pausedAt DateTime? @map("paused_at") pausedReason String? @map("paused_reason") // z.B. "plan_downgrade" scanInterval Int @default(24) @map("scan_interval") lastScannedAt DateTime? @map("last_scanned_at") nextScanAt DateTime? @map("next_scan_at") emailsBlocked Int @default(0) @map("emails_blocked") emailsScanned Int @default(0) @map("emails_scanned") lastConnectError String? @map("last_connect_error") lastConnectErrorAt DateTime? @map("last_connect_error_at") lastIdleHeartbeatAt DateTime? @map("last_idle_heartbeat_at") createdAt DateTime @default(now()) @map("created_at") // ─── OAuth2-Auth-Framework (additiv, Phase 0) ──────────────────────────── // authMethod: 'app_password' (default, alle bestehenden Connections) // | 'oauth2_microsoft' (Outlook/Hotmail/Live — Phase 1) // | 'oauth2_google' (Gmail, future — wenn Google Basic-Auth deprecated) // Bestehende Connections: app_password per DEFAULT, keine Migration nötig. // OAuth-Felder bleiben NULL für app_password-Connections. authMethod String @default("app_password") @map("auth_method") /// AES-256-GCM encrypted (gleiches Verfahren wie passwordEncrypted, gleicher ENCRYPTION_KEY). /// Format: iv(24hex):tag(32hex):ciphertext(hex) — via server/utils/crypto.ts encrypt() oauthAccessToken String? @map("oauth_access_token") /// AES-256-GCM encrypted. Microsoft kann bei Refresh neues refresh_token liefern /// (Token-Rotation) — bei jedem Refresh-Call persistieren. oauthRefreshToken String? @map("oauth_refresh_token") /// UTC-Zeitstempel wann access_token abläuft. Daemon prüft on-connect: /// wenn expiry < now+5min → refresh vor IMAP-Connect. oauthTokenExpiry DateTime? @map("oauth_token_expiry") /// Gespeicherter Scope-String des erteilten Konsents (z.B. "IMAP.AccessAsUser.All offline_access openid"). /// Für Audit und zukünftige Scope-Vergleiche bei Re-Auth. oauthScope String? @map("oauth_scope") // ─── User-definierter Anzeige-Titel (optional, max 60 chars auf API-Ebene) ── // Wird statt der vollen Email-Adresse angezeigt (z.B. "Privat-Gmail"). // NULL → Frontend fällt auf Email-Domain zurück. title String? // ─── Art. 9-Einwilligung (DSGVO-Compliance, Mail-Auto-Delete) ─────────── // consentAt=NULL für Bestandsrows → "Re-Consent pending". // Daemon pausiert Mail-Verarbeitung wenn consentAt=NULL (kein Auto-Delete). // consentVersion: z.B. "art9-mail-v1-2026-05-13". Bump → alle Consents invalid. // consentIpAddress: Beweispflicht Art. 7 Abs. 1 DSGVO. consentAt DateTime? @map("consent_at") consentVersion String? @map("consent_version") consentIpAddress String? @map("consent_ip_address") blockedMails MailBlocked[] blockedStats MailBlockedStat[] @@unique([userId, email]) @@map("mail_connections") @@schema("rebreak") } enum GameChallengeStatus { OPEN ACTIVE FINISHED CANCELLED @@schema("rebreak") } model GameChallenge { id String @id @default(uuid()) @db.Uuid challengerId String @map("challenger_id") @db.Uuid challengerName String @map("challenger_name") opponentId String? @map("opponent_id") @db.Uuid opponentName String? @map("opponent_name") status GameChallengeStatus @default(OPEN) board String @default("---------") currentTurn String @default("X") @map("current_turn") winner String? postId String? @map("post_id") @db.Uuid gameType String @default("tictactoe") @map("game_type") isLive Boolean @default(false) @map("is_live") memoryState Json? @map("memory_state") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("game_challenges") @@schema("rebreak") } model Notification { id String @id @default(uuid()) @db.Uuid recipientId String @map("recipient_id") @db.Uuid type String // "new_comment" | "new_like" | "domain_vote" actorName String @map("actor_name") actorAvatar String? @map("actor_avatar") postId String? @map("post_id") @db.Uuid preview String? readAt DateTime? @map("read_at") createdAt DateTime @default(now()) @map("created_at") @@index([recipientId, readAt]) @@map("notifications") @@schema("rebreak") } model GameScore { userId String @id @map("user_id") @db.Uuid playerName String @map("player_name") wins Int @default(0) losses Int @default(0) draws Int @default(0) points Int @default(0) updatedAt DateTime @updatedAt @map("updated_at") @@map("game_scores") @@schema("rebreak") } model GameRating { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid gameName String @map("game_name") stars Int feedback String? score Int @default(0) createdAt DateTime @default(now()) @map("created_at") @@map("game_ratings") @@schema("rebreak") } model GameHighScore { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid nickname String gameName String @map("game_name") score Int updatedAt DateTime @updatedAt @map("updated_at") @@unique([userId, gameName]) @@index([gameName, score(sort: Desc)]) @@map("game_high_scores") @@schema("rebreak") } model MailBlocked { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid connectionId String @map("connection_id") @db.Uuid gmailMessageId String @map("gmail_message_id") senderEmail String @map("sender_email") senderName String? @map("sender_name") subject String receivedAt DateTime @map("received_at") action String /// Welcher Layer die Blockierung ausgelöst hat (z.B. "domain", "brand+random", "score:85", "llm:0.92"). /// NULL für ältere Einträge (vor Migration 20260514). triggerSource String? @map("trigger_source") @db.VarChar(64) createdAt DateTime @default(now()) @map("created_at") connection MailConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade) @@unique([gmailMessageId, userId]) @@map("mail_blocked") @@schema("rebreak") } /// Klassifikations-Samples für ML-Phase 3 (zukünftiges Fine-Tuning / Modell-Evaluation). /// Enthält Features + Outcomes jeder Mail-Klassifikation. /// KEIN Mail-Body — nur Metadaten (Sender-Domain, Subject, Score-Komponenten). /// Cascade-Delete bei User-Löschung (Art. 17 DSGVO). model MailClassificationSample { id String @id @default(cuid()) userId String @map("user_id") @db.Uuid connectionId String? @map("connection_id") @db.Uuid // Raw features (was analysiert wurde): senderName String? @map("sender_name") @db.VarChar(255) senderDomain String? @map("sender_domain") @db.VarChar(255) relayDecodedDomain String? @map("relay_decoded_domain") @db.VarChar(255) subject String? @db.VarChar(998) // RFC 5322 max // Computed features (Score-Komponenten als JSON): features Json // { score, brandMatch, randomTokens, keywordHits, styleFlags, … } // Outcome: finalAction String @map("final_action") // "blocked" | "passed" triggerSource String @map("trigger_source") // "domain", "brand+random", "score:NN", "llm:0.XX", "whitelist" // Groq verdict (nur wenn Layer 4 lief): groqIsGambling Boolean? @map("groq_is_gambling") groqConfidence Float? @map("groq_confidence") groqReason String? @map("groq_reason") @db.Text // User-Feedback (für später): userFeedback String? @map("user_feedback") // null | "correct" | "false-positive" | "false-negative" feedbackAt DateTime? @map("feedback_at") createdAt DateTime @default(now()) @map("created_at") @@index([userId]) @@index([createdAt]) @@index([finalAction, triggerSource]) @@map("mail_classification_samples") @@schema("rebreak") } /// Permanente Aggregat-Statistiken blockierter Mails pro Tag + Connection. /// Befüllt live beim Scan (vor dem 24h-Cleanup von mail_blocked). /// Enthält KEINE Mail-Inhalte — nur counts/dates (Datenminimierung Art. 5 DSGVO). /// Bei Connection-Disconnect werden Stats mitgelöscht (Cascade) — Konsistenz /// mit Art. 17 DSGVO: User-initiierter Disconnect = Recht auf Löschung aller /// zu dieser Connection gehörenden Daten. model MailBlockedStat { id String @id @default(cuid()) userId String @map("user_id") @db.Uuid /// UTC-Datum (time=00:00:00) — ein Eintrag pro User+Tag+Connection date DateTime @db.Date @map("date") mailConnectionId String @map("mail_connection_id") @db.Uuid /// IMAP-Host-Slug, z.B. "imap.gmail.com" (raw, für resolveProviderMeta zur Read-Zeit) provider String @map("provider") /// Human-readable Label, z.B. "Gmail" (wird bei Scan-Zeit aus resolveProviderMeta befüllt) providerLabel String @map("provider_label") count Int @default(0) connection MailConnection @relation(fields: [mailConnectionId], references: [id], onDelete: Cascade) @@unique([userId, date, mailConnectionId], map: "mail_blocked_stats_unique_day") @@index([userId, date]) @@index([userId, mailConnectionId]) @@map("mail_blocked_stats") @@schema("rebreak") } model ImapProxyAccount { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid proxyUsername String @unique @map("proxy_username") proxyPassword String @map("proxy_password") connectionId String @unique @map("connection_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") @@map("imap_proxy_accounts") @@schema("rebreak") } model CooldownRequest { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid reason String? cooldownStartedAt DateTime @default(now()) @map("cooldown_started_at") cooldownEndsAt DateTime @map("cooldown_ends_at") resolvedAt DateTime? @map("resolved_at") cancelledAt DateTime? @map("cancelled_at") tokenJti String @unique @map("token_jti") @@index([userId, cooldownEndsAt]) @@map("cooldown_requests") @@schema("rebreak") } enum LyraMemoryType { trigger habit strength relationship milestone pain_point goal preference @@schema("rebreak") } /// Persistente Erinnerungen von Lyra über den User — injiziert in System-Prompt jeder Session. /// Enthält Art-9-Gesundheitsdaten (Glücksspielkontext) — strenge RLS: nur service-role schreibt. model LyraMemory { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid type LyraMemoryType content String @db.VarChar(500) confidence Float @default(0.7) source String? // session-id | "manual" | "observed" createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) lastReferencedAt DateTime? @map("last_referenced_at") @db.Timestamptz(6) @@index([userId, type]) @@map("lyra_memories") @@schema("rebreak") } /// Admin-Allowlist — nur Einträge hier erhalten Zugang zur Admin-App. /// Seed: INSERT INTO "rebreak"."admin_users" ("user_id", "created_at") VALUES ('128df360-2008-4d6f-8aa1-bdb41ec1362f', NOW()); model AdminUser { userId String @id @map("user_id") @db.Uuid createdAt DateTime @default(now()) @map("created_at") addedBy String? @map("added_by") @@map("admin_users") @@schema("rebreak") } /// Audit-Log für Moderation-Aktionen (DSGVO-konform: Original-Inhalt bleibt für /// 90 Tage erhalten, danach Cron-Cleanup). Wird von /api/admin/moderation/[id]/* /// nach jeder Aktion (dismiss / delete / ban-user) geschrieben. model ModerationAction { id String @id @default(uuid()) @db.Uuid /// "post" | "comment" targetType String @map("target_type") /// CommunityPost.id oder CommunityReply.id targetId String @map("target_id") @db.Uuid /// "dismiss" | "delete" | "ban_user" action String /// Profile.id des Admins (aus admin_users-Allowlist). adminUserId String? @map("admin_user_id") @db.Uuid /// Snapshot des Original-Contents zum Zeitpunkt der Aktion (Audit-Trail). contentSnapshot String? @map("content_snapshot") /// Optionale Begründung vom Admin. reason String? createdAt DateTime @default(now()) @map("created_at") @@index([targetType, targetId]) @@index([createdAt]) @@map("moderation_actions") @@schema("rebreak") } /// Append-only Audit-Log für DSGVO-Einwilligungen und -Widerrufe. /// Jede Einwilligung (grant) UND jeder Widerruf (revoke) wird als eigener Eintrag geschrieben. /// Beweispflicht Art. 7 Abs. 1 DSGVO — niemals löschen, nur archivieren. model ConsentLog { id String @id @default(cuid()) userId String @map("user_id") @db.Uuid /// 'art9-mail' | künftige Consent-Typen (z.B. 'art9-lyra-memory') consentType String @map("consent_type") /// z.B. "art9-mail-v1-2026-05-13". Gleich der consentVersion in MailConnection. consentVersion String @map("consent_version") consentAt DateTime @map("consent_at") ipAddress String? @map("ip_address") userAgent String? @map("user_agent") /// Optional: welche MailConnection-Row dieser Consent betrifft. mailConnectionId String? @map("mail_connection_id") @db.Uuid /// Gesetzt wenn Einwilligung widerrufen wurde. revokedAt DateTime? @map("revoked_at") /// 'user_disconnect' | 'account_deleted' | 'text_version_updated' revokeReason String? @map("revoke_reason") @@index([userId, consentType]) @@map("consent_logs") @@schema("rebreak") } // Device-Binding pro User: Free=1, Pro=1, Legend=3 (siehe plan-features.ts maxDevices). // Frontend liefert via Capacitor Device.getId() eine persistente UUID — diese wird // bei jedem authentifizierten Request via x-device-id Header geprüft. // // Device-Account-Lock (Bypass-Schutz): // boundToPlan — Plan des Users zum Zeitpunkt der Bindung. NULL = noch nicht // gebunden (Free-User oder Device vor Migration). Lock gilt // nur wenn boundToPlan IN (pro, legend, standard, premium). // releaseRequestedAt — wann Original-User "Gerät freigeben" angeklickt hat. // releaseRequestedAt + 24h = Freigabe (Drang-Cooldown). // lockNotifiedAt — Rate-Limit-Marker: letzte Mail-Notification bei Login-Versuch // auf gebundenem Gerät. Max 1 Mail / 6h / Device. // // DSGVO Hans-Müller: Art-17-Konto-Löschung kaskadiert user_devices via ON DELETE CASCADE // im DB-FK → alle Device-Locks automatisch released. Keine gesonderte Logik nötig. model UserDevice { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid deviceId String @map("device_id") // Capacitor persistent UUID platform String // "ios" | "android" | "web" model String? // z.B. "iPhone15,2" name String? // z.B. "Chahines iPhone" osVersion String? @map("os_version") // z.B. "18.4.1" lastSeenAt DateTime @default(now()) @map("last_seen_at") createdAt DateTime @default(now()) @map("created_at") // ─── Device-Account-Lock ──────────────────────────────────────────────── /// Plan des Users zum Zeitpunkt der Bindung. NULL → kein Lock aktiv. /// Lock gilt wenn: boundToPlan != null AND releaseRequestedAt + 24h > NOW() boundToPlan String? @map("bound_to_plan") /// Wann der Original-User "Gerät freigeben" beantragt hat. /// NULL → noch kein Release-Request. Freigabe wird aktiv nach +24h. releaseRequestedAt DateTime? @map("release_requested_at") /// Letzte Mail-Notification bei fremdem Login-Versuch. Rate-Limit 6h. lockNotifiedAt DateTime? @map("lock_notified_at") @@unique([userId, deviceId]) @@index([userId]) @@index([deviceId]) @@map("user_devices") @@schema("rebreak") } // Multi-Device DNS-Schutz: Legend-User können bis zu 3 Geräte (Mac/iOS/Android/Windows) // mit einem per-Device DoH-Token schützen. Token wird in die mobileconfig/DoH-URL // eingebettet — ist die einzige Auth für den DoH-Server (Phase 2). model ProtectedDevice { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid /// Per-Device DoH-Token, embedded in mobileconfig ServerURL. /// Format: 32-char cryptographic-random hex. dnsToken String @unique @map("dns_token") /// "mac" | "windows" | "ios" | "android" platform String /// User-friendly label, z.B. "MacBook Pro" oder "Olfas iPhone" label String /// pending (enrolled, profile not installed yet) | active (user confirmed install) | degraded (plan downgrade — Grace läuft) | revoked status String @default("pending") /// User confirmed install via App (not server-side verified yet — DoH-routing kommt in Phase 2) installedAt DateTime? @map("installed_at") /// Optional: DoH-server pingt das später (Phase 2, separater Sprint) lastDnsQueryAt DateTime? @map("last_dns_query_at") /// Gesetzt wenn Plan-Downgrade das Gerät auf degraded setzt. Nach 14d Passthrough. /// Bei Re-Upgrade: zurück auf active, degradedAt=null. degradedAt DateTime? @map("degraded_at") revokedAt DateTime? @map("revoked_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@index([userId]) @@index([dnsToken]) @@map("protected_devices") @@schema("rebreak") } /// Short-lived PKCE state entries for Microsoft OAuth flow. /// Created by POST /api/mail/oauth/microsoft/init, consumed + deleted by /// POST /api/mail/oauth/microsoft/callback. /// TTL: 10 minutes — entries older than that are rejected and garbage-collected. model OauthPendingState { id String @id @default(uuid()) @db.Uuid /// Random 128-bit hex string used as CSRF state parameter in auth URL. stateId String @unique @map("state_id") userId String @map("user_id") @db.Uuid /// PKCE code_verifier (random 43-128 char string, S256 method). /// Never leaves the server — only used for token exchange. codeVerifier String @map("code_verifier") /// Optional: pre-filled email for login_hint parameter. email String? createdAt DateTime @default(now()) @map("created_at") @@index([stateId]) @@index([createdAt]) @@map("oauth_pending_states") @@schema("rebreak") }