echos-ocr/prisma/schema.prisma
Randall Stillwell a83c41694f Add change password to profile, persist all profile fields to DB
- Add jobTitle, company, bio columns to User model (previously localStorage only)
- Replace legacy Authentik GET /api/auth/me with session-based profile endpoint
- Update PUT /api/auth/me to persist all profile fields to DB
- Add PUT /api/auth/change-password endpoint with current password verification
- Add Security card to profile page with change password form
- Update user-profile provider to fetch from API instead of localStorage

Made-with: Cursor
2026-04-17 15:35:24 -05:00

487 lines
15 KiB
Text

generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
// ─── Auth.js tables ──────────────────────────────────────────
model User {
id String @id @default(cuid())
email String @unique
emailVerified DateTime?
hashedPassword String?
username String?
displayName String?
avatarUrl String @default("")
jobTitle String @default("")
company String @default("")
bio String @default("")
role String @default("viewer")
activeOrgId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
sessions Session[]
memberships OrgMember[]
assignedCards ResponseCard[] @relation("AssignedCards")
assignorCards ResponseCard[] @relation("AssignorCards")
reviewedCards ResponseCard[] @relation("ReviewedCards")
activityLogs ActivityLog[]
notifications Notification[]
authentikUid String? @unique
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
// ─── Organization / Multi-tenancy ────────────────────────────
model Organization {
id String @id @default(cuid())
name String
slug String @unique
type String @default("church")
timezone String @default("America/Chicago")
settings Json?
allowedDomains String[] @default([])
onboardingComplete Boolean @default(false)
onboardingStep Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
locations Location[]
members OrgMember[]
invitations Invitation[]
apiKeys ApiKey[]
cards ResponseCard[]
jobs ProcessingJob[]
integrations Integration[]
formTemplates FormTemplate[]
persons Person[]
}
model OrgMember {
id String @id @default(cuid())
userId String
organizationId String
role String @default("viewer")
dismissedHints Json?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([userId, organizationId])
}
model Location {
id String @id @default(cuid())
organizationId String
name String
address String?
timezone String?
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
collectionDays CollectionDay[]
cards ResponseCard[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CollectionDay {
id String @id @default(cuid())
locationId String
name String
description String?
rrule String?
dayOfWeek Int?
timeStart String?
timeEnd String?
isRecurring Boolean @default(true)
date DateTime?
isActive Boolean @default(true)
location Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
cards ResponseCard[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ─── Form Templates (Dynamic Fields) ────────────────────────
model FormTemplate {
id String @id @default(cuid())
organizationId String
name String
slug String
description String?
isDefault Boolean @default(false)
isActive Boolean @default(true)
version Int @default(1)
branding Json?
settings Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
fields FormField[]
cards ResponseCard[]
@@unique([organizationId, slug])
@@index([organizationId])
}
model FormField {
id String @id @default(cuid())
formTemplateId String
key String
label String
type String
section String @default("personal")
required Boolean @default(false)
removable Boolean @default(true)
sortOrder Int @default(0)
options Json?
placeholder String?
helpText String?
validation Json?
visibleOnCard Boolean @default(true)
visibleOnSurvey Boolean @default(true)
isCore Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
formTemplate FormTemplate @relation(fields: [formTemplateId], references: [id], onDelete: Cascade)
@@unique([formTemplateId, key])
@@index([formTemplateId, sortOrder])
}
// ─── People (Contact Directory) ─────────────────────────────
model Person {
id String @id @default(cuid())
organizationId String
firstName String
lastName String
email String?
cellPhone String?
fieldData Json?
mergedIntoId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
cards ResponseCard[]
mergedInto Person? @relation("PersonMerge", fields: [mergedIntoId], references: [id])
mergedFrom Person[] @relation("PersonMerge")
@@index([organizationId])
@@index([email])
@@index([organizationId, firstName, lastName])
@@index([mergedIntoId])
}
// ─── Integrations ────────────────────────────────────────────
model Integration {
id String @id @default(cuid())
organizationId String
provider String
name String
enabled Boolean @default(false)
config Json
fieldMapping Json?
syncDirection String @default("push")
triggerEvents Json?
lastSyncAt DateTime?
lastSyncStatus String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
@@unique([organizationId, provider, name])
@@index([organizationId, provider])
}
// ─── Invitations ─────────────────────────────────────────────
model Invitation {
id String @id @default(cuid())
email String
role String @default("viewer")
organizationId String
invitedById String
token String @unique @default(cuid())
expiresAt DateTime
acceptedAt DateTime?
createdAt DateTime @default(now())
organization Organization @relation(fields: [organizationId], references: [id])
}
// ─── Password Reset ─────────────────────────────────────────
model PasswordResetToken {
id String @id @default(cuid())
email String
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
@@index([email])
}
// ─── System config ───────────────────────────────────────────
model SystemConfig {
id String @id @default("singleton")
isSetupComplete Boolean @default(false)
setupStep Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ─── Core application models ─────────────────────────────────
model ResponseCard {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
firstName String?
lastName String?
name String?
gender String?
dateOfBirth String?
maritalStatus String?
maritalStatusOther String?
visitType String?
cellPhone String?
homePhone String?
email String?
address String?
aptNumber String?
city String?
state String?
zip String?
prayerRequests String?
prayerForTeam Boolean @default(false)
prayerConfidential Boolean @default(false)
messageTopics Json?
messageTopicsOther String?
nextStep Json?
attendanceDuration String?
campusPreference Json?
campusPreferenceOther String?
howHeard Json?
howHeardOther String?
serviceAttended String?
followUp String?
notes String?
serviceTime String?
planningCenter String?
iSaidYesBookSent Boolean @default(false)
ftGuestLetterSent Boolean @default(false)
firstTimeGuestDate DateTime?
salvationDate DateTime?
assignedToId String?
assignedById String?
assignedAt DateTime?
reviewedById String?
reviewedAt DateTime?
reviewNotes String?
assignedTo User? @relation("AssignedCards", fields: [assignedToId], references: [id])
assignedBy User? @relation("AssignorCards", fields: [assignedById], references: [id])
reviewedBy User? @relation("ReviewedCards", fields: [reviewedById], references: [id])
sourceFile String?
frontImagePath String?
backImagePath String?
ocrStatus String @default("pending")
reviewStatus String @default("unreviewed")
ocrConfidence Float?
ocrError String?
rawOcrResponse Json?
mondayItemId String?
fieldData Json?
formTemplateId String?
personId String?
submissionSource String?
organizationId String?
locationId String?
collectionDayId String?
collectionDate DateTime?
organization Organization? @relation(fields: [organizationId], references: [id])
location Location? @relation(fields: [locationId], references: [id])
collectionDay CollectionDay? @relation(fields: [collectionDayId], references: [id])
formTemplate FormTemplate? @relation(fields: [formTemplateId], references: [id])
person Person? @relation(fields: [personId], references: [id])
@@index([ocrStatus])
@@index([reviewStatus])
@@index([name])
@@index([firstName, lastName])
@@index([createdAt])
@@index([mondayItemId])
@@index([assignedToId])
@@index([reviewedById])
@@index([organizationId])
@@index([locationId])
@@index([collectionDayId])
@@index([formTemplateId])
@@index([personId])
}
model ProcessingJob {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
fileName String
filePath String
status String @default("queued")
totalPages Int @default(0)
processed Int @default(0)
error String?
cardIds Json?
organizationId String?
organization Organization? @relation(fields: [organizationId], references: [id])
@@index([status])
}
model AppSettings {
id String @id @default("singleton")
ollamaUrl String @default("http://192.168.68.108:11434")
model String @default("llava:7b")
watchDir String @default("")
watching Boolean @default(false)
sourceRetentionDays Int @default(30)
imageRetentionDays Int @default(180)
aiProvider String @default("gateway")
aiModel String @default("")
mondayApiToken String @default("")
mondayBoardId String @default("")
mondayEnabled Boolean @default(false)
mondayColumnMap Json?
mondayWebhookId String @default("")
mondayWebhookUrl String @default("")
webhookUrl String @default("")
webhookSecret String @default("")
webhookEnabled Boolean @default(false)
webhookEvents Json?
emailImapHost String @default("imap.dreamhost.com")
emailImapPort Int @default(993)
emailImapUser String @default("echo-ocr@stillwell.cloud")
emailImapPass String @default("")
emailImapTls Boolean @default(true)
emailFolder String @default("INBOX")
emailWatching Boolean @default(false)
emailProcessed String @default("mark_read")
emailProcessedFolder String @default("Processed")
ftpEnabled Boolean @default(false)
ftpHost String @default("")
ftpPort Int @default(22)
ftpUser String @default("")
ftpPass String @default("")
ftpProtocol String @default("sftp") // "sftp" | "ftp" | "ftps"
ftpIncomingDir String @default("/incoming")
ftpProcessedDir String @default("/processed")
}
model ActivityLog {
id String @id @default(cuid())
createdAt DateTime @default(now())
cardId String?
action String
source String
summary String
changes Json?
userId String?
user User? @relation(fields: [userId], references: [id])
@@index([cardId, createdAt])
}
model Notification {
id String @id @default(cuid())
createdAt DateTime @default(now())
read Boolean @default(false)
dismissed Boolean @default(false)
type String
title String
message String
cardId String?
actionUrl String?
meta Json?
userId String?
user User? @relation(fields: [userId], references: [id])
@@index([read, dismissed, createdAt])
@@index([cardId])
@@index([userId])
}
model ApiKey {
id String @id @default(cuid())
name String
hashedKey String @unique
prefix String
organizationId String
permissions Json
lastUsedAt DateTime?
expiresAt DateTime?
createdAt DateTime @default(now())
organization Organization @relation(fields: [organizationId], references: [id])
}