Files
backend/docs/data-model.md
Sathish c9b38dc6ab fix(security): resolve audit findings — secrets, env contract, migrations
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>
2026-08-04 16:03:08 +05:30

517 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Data model (entityrelationship)
Generated from `backend/prisma/schema.prisma` — 24 entities. Regenerate after any schema change; the schema is the source of truth.
## ER diagram
```mermaid
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" | "year" |
| `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`