Removes hardcoded fallback secrets and makes a misconfigured deploy fail loudly instead of silently falling back to development defaults. - Remove insecure JWT fallback secrets (messages.module, configuration) - Remove the 'default-secret' fallback for the 2FA TOTP encryption key and allow a dedicated TWO_FACTOR_ENCRYPTION_KEY so rotating JWT_SECRET no longer locks out every 2FA user (see docs/2fa-key-rotation.md) - Require EMAIL_API_URL; drop the hardcoded vendor email endpoint - Drive WebSocket CORS from CORS_ORIGINS instead of origin:'*' - Load .env before any Nest module is imported (src/load-env.ts). Decorator arguments evaluate at import time, so the gateway previously froze its CORS config to the localhost fallback even when CORS_ORIGINS was set - Add boot-time env validation: missing required vars, weak JWT_SECRET, and inverted access/refresh token lifetimes now abort startup - Enable Redis TLS certificate verification - Require ADMIN_EMAIL/ADMIN_PASSWORD for the seed; remove the published default super-admin credentials and stop printing them - Add the initial Prisma migration and stop gitignoring prisma/migrations - Make .env.example an accurate configuration contract (admin bootstrap, REDIS_TLS, S3_ENDPOINT, 2FA key, Firebase path; drop the dead SMTP block) - Add handover documentation: architecture, ER model, sequence and data-flow diagrams, 2FA key rotation runbook Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
19 KiB
19 KiB
Data model (entity–relationship)
Generated from backend/prisma/schema.prisma — 24 entities. Regenerate after any schema change; the schema is the source of truth.
ER diagram
erDiagram
AgentProfile ||--o{ AgentProfileFieldValue : agentProfile
AgentProfile ||--o{ ConnectionRequest : agentProfile
AgentProfile ||--o{ Conversation : agentProfile
AgentProfile ||--o{ Testimonial : agentProfile
AgentProfile ||--o{ VerificationHistory : agentProfile
AgentSubscription o|--o{ Payment : subscription
AgentType o|--o{ AgentProfile : agentType
AgentType ||--o{ AgentTypeSection : agentType
Conversation ||--o{ Message : conversation
ProfileField ||--o{ AgentProfileFieldValue : field
ProfileSection ||--o{ AgentTypeSection : section
ProfileSection ||--o{ ProfileField : section
SubscriptionPlan ||--o{ AgentSubscription : plan
SupportChat ||--o{ SupportMessage : chat
User o|--o{ AuditLog : actor
User o|--o{ VerificationHistory : admin
User ||--o{ AgentSubscription : user
User ||--o{ ConnectionRequest : user
User ||--o{ Conversation : user
User ||--o{ Message : sender
User ||--o{ Notification : user
User ||--o{ Payment : user
User ||--o{ Session : user
User ||--o{ SupportChat : user
User ||--o{ UserReport : reportedUser
User ||--o{ UserReport : reporter
User ||--o| AgentProfile : user
User ||--o| UserProfile : user
Entities
AgentProfile
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
userId |
String |
unique |
slug |
String |
unique |
agentTypeId |
String? |
|
firstName |
String? |
|
lastName |
String? |
|
phone |
String? |
|
avatar |
String? |
|
bio |
String? |
|
headline |
String? |
|
city |
String? |
|
state |
String? |
|
country |
String? |
|
address |
String? |
|
zipCode |
String? |
|
latitude |
Float? |
|
longitude |
Float? |
|
yearsOfExperience |
Int? |
|
licenseNumber |
String? |
|
companyName |
String? |
|
website |
String? |
|
facebookUrl |
String? |
|
twitterUrl |
String? |
|
linkedinUrl |
String? |
|
instagramUrl |
String? |
|
isVerified |
Boolean |
default false |
verificationStatus |
VerificationStatus |
enum, default NONE |
verificationNote |
String? |
Admin note (rejection reason) |
verifiedAt |
DateTime? |
|
verifiedBy |
String? |
Admin user ID who verified |
isProfileComplete |
Boolean |
default false |
profileCompleteness |
Int |
default 0 |
isPublic |
Boolean |
default true |
isFeatured |
Boolean |
default false |
isAvailable |
Boolean |
default true, Agent availability status for connect requests |
subscriptionStatus |
String? |
"ACTIVE", "NONE", etc. |
totalReviews |
Int |
default 0 |
averageRating |
Float |
default 0 |
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
user |
User |
FK, relation |
agentType |
AgentType? |
FK, relation |
fieldValues |
AgentProfileFieldValue[] |
relation |
testimonialToken |
String? |
unique |
connectionRequests |
ConnectionRequest[] |
relation |
conversations |
Conversation[] |
relation |
testimonials |
Testimonial[] |
relation |
verificationHistory |
VerificationHistory[] |
relation |
AgentProfileFieldValue
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
agentProfileId |
String |
|
fieldId |
String |
|
textValue |
String? |
For TEXT, TEXTAREA |
numberValue |
Float? |
For NUMBER, RANGE |
booleanValue |
Boolean? |
For single CHECKBOX |
jsonValue |
Json? |
For MULTI_SELECT, RADIO, complex data |
dateValue |
DateTime? |
For DATE |
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
agentProfile |
AgentProfile |
FK, relation |
field |
ProfileField |
FK, relation |
AgentSubscription
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
userId |
String |
|
planId |
String |
|
stripeCustomerId |
String |
cus_xxx |
stripeSubscriptionId |
String? |
unique, sub_xxx |
status |
SubscriptionStatus |
enum, default INCOMPLETE |
currentPeriodStart |
DateTime? |
|
currentPeriodEnd |
DateTime? |
|
cancelAtPeriodEnd |
Boolean |
default false |
canceledAt |
DateTime? |
|
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
user |
User |
FK, relation |
plan |
SubscriptionPlan |
FK, relation |
payments |
Payment[] |
relation |
AgentType
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
name |
String |
unique |
description |
String? |
|
icon |
String? |
|
isActive |
Boolean |
default true |
sortOrder |
Int |
default 0 |
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
agents |
AgentProfile[] |
relation |
agentTypeSections |
AgentTypeSection[] |
relation |
AgentTypeSection
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
agentTypeId |
String |
|
sectionId |
String |
|
sortOrder |
Int |
default 0, Order specific to this agent type |
isRequired |
Boolean |
default false, Is this section required for this type |
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
agentType |
AgentType |
FK, relation |
section |
ProfileSection |
FK, relation |
AuditLog
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
actorId |
String? |
null = system / unauthenticated |
actorRole |
String? |
USER, AGENT, ADMIN, SUPER_ADMIN, SYSTEM |
action |
String |
see AuditAction enum in audit.constants.ts |
resourceType |
String? |
User, AgentProfile, Subscription, Payment, etc. |
resourceId |
String? |
|
metadata |
Json? |
{ before, after, reason, params, ... } |
ipAddress |
String? |
|
userAgent |
String? |
|
createdAt |
DateTime |
default now( |
actor |
User? |
FK, relation |
CmsContent
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
pageSlug |
String |
|
sectionKey |
String |
|
content |
Json |
|
isPublished |
Boolean |
default true |
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
ConnectionRequest
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
userId |
String |
User sending the request |
agentProfileId |
String |
Agent receiving the request |
status |
ConnectionStatus |
enum, default PENDING |
message |
String? |
Optional message from user |
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
respondedAt |
DateTime? |
When agent responded |
user |
User |
FK, relation |
agentProfile |
AgentProfile |
FK, relation |
ContactMessage
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
name |
String |
|
email |
String |
|
phone |
String? |
|
message |
String |
|
isRead |
Boolean |
default false |
createdAt |
DateTime |
default now( |
Conversation
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
userId |
String |
Regular user in the conversation |
agentProfileId |
String |
Agent in the conversation |
lastMessageAt |
DateTime? |
|
lastMessageText |
String? |
|
userUnreadCount |
Int |
default 0, Unread count for the user |
agentUnreadCount |
Int |
default 0, Unread count for the agent |
userMuted |
Boolean |
default false |
agentMuted |
Boolean |
default false |
userFavorited |
Boolean |
default false |
agentFavorited |
Boolean |
default false |
userClearedAt |
DateTime? |
|
agentClearedAt |
DateTime? |
|
userDeletedAt |
DateTime? |
|
agentDeletedAt |
DateTime? |
|
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
user |
User |
FK, relation |
agentProfile |
AgentProfile |
FK, relation |
messages |
Message[] |
relation |
Message
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
conversationId |
String |
|
senderId |
String |
User ID of the sender (can be user or agent's user) |
content |
String |
|
messageType |
MessageType |
enum, default TEXT |
fileUrl |
String? |
|
fileName |
String? |
|
fileSize |
Int? |
File size in bytes |
mimeType |
String? |
|
status |
MessageStatus |
enum, default SENT |
deliveredAt |
DateTime? |
|
readAt |
DateTime? |
|
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
conversation |
Conversation |
FK, relation |
sender |
User |
FK, relation |
Notification
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
userId |
String |
|
type |
String |
'connection', 'message', 'system', 'update', 'request' |
title |
String |
|
description |
String |
|
read |
Boolean |
default false |
actionUrl |
String? |
|
data |
Json? |
Extra metadata (conversationId, connectionRequestId, etc.) |
createdAt |
DateTime |
default now( |
user |
User |
FK, relation |
Payment
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
subscriptionId |
String? |
|
userId |
String |
|
stripePaymentIntentId |
String? |
unique, pi_xxx |
stripeInvoiceId |
String? |
unique, in_xxx |
amount |
Int |
cents |
currency |
String |
default "usd" |
status |
String |
"succeeded", "failed", "pending" |
receiptUrl |
String? |
|
createdAt |
DateTime |
default now( |
subscription |
AgentSubscription? |
FK, relation |
user |
User |
FK, relation |
ProfileField
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
sectionId |
String |
|
name |
String |
e.g., "State", "Years in Business" |
slug |
String |
Unique within section: e.g., "state", "years_in_business" |
fieldType |
FieldType |
enum |
description |
String? |
Help text shown to user |
placeholder |
String? |
Placeholder text for input |
defaultValue |
String? |
Default value (JSON for complex types) |
sortOrder |
Int |
default 0 |
isActive |
Boolean |
default true |
isRequired |
Boolean |
default false |
isSearchableOnly |
Boolean |
default false, If true, shown in edit form & search, but hidden on public profile |
validation |
Json? |
{ min, max, minLength, maxLength, pattern, etc. } |
options |
Json? |
[{ value: "...", label: "...", sortOrder: 0 }] |
rangeConfig |
Json? |
{ min: 0, max: 100, step: 1 } |
uiConfig |
Json? |
{ columns: 2, showInPreview: true, etc. } |
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
section |
ProfileSection |
FK, relation |
fieldValues |
AgentProfileFieldValue[] |
relation |
ProfileSection
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
name |
String |
e.g., "Location", "Experience", "Specialization" |
slug |
String |
unique, URL-friendly identifier |
description |
String? |
|
icon |
String? |
Icon name or URL |
sortOrder |
Int |
default 0 |
isActive |
Boolean |
default true |
isGlobal |
Boolean |
default false, If true, applies to ALL agent types |
isSystem |
Boolean |
default false, If true, section cannot be deleted (system default) |
isRepeatable |
Boolean |
default false, If true, user can add multiple entries (e.g., certifications) |
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
fields |
ProfileField[] |
relation |
agentTypeSections |
AgentTypeSection[] |
relation |
Session
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
userId |
String |
|
token |
String |
unique |
refreshToken |
String? |
unique |
userAgent |
String? |
|
ipAddress |
String? |
|
expiresAt |
DateTime |
|
createdAt |
DateTime |
default now( |
user |
User |
FK, relation |
SubscriptionPlan
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
name |
String |
"Professional Annual" |
description |
String? |
|
stripePriceId |
String |
unique, Stripe Price ID (price_xxx) |
amount |
Int |
Amount in cents (49900) |
currency |
String |
default "usd" |
interval |
String |
default "year", "month" |
features |
Json? |
["Feature 1", "Feature 2"] |
isActive |
Boolean |
default true |
sortOrder |
Int |
default 0 |
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
subscriptions |
AgentSubscription[] |
relation |
SupportChat
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
userId |
String |
|
status |
SupportChatStatus |
enum, default OPEN |
lastMessageAt |
DateTime? |
|
lastMessageText |
String? |
|
userUnreadCount |
Int |
default 0 |
adminUnreadCount |
Int |
default 0 |
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
user |
User |
FK, relation |
messages |
SupportMessage[] |
relation |
SupportMessage
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
chatId |
String |
|
senderId |
String |
|
senderRole |
String |
"USER" or "ADMIN" |
content |
String |
|
createdAt |
DateTime |
default now( |
chat |
SupportChat |
FK, relation |
Testimonial
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
agentProfileId |
String |
|
rating |
Int |
1-5 |
text |
String |
|
authorName |
String |
|
authorRole |
String |
"Home Buyer", "Investor", etc. |
isPublished |
Boolean |
default true |
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
agentProfile |
AgentProfile |
FK, relation |
User
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
email |
String |
unique |
password |
String? |
Null for social login users |
role |
UserRole |
enum, default USER |
status |
UserStatus |
enum, default ACTIVE |
emailVerified |
Boolean |
default false |
emailVerifiedAt |
DateTime? |
|
avatar |
String? |
Profile picture URL |
authProvider |
AuthProvider |
enum, default LOCAL |
googleId |
String? |
unique |
facebookId |
String? |
unique |
twitterId |
String? |
unique |
appleId |
String? |
unique |
twoFactorEnabled |
Boolean |
default false |
twoFactorSecret |
String? |
Encrypted TOTP secret |
twoFactorBackupCodes |
String? |
JSON array of hashed backup codes |
twoFactorVerifiedAt |
DateTime? |
When 2FA was enabled |
notificationPreferences |
Json? |
{ email: {...}, push: {...} } |
privacyPreferences |
Json? |
{ privacySettings: {...}, dataSettings: {...} } |
fcmTokens |
Json? |
[{ token, device, createdAt }] |
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
lastLoginAt |
DateTime? |
|
isOnline |
Boolean |
default false |
lastSeenAt |
DateTime? |
|
userProfile |
UserProfile? |
relation |
agentProfile |
AgentProfile? |
relation |
sessions |
Session[] |
relation |
connectionRequests |
ConnectionRequest[] |
relation |
conversations |
Conversation[] |
relation |
messages |
Message[] |
relation |
notifications |
Notification[] |
relation |
supportChats |
SupportChat[] |
relation |
subscriptions |
AgentSubscription[] |
relation |
payments |
Payment[] |
relation |
reportsSubmitted |
UserReport[] |
relation |
reportsReceived |
UserReport[] |
relation |
verificationActions |
VerificationHistory[] |
relation |
auditLogs |
AuditLog[] |
relation |
UserProfile
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
userId |
String |
unique |
firstName |
String? |
|
lastName |
String? |
|
phone |
String? |
|
avatar |
String? |
|
city |
String? |
|
state |
String? |
|
country |
String? |
|
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
user |
User |
FK, relation |
UserReport
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
reporterId |
String |
|
reportedUserId |
String |
|
conversationId |
String? |
|
reason |
String |
|
description |
String? |
|
status |
ReportStatus |
enum, default PENDING |
adminNotes |
String? |
|
createdAt |
DateTime |
default now( |
updatedAt |
DateTime |
|
reporter |
User |
FK, relation |
reportedUser |
User |
FK, relation |
VerificationHistory
| Field | Type | Notes |
|---|---|---|
id |
String |
PK, default uuid( |
agentProfileId |
String |
|
status |
VerificationStatus |
enum |
note |
String? |
|
adminId |
String? |
|
submittedData |
Json? |
Snapshot of agent profile + documents at submission time |
createdAt |
DateTime |
default now( |
agentProfile |
AgentProfile |
FK, relation |
admin |
User? |
FK, relation |
Enums
- AuthProvider:
LOCAL,GOOGLE,FACEBOOK,TWITTER,APPLE - ConnectionStatus:
PENDING,ACCEPTED,REJECTED - FieldType:
TEXT // Single line text input,TEXTAREA // Multi-line text area,CHECKBOX // Single checkbox (Yes/No),CHECKBOX_GROUP // Multiple checkboxes in grid (select multiple),RADIO // Radio buttons (select one),SELECT // Dropdown (select one),MULTI_SELECT // Dropdown with multi-select,RANGE // Slider with min/max,NUMBER // Number input,DATE // Date picker,TAG_INPUT // Tag input (add custom tags),FILE // File upload (documents, images),REPEATER // Repeatable group of fields (e.g., certification + years) - MessageStatus:
SENT,DELIVERED,READ - MessageType:
TEXT,FILE,IMAGE,SYSTEM // For system messages like "connection accepted" - ReportStatus:
PENDING,REVIEWED,RESOLVED,DISMISSED - SubscriptionStatus:
ACTIVE,PAST_DUE,CANCELED,UNPAID,TRIALING,INCOMPLETE - SupportChatStatus:
OPEN,CLOSED - UserRole:
USER,AGENT,ADMIN,SUPER_ADMIN - UserStatus:
ACTIVE,INACTIVE,SUSPENDED,PENDING_VERIFICATION - VerificationStatus:
NONE // Agent hasn't uploaded documents,PENDING_REVIEW // Documents uploaded, awaiting admin review,APPROVED // Admin approved verification,REJECTED // Admin rejected verification