620 lines
20 KiB
Markdown
620 lines
20 KiB
Markdown
# 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/<filename>`
|
|
- 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/<filename>`
|
|
- 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**:
|
|
|
|
```
|
|
<type>_<shortid>_<timestamp>.<ext>
|
|
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.
|