# Looking - Database Schema > **MongoDB collection schemas and data models for the Looking API** This document details all MongoDB collections, their schemas, relationships, indexes, and data processing hooks. **Database Name**: `urge` **MongoDB Version**: 4.4 **ODM**: Mongoose 4.7.4 --- ## πŸ“‹ Table of Contents - [Collections Overview](#collections-overview) - [User Collection](#user-collection) - [Profile Collection](#profile-collection) - [Message Schema](#message-schema) - [Reset Collection](#reset-collection) - [Geocache Collection](#geocache-collection) - [Entity Relationships](#entity-relationships) - [Indexes](#indexes) - [Image Processing Hooks](#image-processing-hooks) --- ## πŸ“Š Collections Overview | Collection | Purpose | Model File | | ------------- | -------------------------------- | -------------------------------------------- | | **users** | User accounts and authentication | [models/user.js](src/models/user.js) | | **profiles** | Dating profile data and stories | [models/profile.js](src/models/profile.js) | | **messages** | (Embedded in profiles) | [models/message.js](src/models/message.js) | | **resets** | Password reset tokens | [models/reset.js](src/models/reset.js) | | **geocaches** | Location geocoding cache | [models/geocache.js](src/models/geocache.js) | --- ## πŸ‘€ User Collection **Collection Name**: `users` **Purpose**: User authentication, authorization, and account management ### Schema | Field | Type | Required | Unique | Default | Description | | -------------- | -------- | -------- | ------ | ------------ | ------------------------------------------- | | **username** | String | βœ… | βœ… | - | Unique username for login | | **password** | String | ❌ | ❌ | - | PBKDF2 hashed password (hash + salt stored) | | **name.first** | String | βœ… | ❌ | - | User's first name | | **name.last** | String | βœ… | ❌ | - | User's last name | | **email** | String | βœ… | βœ… | - | Unique email address | | **can** | [String] | ❌ | ❌ | `['view']` | Permission array (enum values) | | **forceReset** | Boolean | ❌ | ❌ | `true` | Force password change on next login | | **updated_at** | Date | ❌ | ❌ | `Date.now()` | Last update timestamp | ### Permission Enum Values ```javascript ["add", "edit", "delete", "manage", "super", "update", "view"]; ``` | Permission | Access Level | | ---------- | --------------------------------------- | | **view** | Read-only access to profiles | | **add** | Create new profiles | | **edit** | Modify existing profiles | | **update** | Update profile details | | **delete** | Remove profiles | | **manage** | Approve submissions, content management | | **super** | Full admin access, user management | ### Password Storage Passwords are **never stored in plaintext**. The system uses: **Algorithm**: PBKDF2 **Hash**: SHA-512 **Iterations**: 233,335 **Hash Length**: 32 bytes **Salt Length**: 24 bytes **Stored Format**: ```javascript { "password": "hash:salt" // 32-byte hash + 24-byte salt (hex encoded) } ``` ### Pre-Save Hooks **Password Hashing** (`findOneAndUpdate` hook): - Detects password changes in updates - Validates `password` matches `confirmPassword` - If `currentPassword` provided, validates before allowing change - Hashes new password with `authentication.hashPassword()` - Sets `forceReset: false` after successful password change ### Example Document ```json { "_id": ObjectId("507f1f77bcf86cd799439011"), "username": "admin", "password": "a3f4b2c1...e5d6c7b8:1a2b3c4d...5e6f7a8b", "name": { "first": "John", "last": "Doe" }, "email": "admin@example.com", "can": ["add", "edit", "delete", "manage", "super", "update", "view"], "forceReset": false, "updated_at": ISODate("2024-01-15T10:30:00Z") } ``` --- ## πŸ“‹ Profile Collection **Collection Name**: `profiles` **Purpose**: User profile data, stories, and message threads ### Schema | Field | Type | Required | Index | Default | Description | | ------------- | --------- | -------- | ----- | ------- | ------------------------------- | | **order** | Number | ❌ | ❌ | `0` | Display order for sorting | | **details** | Object | ❌ | ❌ | `{}` | Profile information (see below) | | **messages** | [Message] | ❌ | ❌ | `[]` | Array of embedded messages | | **submitted** | Boolean | ❌ | ❌ | `false` | Story submitted by user | | **approved** | Boolean | ❌ | ❌ | `false` | Approved by admin | ### Details Object Schema | Field | Type | Index | Default | Description | | ---------------------- | -------- | ----- | --------------------------------- | ----------------------------------------------------- | | **details.about** | String | ❌ | - | User's story/bio text | | **details.age** | Number | βœ… | `0` | User's age | | **details.location** | String | ❌ | - | City, State format | | **details.name** | String | βœ… | - | Display name | | **details.pic.detail** | String | ❌ | `"profile/default_detail.png"` | Full-size profile photo path | | **details.pic.thumb** | String | ❌ | `"profile/default_thumbnail.png"` | Thumbnail photo path | | **details.position** | [String] | ❌ | - | Position preferences (Top/Bottom/Versatile) | | **details.looking** | [String] | ❌ | - | What user is looking for (Dates/Friends/Relationship) | | **details.tribes** | [String] | ❌ | - | Tribes/groups (Geek/Jock/Bear/Otter/etc.) | | **details.ethnos** | [String] | ❌ | - | Ethnicity (White/Black/Latino/Asian/etc.) | ### Pre-Save Hooks **Image Processing** (both `save` and `findOneAndUpdate` hooks): 1. **Detects Base64 Image Data**: - Checks if `details.pic.detail` or `details.pic.thumb` is an object/base64 string 2. **Processes Images**: - **Detail Image**: Calls `Images.saveProfileDetailImage()` - Decodes base64 - Generates unique filename - Saves to `src/images/profile/` - Returns path string - **Thumbnail**: Calls `Images.saveProfileThumbnailImage()` - Same process for thumbnails - May resize/compress image 3. **Updates Document**: - Replaces base64 data with file path string - Continues with save operation ### Example Document ```json { "_id": ObjectId("507f1f77bcf86cd799439011"), "order": 1, "details": { "name": "John", "age": 28, "location": "San Francisco, CA", "about": "I've been using dating apps for 5 years. My experience has been...", "pic": { "thumb": "profile/john_1234567890_thumbnail.png", "detail": "profile/john_1234567890_detail.png" }, "position": ["Top", "Versatile"], "looking": ["Dates", "Friends"], "tribes": ["Geek", "Jock"], "ethnos": ["White", "Latino"] }, "messages": [ { "_id": ObjectId("507f191e810c19729de860ea"), "text": "What brought you to dating apps?", "isUser": false, "timestamp": ISODate("2024-01-15T10:30:00Z") }, { "_id": ObjectId("507f191e810c19729de860eb"), "text": "I moved to San Francisco for work and didn't know anyone...", "image": "message/john_response_1234567890.png", "isUser": true, "timestamp": ISODate("2024-01-15T10:32:00Z") } ], "submitted": true, "approved": true } ``` --- ## πŸ’¬ Message Schema **Collection**: Embedded in `profiles.messages` array **Purpose**: Q&A conversation threads between interviewer and profile subject ### Schema | Field | Type | Required | Index | Default | Description | | ------------- | ------- | -------- | ----- | ------------ | ------------------------------------------ | | **text** | String | ❌ | ❌ | - | Message content | | **image** | String | ❌ | βœ… | - | Image file path (if message has photo) | | **isUser** | Boolean | βœ… | βœ… | `false` | `true` = user response, `false` = question | | **timestamp** | Date | ❌ | ❌ | `Date.now()` | Message timestamp | ### Message Types | `isUser` | Type | Purpose | Display | | --------- | -------- | ---------------------- | --------------------- | | **false** | Question | Interviewer's question | Left-aligned, bold | | **true** | Response | User's answer | Right-aligned, normal | ### Pre-Save Hooks **Image Processing** (both `save` and `findOneAndUpdate` hooks): 1. **Detects Image Data**: - Checks if `message.image` is an object (base64) 2. **Processes Image**: - Calls `Images.saveMessageImage()` - Decodes base64 data - Generates unique filename - Saves to `src/images/message/` - Returns path string 3. **Updates Document**: - Replaces base64 with file path - Continues with save ### Example Messages ```json [ { "_id": ObjectId("507f191e810c19729de860ea"), "text": "What's your most memorable dating app experience?", "isUser": false, "timestamp": ISODate("2024-01-15T10:30:00Z") }, { "_id": ObjectId("507f191e810c19729de860eb"), "text": "I matched with someone who shared my love of hiking...", "image": "message/hiking_photo_1234567890.png", "isUser": true, "timestamp": ISODate("2024-01-15T10:35:00Z") } ] ``` --- ## πŸ”„ Reset Collection **Collection Name**: `resets` **Purpose**: Password reset token management ### Schema | Field | Type | Required | Default | Description | | -------------- | -------- | -------- | ------------ | -------------------------- | | **user** | ObjectId | βœ… | - | Reference to `users._id` | | **expires** | Date | ❌ | `Date.now()` | Token expiration timestamp | | **used** | Boolean | ❌ | `false` | Token already consumed | | **updated_at** | Date | ❌ | `Date.now()` | Last update timestamp | ### Token Generation **HMAC-SHA1 Token**: ```javascript const secret = "Creepily hooking the gays up since 2008!"; const token = crypto .createHmac("sha1", secret) .update(userId + "|" + expires) .digest("hex"); ``` **Expiration**: Typically 1-24 hours from creation ### Reset Workflow 1. **User requests reset**: `POST /auth/reset` with username 2. **System creates reset document**: Links user, generates token, sets expiration 3. **Email sent**: Password reset link with `/auth/reset/:id/:token` 4. **User clicks link**: `GET /auth/reset/:id/:token` validates token 5. **User sets new password**: `PUT /auth/reset/:id/:token` with new password 6. **Token marked as used**: `used: true` prevents reuse ### Example Document ```json { "_id": ObjectId("507f1f77bcf86cd799439020"), "user": ObjectId("507f1f77bcf86cd799439011"), "expires": ISODate("2024-01-16T10:00:00Z"), "used": false, "updated_at": ISODate("2024-01-15T10:00:00Z") } ``` --- ## πŸ—ΊοΈ Geocache Collection **Collection Name**: `geocaches` **Purpose**: Cache Google Maps API geocoding results to reduce API calls ### Schema | Field | Type | Required | Unique | Default | Description | | ------------------- | -------- | -------- | ------ | --------- | ---------------------------------------------------- | | **key** | String | βœ… | βœ… | - | Original location string (e.g., "San Francisco, CA") | | **formatted** | String | βœ… | ❌ | - | Google's formatted address | | **loc.type** | String | ❌ | ❌ | `"Point"` | GeoJSON type | | **loc.coordinates** | [Number] | ❌ | ❌ | `[0, 0]` | [longitude, latitude] array | | **georesult** | Mixed | ❌ | ❌ | - | Full Google Maps API response | ### GeoJSON Structure MongoDB's geospatial queries require **GeoJSON Point** format: ```json { "type": "Point", "coordinates": [-122.4194, 37.7749] // [longitude, latitude] } ``` ⚠️ **Note**: Coordinates are `[lng, lat]`, not `[lat, lng]` ### Example Document ```json { "_id": ObjectId("507f1f77bcf86cd799439030"), "key": "San Francisco, CA", "formatted": "San Francisco, California, USA", "loc": { "type": "Point", "coordinates": [-122.4194, 37.7749] }, "georesult": { "address_components": [ ... ], "formatted_address": "San Francisco, California, USA", "geometry": { "location": { "lat": 37.7749, "lng": -122.4194 } }, "place_id": "ChIJIQBpAG2ahYAR_6128GcTUEo" } } ``` ### Usage **Geocoding Workflow**: 1. Check if location exists in geocache by `key` 2. If cached, return stored result (no API call) 3. If not cached: - Call Google Maps Geocoding API - Store result in geocache - Return result 4. Future requests for same location use cache **Benefits**: - Reduces API costs - Faster response times - Works offline for cached locations --- ## πŸ”— Entity Relationships ```mermaid erDiagram USER ||--o{ RESET : "can have" PROFILE ||--o{ MESSAGE : "contains" PROFILE }o--|| GEOCACHE : "references location" USER { ObjectId _id PK string username UK string password string email UK array can boolean forceReset } PROFILE { ObjectId _id PK number order object details array messages boolean submitted boolean approved } MESSAGE { ObjectId _id PK string text string image boolean isUser date timestamp } RESET { ObjectId _id PK ObjectId user FK date expires boolean used } GEOCACHE { ObjectId _id PK string key UK string formatted object loc object georesult } ``` ### Relationship Details | Relationship | Type | Description | | ---------------------- | ---------------------- | ---------------------------------------------- | | **User β†’ Reset** | One-to-Many | User can have multiple reset tokens (history) | | **Profile β†’ Message** | One-to-Many (Embedded) | Profile contains array of messages | | **Profile β†’ Geocache** | Many-to-One (Soft) | Profile location string may match geocache key | **Note**: No foreign key constraints in MongoDB. Relationships are application-level only. --- ## πŸ“‡ Indexes ### Current Indexes | Collection | Field | Type | Purpose | | ------------- | -------------- | -------- | -------------------------------------- | | **users** | `username` | Unique | Fast login lookups, prevent duplicates | | **users** | `email` | Unique | Email validation, prevent duplicates | | **profiles** | `details.age` | Standard | Age range filtering | | **profiles** | `details.name` | Standard | Name search/sorting | | **messages** | `image` | Standard | Find messages with images | | **messages** | `isUser` | Standard | Filter by message type | | **geocaches** | `key` | Unique | Location lookup cache | ### Index Usage Queries ```javascript // Fast age range query db.profiles .find({ "details.age": { $gte: 25, $lte: 35 }, }) .sort({ order: 1 }); // User login db.users.findOne({ username: "admin" }); // Geocache lookup db.geocaches.findOne({ key: "San Francisco, CA" }); // Messages with images db.profiles.find({ "messages.image": { $exists: true } }); ``` ### Recommended Additional Indexes For production optimization: ```javascript // Approved profiles (most common query) db.profiles.createIndex({ approved: 1, order: 1 }); // Submitted profiles (admin review) db.profiles.createIndex({ submitted: 1, approved: 1 }); // User permissions lookup db.users.createIndex({ can: 1 }); // Location geospatial queries db.geocaches.createIndex({ loc: "2dsphere" }); ``` --- ## πŸ–ΌοΈ Image Processing Hooks ### Overview Images are processed **automatically** via Mongoose pre-save hooks before documents are saved to MongoDB. **Supported Formats**: - Base64 encoded strings (from frontend) - Data URLs: `data:image/png;base64,iVBORw0KGg...` - File paths (strings are passed through) ### Processing Flow ```mermaid graph TD A[Document Save Triggered] --> B{Field is Base64?} B -->|Yes| C[Extract Base64 Data] B -->|No string path| D[Skip Processing] C --> E[Decode to Binary] E --> F[Generate Unique Filename] F --> G{Image Type?} G -->|Profile Detail| H[Save to src/images/profile/] G -->|Profile Thumb| I[Save to src/images/profile/] G -->|Message Image| J[Save to src/images/message/] H --> K[Return File Path] I --> K J --> K K --> L[Update Document Field] L --> M[Continue Save to MongoDB] D --> M ``` ### Image Modules **Location**: `modules/images.js` **Functions**: - `saveProfileDetailImage(data, callback)` - Processes full-size profile photos - `saveProfileThumbnailImage(data, callback)` - Processes profile thumbnails - `saveMessageImage(data, callback)` - Processes message attachments **Filename Format**: ``` __. Example: profile_aB3xF2_1705315200000.png ``` ### Storage Locations | Image Type | Directory | Example Path | | --------------------- | --------------------- | ------------------------------------ | | **Profile Detail** | `src/images/profile/` | `profile/john_detail_1234567890.png` | | **Profile Thumbnail** | `src/images/profile/` | `profile/john_thumb_1234567890.png` | | **Message Image** | `src/images/message/` | `message/response_1234567890.png` | | **Additional** | `src/images/cruise/` | `cruise/event_1234567890.png` | ### Error Handling If image processing fails: - **Error logged** via Winston logger - **Save continues** with original data - **Client receives error** in response **Example Error**: ```javascript Logger.error( "[MessageSchema.pre(save)] There was an error processing the message image.", { error: err, } ); ``` ### Volume Persistence **Development**: Images stored in DevContainer volume **Production**: Images stored in Docker volume `api_images` **Volume Mount**: ```yaml volumes: - api_images:/app/src/images ``` This ensures images persist across container restarts. --- ## πŸ“š Related Documentation - **[Backend README](README.md)** - Server setup and development - **[API Reference](API.md)** - Complete REST API documentation - **[Environment Variables](.env.example)** - Configuration - **[Root README](../README.md)** - Project overview - **[Deployment Guide](../DEPLOYMENT.md)** - Production deployment --- **Need Help?** Check the backend [troubleshooting section](README.md#troubleshooting) for database-related issues.