- plan-features.ts: globalBlocklist 'curated'|'full' (curated = 30-domain stub,
TODO real ~1-2k HaGeZi subset); maxAppDevices vs maxProtectedDevices split
(legend maxProtectedDevices: 2); mail 1/3/Infinity
- limit-enforcement structured errors on mail/connect, custom-domains/add, devices/enroll
({ error:'plan_limit', resource, current, limit }); approved-own-submissions already
excluded from custom-domain count (slot frees on approval)
- server/utils/downgrade-reconciliation.ts: founding-member exemption; re-upgrade
reactivates paused mail + degraded devices; downgrade pauses newest-N mail accounts
(isActive=false, pausedAt, pausedReason; pre-pause sets nextScanAt=now for a final
sweep — real direct IMAP scan is TODO/stub); degrades excess device profiles
(status='degraded', degradedAt); free → globalBlocklistGraceUntil = now+14d;
custom domains grandfathered
- set-plan.post.ts + stripe/webhook.post.ts: run reconciliation on plan change;
set-plan accepts { foundingMember } for testing
- GET /api/plan/change-preview?to=<plan>: gains/keeps/changes per resource (8 axes),
founding-member → direction 'same'
- me.get.ts: + foundingMember, globalBlocklistGraceUntil, planLimits block
- blocklist + mail-scan honour globalBlocklistGraceUntil (grace → treat as 'full')
- db: countMailConnections/getMailConnections exclude paused; getAllMailConnections;
getDeviceBlocklistMode (active|grace|passthrough|revoked)
- migration 20260511_tier_system_phase2 (profiles.founding_member +
global_blocklist_grace_until; mail_connections.paused_at/paused_reason;
protected_devices.degraded_at). prisma generate + build:backend clean.
TODOs (separate tickets): founding-member auto-counter on signup; real direct IMAP
final-scan (not just nextScanAt nudge); real curated blocklist data + wiring the
stub into the blocklist response for free users.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
759 lines
29 KiB
Plaintext
759 lines
29 KiB
Plaintext
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("free")
|
|
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")
|
|
|
|
// ─── 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")
|
|
}
|
|
|
|
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")
|
|
postId String? @map("post_id") @db.Uuid
|
|
addedAt DateTime @default(now()) @map("added_at")
|
|
|
|
submission DomainSubmission?
|
|
|
|
@@unique([userId, domain])
|
|
@@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")
|
|
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])
|
|
@@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")
|
|
|
|
blockedMails MailBlocked[]
|
|
|
|
@@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
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
connection MailConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([gmailMessageId, userId])
|
|
@@map("mail_blocked")
|
|
@@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")
|
|
}
|
|
|
|
// 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.
|
|
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"
|
|
lastSeenAt DateTime @default(now()) @map("last_seen_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@unique([userId, deviceId])
|
|
@@index([userId])
|
|
@@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")
|
|
}
|