Documentation
This commit is contained in:
227
backend/.env.example
Normal file
227
backend/.env.example
Normal file
@@ -0,0 +1,227 @@
|
||||
# Looking Backend - Environment Variables
|
||||
# Copy this file to .env and fill in your values
|
||||
# NEVER commit .env files to version control
|
||||
|
||||
# ============================================
|
||||
# SERVER CONFIGURATION
|
||||
# ============================================
|
||||
|
||||
# Port for Express server to listen on
|
||||
# Default: 3069
|
||||
# Production: Use same port or configure via load balancer
|
||||
PORT=3069
|
||||
|
||||
# Node environment
|
||||
# Options: development, production, test
|
||||
NODE_ENV=development
|
||||
|
||||
# ============================================
|
||||
# DATABASE CONFIGURATION
|
||||
# ============================================
|
||||
|
||||
# MongoDB connection string
|
||||
# Development (DevContainer): mongodb://mongo:27017/urge
|
||||
# Development (Local): mongodb://localhost:27017/urge
|
||||
# Production: Use MongoDB Atlas or managed instance
|
||||
MONGODB_URI=mongodb://mongo:27017/urge
|
||||
|
||||
# MongoDB Admin Credentials (for connection if auth enabled)
|
||||
# Only needed if MongoDB requires authentication
|
||||
# MONGO_USER=admin
|
||||
# MONGO_PASS=password
|
||||
|
||||
# ============================================
|
||||
# JWT AUTHENTICATION
|
||||
# ============================================
|
||||
|
||||
# Secret key for JWT token signing
|
||||
# CRITICAL: Use a strong random string (minimum 32 characters)
|
||||
# Generate with: openssl rand -base64 32
|
||||
# NEVER share or commit this value
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this-to-random-32-chars
|
||||
|
||||
# JWT token expiration time
|
||||
# Options: '15m', '1h', '24h', '7d'
|
||||
# Default: 15m (15 minutes)
|
||||
JWT_EXPIRES_IN=15m
|
||||
|
||||
# ============================================
|
||||
# GOOGLE MAPS API
|
||||
# ============================================
|
||||
|
||||
# Google Maps API key for geocoding features
|
||||
# Get API key: https://console.cloud.google.com/apis/credentials
|
||||
# Enable: Geocoding API, Places API (if used)
|
||||
# Restrict: Set HTTP referrer or IP restrictions for security
|
||||
GOOGLE_MAPS_API_KEY=AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
# ============================================
|
||||
# EMAIL CONFIGURATION (SMTP)
|
||||
# ============================================
|
||||
|
||||
# SMTP server hostname
|
||||
# Examples:
|
||||
# Gmail: smtp.gmail.com
|
||||
# Outlook: smtp-mail.outlook.com
|
||||
# SendGrid: smtp.sendgrid.net
|
||||
# Custom: mail.yourdomain.com
|
||||
MAIL_HOST=smtp.gmail.com
|
||||
|
||||
# SMTP server port
|
||||
# Common ports:
|
||||
# 587 - TLS/STARTTLS (recommended)
|
||||
# 465 - SSL
|
||||
# 25 - Unencrypted (not recommended)
|
||||
MAIL_PORT=587
|
||||
|
||||
# SMTP username (usually your email address)
|
||||
MAIL_USER=support@example.com
|
||||
|
||||
# SMTP password or app-specific password
|
||||
# For Gmail: Use App Password (not your account password)
|
||||
# 1. Go to Google Account → Security → 2-Step Verification
|
||||
# 2. Scroll to "App passwords"
|
||||
# 3. Generate password for "Mail"
|
||||
# 4. Use that 16-character password here
|
||||
MAIL_PASS=your-email-password-or-app-specific-password
|
||||
|
||||
# Email sender name (displayed in "From" field)
|
||||
MAIL_FROM_NAME=Looking App Support
|
||||
|
||||
# Email sender address (must match MAIL_USER or authorized sender)
|
||||
MAIL_FROM_ADDRESS=support@example.com
|
||||
|
||||
# ============================================
|
||||
# CORS CONFIGURATION
|
||||
# ============================================
|
||||
|
||||
# Allowed origins for CORS (comma-separated)
|
||||
# Development: * (all origins)
|
||||
# Production: Specific domains only
|
||||
# Examples:
|
||||
# Development: *
|
||||
# Production: https://pfosi.mifi.dev,https://www.pfosi.mifi.dev
|
||||
CORS_ORIGIN=*
|
||||
|
||||
# ============================================
|
||||
# FILE UPLOAD CONFIGURATION
|
||||
# ============================================
|
||||
|
||||
# Maximum file size for image uploads (in bytes)
|
||||
# 5MB = 5242880 bytes
|
||||
# 10MB = 10485760 bytes
|
||||
MAX_FILE_SIZE=5242880
|
||||
|
||||
# Allowed image MIME types (comma-separated)
|
||||
ALLOWED_IMAGE_TYPES=image/jpeg,image/png,image/gif,image/webp
|
||||
|
||||
# ============================================
|
||||
# LOGGING CONFIGURATION
|
||||
# ============================================
|
||||
|
||||
# Log level
|
||||
# Options: error, warn, info, http, verbose, debug, silly
|
||||
# Production: info or warn
|
||||
# Development: debug or verbose
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# Log file location (if file logging enabled)
|
||||
# Default: logs/combined.log
|
||||
LOG_FILE=logs/combined.log
|
||||
|
||||
# Error log file location
|
||||
LOG_ERROR_FILE=logs/error.log
|
||||
|
||||
# ============================================
|
||||
# SECURITY CONFIGURATION
|
||||
# ============================================
|
||||
|
||||
# Password hashing iterations (PBKDF2)
|
||||
# Higher = more secure but slower
|
||||
# Default: 233335
|
||||
# Recommended: 100000+
|
||||
PASSWORD_HASH_ITERATIONS=233335
|
||||
|
||||
# Password minimum length
|
||||
PASSWORD_MIN_LENGTH=8
|
||||
|
||||
# Session secret for express-session (if using sessions)
|
||||
# SESSION_SECRET=your-session-secret-change-this
|
||||
|
||||
# ============================================
|
||||
# RATE LIMITING (if implemented)
|
||||
# ============================================
|
||||
|
||||
# Maximum requests per window
|
||||
# RATE_LIMIT_MAX=100
|
||||
|
||||
# Time window in milliseconds (15 minutes = 900000)
|
||||
# RATE_LIMIT_WINDOW_MS=900000
|
||||
|
||||
# ============================================
|
||||
# PRODUCTION DEPLOYMENT
|
||||
# ============================================
|
||||
|
||||
# Domain/hostname for the application
|
||||
# Used for email links, CORS, etc.
|
||||
# APP_URL=https://pfosi.mifi.dev
|
||||
# API_URL=https://api.pfosi.mifi.dev
|
||||
|
||||
# Traefik labels (if using docker-compose with Traefik)
|
||||
# TRAEFIK_ENABLE=true
|
||||
# TRAEFIK_DOMAIN=api.pfosi.mifi.dev
|
||||
|
||||
# ============================================
|
||||
# MONITORING & ANALYTICS (optional)
|
||||
# ============================================
|
||||
|
||||
# Sentry DSN for error tracking
|
||||
# SENTRY_DSN=https://xxxxx@sentry.io/xxxxx
|
||||
|
||||
# Google Analytics tracking ID
|
||||
# GA_TRACKING_ID=UA-XXXXXXXXX-X
|
||||
|
||||
# ============================================
|
||||
# DATABASE SEEDING
|
||||
# ============================================
|
||||
|
||||
# Path to seed data file
|
||||
# Default: data/profiles.json
|
||||
SEED_DATA_PATH=data/profiles.json
|
||||
|
||||
# Auto-seed database on startup (true/false)
|
||||
# WARNING: This will wipe existing data
|
||||
# Only use in development
|
||||
AUTO_SEED=false
|
||||
|
||||
# ============================================
|
||||
# FEATURE FLAGS (optional)
|
||||
# ============================================
|
||||
|
||||
# Enable user story submissions via public endpoint
|
||||
# ENABLE_SUBMISSIONS=true
|
||||
|
||||
# Require admin approval for submitted stories
|
||||
# REQUIRE_APPROVAL=true
|
||||
|
||||
# Enable email notifications for new submissions
|
||||
# NOTIFY_ON_SUBMISSION=true
|
||||
|
||||
# ============================================
|
||||
# NOTES
|
||||
# ============================================
|
||||
|
||||
# 1. NEVER commit this file with real values to version control
|
||||
# 2. Add .env to .gitignore (already done)
|
||||
# 3. Use different values for development and production
|
||||
# 4. Rotate secrets regularly in production
|
||||
# 5. Use environment-specific .env files:
|
||||
# - .env.development
|
||||
# - .env.production
|
||||
# - .env.test
|
||||
# 6. In production, use secret management tools:
|
||||
# - Docker secrets
|
||||
# - Kubernetes secrets
|
||||
# - AWS Secrets Manager
|
||||
# - Azure Key Vault
|
||||
# - HashiCorp Vault
|
||||
1029
backend/API.md
Normal file
1029
backend/API.md
Normal file
File diff suppressed because it is too large
Load Diff
652
backend/README.md
Normal file
652
backend/README.md
Normal file
@@ -0,0 +1,652 @@
|
||||
# Looking - Backend API
|
||||
|
||||
> **Express.js REST API with MongoDB for the Looking art project**
|
||||
|
||||
This is the backend REST API that powers the "Looking" application. Built with Express 4 and MongoDB 4.4, it provides endpoints for profile management, user authentication, messaging, and geolocation features.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Tech Stack](#tech-stack)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Data Flow](#data-flow)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Database Seeding](#database-seeding)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Related Documentation](#related-documentation)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
The backend API provides:
|
||||
|
||||
- **Authentication**: JWT-based user sessions with PBKDF2 password hashing
|
||||
- **Profile Management**: CRUD operations with approval workflow
|
||||
- **Messaging**: Conversation threads with image support
|
||||
- **Image Processing**: Automatic thumbnail and detail image generation
|
||||
- **Geolocation**: Google Maps API integration for location features
|
||||
- **Email**: Password reset and notification emails via Nodemailer
|
||||
|
||||
**Server Port**: 3069 (configurable via PORT environment variable)
|
||||
**Database**: MongoDB 4.4 on port 27017 (database name: `urge`)
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Tech Stack
|
||||
|
||||
| Technology | Version | Purpose |
|
||||
| ---------------- | ------- | ---------------------- |
|
||||
| **Express** | 4.14.0 | Web framework |
|
||||
| **Node.js** | 14.x | Runtime |
|
||||
| **MongoDB** | 4.4 | Database |
|
||||
| **Mongoose** | 4.7.4 | MongoDB ODM |
|
||||
| **jsonwebtoken** | 7.3.0 | JWT authentication |
|
||||
| **multer** | 1.2.0 | File upload handling |
|
||||
| **@google/maps** | 0.4.5 | Google Maps API client |
|
||||
| **nodemailer** | 4.0.1 | Email sending |
|
||||
| **winston** | 2.4.0 | Application logging |
|
||||
| **morgan** | 1.7.0 | HTTP request logging |
|
||||
| **moment** | 2.17.1 | Date/time manipulation |
|
||||
| **shortid** | 2.2.8 | Short ID generation |
|
||||
| **vcard-js** | 1.2.2 | VCard generation |
|
||||
|
||||
**Dev Tools:**
|
||||
|
||||
- **gulp** 3.9.1 - Task runner
|
||||
- **nodemon** 1.11.0 - Auto-restart on file changes
|
||||
- **mocha** 3.0.1 - Test framework
|
||||
- **chai** 3.5.0 - Assertion library
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── app.js # Express app configuration
|
||||
│ │
|
||||
│ ├── bin/
|
||||
│ │ └── www # Server startup script (port 3069)
|
||||
│ │
|
||||
│ ├── routes/ # API route handlers
|
||||
│ │ ├── auth.js # Authentication (login, logout, password reset)
|
||||
│ │ ├── profiles.js # Profile CRUD + approval workflow
|
||||
│ │ ├── users.js # User management
|
||||
│ │ └── geocache.js # Geolocation/geocoding
|
||||
│ │
|
||||
│ ├── models/ # Mongoose schemas
|
||||
│ │ ├── user.js # User schema (auth, permissions)
|
||||
│ │ ├── profile.js # Profile schema (details, messages)
|
||||
│ │ ├── message.js # Message schema (text, image, timestamp)
|
||||
│ │ ├── reset.js # Password reset token schema
|
||||
│ │ └── geocache.js # Location cache schema
|
||||
│ │
|
||||
│ ├── modules/ # Utility modules
|
||||
│ │ ├── authentication.js # PBKDF2 password hashing
|
||||
│ │ ├── token.js # JWT creation/verification
|
||||
│ │ ├── images.js # Image upload/processing (multer)
|
||||
│ │ ├── mailer.js # Email sending (nodemailer)
|
||||
│ │ ├── geocoder.js # Google Maps geocoding
|
||||
│ │ └── logger.js # Winston logging configuration
|
||||
│ │
|
||||
│ └── images/ # Uploaded image storage
|
||||
│ ├── profile/ # Profile photos (thumb + detail)
|
||||
│ ├── message/ # Message attachments
|
||||
│ └── cruise/ # Additional images
|
||||
│
|
||||
├── data/
|
||||
│ └── profiles.json # Seed data (source of truth)
|
||||
│
|
||||
├── package.json # Dependencies and scripts
|
||||
├── gulpfile.js # Gulp tasks (dev, test)
|
||||
└── Dockerfile # Docker build configuration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 14.x (included in DevContainer)
|
||||
- MongoDB 4.4 (included in DevContainer)
|
||||
- Environment variables configured (see [Environment Variables](#environment-variables))
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# From project root
|
||||
npm run install:all
|
||||
|
||||
# Or from backend directory
|
||||
cd backend
|
||||
npm install
|
||||
```
|
||||
|
||||
### Start Development Server
|
||||
|
||||
```bash
|
||||
# From project root (recommended)
|
||||
npm run dev:backend
|
||||
|
||||
# Or from backend directory
|
||||
cd backend
|
||||
npm run dev
|
||||
|
||||
# Or using gulp directly
|
||||
gulp
|
||||
```
|
||||
|
||||
Server starts on **port 3069** with auto-restart via nodemon.
|
||||
|
||||
### Verify Server is Running
|
||||
|
||||
```bash
|
||||
curl http://localhost:3069/profiles
|
||||
```
|
||||
|
||||
Expected: JSON array of profiles or 404 if no data seeded.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Data Flow
|
||||
|
||||
### Request Lifecycle
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Client HTTP Request] --> B[Express App app.js]
|
||||
B --> C[Morgan Logger logs request]
|
||||
C --> D[Body Parser parses JSON]
|
||||
D --> E[CORS Headers added]
|
||||
E --> F{Route Match?}
|
||||
F -->|Yes| G[Route Handler routes/*]
|
||||
F -->|No| H[404 Error Handler]
|
||||
G --> I{Auth Required?}
|
||||
I -->|Yes| J[Token.verifyThen]
|
||||
I -->|No| K[Execute Controller Logic]
|
||||
J --> L{Valid Token?}
|
||||
L -->|Yes| M{Has Permission?}
|
||||
L -->|No| N[403 Forbidden]
|
||||
M -->|Yes| K
|
||||
M -->|No| N
|
||||
K --> O[Mongoose Model models/*]
|
||||
O --> P[MongoDB Query]
|
||||
P --> Q[Pre-Save Hooks?]
|
||||
Q -->|Image Processing| R[modules/images]
|
||||
R --> S[Save to Filesystem]
|
||||
S --> T[Update Document Path]
|
||||
Q -->|No Hooks| U[Save to DB]
|
||||
T --> U
|
||||
U --> V[Return Response]
|
||||
V --> W[Winston Logger logs result]
|
||||
W --> X[Send JSON to Client]
|
||||
```
|
||||
|
||||
### Example: Message Creation with Image
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[POST /profiles/:id/message] --> B[Token Verification]
|
||||
B --> C[Extract message data from request body]
|
||||
C --> D[Message.save pre-hook triggered]
|
||||
D --> E{message.image is object?}
|
||||
E -->|Yes Base64 data| F[Images.saveMessageImage]
|
||||
E -->|No string path| G[Skip processing]
|
||||
F --> H[Decode Base64]
|
||||
H --> I[Generate unique filename]
|
||||
I --> J[Write to src/images/message/]
|
||||
J --> K[Return filename path]
|
||||
K --> L[Update message.image = filename]
|
||||
L --> M[Save to MongoDB]
|
||||
G --> M
|
||||
M --> N[Emit event to route handler]
|
||||
N --> O[Return 200 with message data]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Environment Variables
|
||||
|
||||
**Required environment variables** must be configured in [`.env.example`](.env.example).
|
||||
|
||||
### Create Environment File
|
||||
|
||||
```bash
|
||||
# Copy example file
|
||||
cp .env.example .env
|
||||
|
||||
# Edit with your values
|
||||
nano .env
|
||||
```
|
||||
|
||||
### Required Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
| ----------------------- | --------------------------------- | ------------------------------------ |
|
||||
| **PORT** | Server port | `3069` |
|
||||
| **MONGODB_URI** | MongoDB connection string | `mongodb://mongo:27017/urge` |
|
||||
| **JWT_SECRET** | Secret key for JWT signing | `your-super-secret-key-min-32-chars` |
|
||||
| **GOOGLE_MAPS_API_KEY** | Google Maps API key for geocoding | `AIzaSy...` |
|
||||
| **MAIL_HOST** | SMTP server hostname | `smtp.gmail.com` |
|
||||
| **MAIL_PORT** | SMTP server port | `587` |
|
||||
| **MAIL_USER** | SMTP username | `support@example.com` |
|
||||
| **MAIL_PASS** | SMTP password | `your-password` |
|
||||
|
||||
### Security Notes
|
||||
|
||||
- **JWT_SECRET**: Use a random 32+ character string. Generate with:
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
- **MAIL_PASS**: Use app-specific passwords for Gmail/G Suite
|
||||
- **GOOGLE_MAPS_API_KEY**: Restrict API key to your backend IP/domain
|
||||
- **Never commit** `.env` files to version control
|
||||
|
||||
📄 **See [.env.example](.env.example) for complete configuration with descriptions**
|
||||
|
||||
---
|
||||
|
||||
## 💾 Database Seeding
|
||||
|
||||
### ⚠️ Important: Data Entry Workflow
|
||||
|
||||
**This application does NOT support interactive data entry through the UI.** All profile and message data must be added to the seed file and database reseeded.
|
||||
|
||||
### Seed Data Location
|
||||
|
||||
**Source of Truth**: [`data/profiles.json`](data/profiles.json)
|
||||
|
||||
### How to Add/Edit Profiles
|
||||
|
||||
1. **Edit `data/profiles.json`**:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"order": 1,
|
||||
"details": {
|
||||
"name": "John",
|
||||
"age": 28,
|
||||
"location": "San Francisco, CA",
|
||||
"about": "User's story...",
|
||||
"pic": {
|
||||
"thumb": "profile/john_thumbnail.png",
|
||||
"detail": "profile/john_detail.png"
|
||||
},
|
||||
"position": ["Top", "Versatile"],
|
||||
"looking": ["Dates", "Friends"],
|
||||
"tribes": ["Geek", "Jock"],
|
||||
"ethnos": ["White", "Latino"]
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"text": "What brought you to dating apps?",
|
||||
"isUser": false,
|
||||
"timestamp": "2024-01-15T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"text": "I moved to a new city and wanted to meet people...",
|
||||
"isUser": true,
|
||||
"timestamp": "2024-01-15T10:32:00Z"
|
||||
}
|
||||
],
|
||||
"submitted": true,
|
||||
"approved": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
2. **Add images** to `src/images/profile/` and `src/images/message/`
|
||||
|
||||
3. **Run seed script**:
|
||||
|
||||
```bash
|
||||
# From project root
|
||||
npm run seed
|
||||
|
||||
# Or from backend directory
|
||||
cd backend
|
||||
npm run seed
|
||||
```
|
||||
|
||||
4. **Verify data loaded**:
|
||||
```bash
|
||||
curl http://localhost:3069/profiles
|
||||
```
|
||||
|
||||
### Seed Script Behavior
|
||||
|
||||
- **Drops existing database** (all data wiped)
|
||||
- **Creates fresh collections** from schema
|
||||
- **Loads data** from `data/profiles.json`
|
||||
- **Processes images** via pre-save hooks
|
||||
- **No backup created** - commit changes to Git first
|
||||
|
||||
### Production Deployment
|
||||
|
||||
⚠️ **Database is wiped and reseeded on each deployment**. All content changes must be committed to `data/profiles.json` before deploying.
|
||||
|
||||
If the application becomes interactive with user-generated content, implement proper backup strategies (see [DEPLOYMENT.md](../DEPLOYMENT.md)).
|
||||
|
||||
---
|
||||
|
||||
## 💻 Development Workflow
|
||||
|
||||
### Gulp Tasks
|
||||
|
||||
The project uses **Gulp** for development automation.
|
||||
|
||||
**Default task** (auto-restart on changes):
|
||||
|
||||
```bash
|
||||
gulp
|
||||
```
|
||||
|
||||
This runs:
|
||||
|
||||
- **nodemon** - Restarts server on `.js` file changes
|
||||
- **mocha** - Runs tests on changes
|
||||
- Watches `src/**/*.js` for changes
|
||||
|
||||
**Test task**:
|
||||
|
||||
```bash
|
||||
gulp test
|
||||
```
|
||||
|
||||
Runs Mocha tests with "nyan" reporter (cat animation 🐱).
|
||||
|
||||
### Manual Development
|
||||
|
||||
Without Gulp:
|
||||
|
||||
```bash
|
||||
# Start with nodemon
|
||||
nodemon src/bin/www
|
||||
|
||||
# Or plain Node.js
|
||||
node src/bin/www
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Or with Gulp
|
||||
gulp test
|
||||
```
|
||||
|
||||
**Note**: Test coverage is limited. Most tests are in backend, none in frontend.
|
||||
|
||||
### Logging
|
||||
|
||||
**Winston logger** outputs to:
|
||||
|
||||
- **Console**: Colorized logs during development
|
||||
- **Files**: `logs/error.log`, `logs/combined.log` (if configured)
|
||||
|
||||
**Morgan HTTP logger** logs all requests:
|
||||
|
||||
```
|
||||
GET /profiles 200 45ms - 2.5kb
|
||||
POST /auth/login 401 12ms - 87b
|
||||
```
|
||||
|
||||
### Code Linting
|
||||
|
||||
```bash
|
||||
# From project root
|
||||
npm run lint:backend
|
||||
|
||||
# Or from backend directory
|
||||
cd backend
|
||||
npm run lint
|
||||
```
|
||||
|
||||
**Note**: No ESLint config in backend currently. Consider adding:
|
||||
|
||||
```bash
|
||||
npm install -D eslint
|
||||
npx eslint --init
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### MongoDB Connection Failed
|
||||
|
||||
**Error:**
|
||||
|
||||
```
|
||||
MongooseServerSelectionError: connect ECONNREFUSED 127.0.0.1:27017
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Verify MongoDB is running**:
|
||||
|
||||
```bash
|
||||
docker ps | grep mongo
|
||||
```
|
||||
|
||||
2. **Check connection string**:
|
||||
|
||||
```bash
|
||||
echo $MONGODB_URI
|
||||
# Should be: mongodb://mongo:27017/urge (DevContainer)
|
||||
# Or: mongodb://localhost:27017/urge (local)
|
||||
```
|
||||
|
||||
3. **Test connection**:
|
||||
|
||||
```bash
|
||||
docker exec -it <mongo-container> mongo --eval "db.adminCommand('ping')"
|
||||
```
|
||||
|
||||
4. **Restart MongoDB**:
|
||||
```bash
|
||||
docker-compose -f .devcontainer/docker-compose.yml restart mongo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### JWT Token Invalid
|
||||
|
||||
**Error:**
|
||||
|
||||
```
|
||||
JsonWebTokenError: invalid signature
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Verify JWT_SECRET is set**:
|
||||
|
||||
```bash
|
||||
echo $JWT_SECRET
|
||||
```
|
||||
|
||||
2. **Check .env file exists**:
|
||||
|
||||
```bash
|
||||
ls -la .env
|
||||
```
|
||||
|
||||
3. **Restart backend** after changing environment variables:
|
||||
|
||||
```bash
|
||||
npm run dev:backend
|
||||
```
|
||||
|
||||
4. **Generate new token** by logging in again
|
||||
|
||||
---
|
||||
|
||||
### Image Upload Fails
|
||||
|
||||
**Error:**
|
||||
|
||||
```
|
||||
ENOENT: no such file or directory, open 'src/images/profile/...'
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Create image directories**:
|
||||
|
||||
```bash
|
||||
mkdir -p src/images/profile src/images/message src/images/cruise
|
||||
```
|
||||
|
||||
2. **Check permissions**:
|
||||
|
||||
```bash
|
||||
chmod -R 755 src/images
|
||||
```
|
||||
|
||||
3. **Verify volume mount** in DevContainer:
|
||||
|
||||
```bash
|
||||
ls -la src/images
|
||||
```
|
||||
|
||||
4. **Check multer configuration** in `modules/images.js`
|
||||
|
||||
---
|
||||
|
||||
### Port 3069 Already in Use
|
||||
|
||||
**Error:**
|
||||
|
||||
```
|
||||
Error: listen EADDRINUSE: address already in use :::3069
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
```bash
|
||||
# Find process using port 3069
|
||||
lsof -i :3069
|
||||
|
||||
# Kill the process
|
||||
kill -9 <PID>
|
||||
|
||||
# Or use different port
|
||||
export PORT=3070
|
||||
npm run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Seed Script Fails
|
||||
|
||||
**Error:**
|
||||
|
||||
```
|
||||
Error: Cannot find module 'data/profiles.json'
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Verify file exists**:
|
||||
|
||||
```bash
|
||||
ls -la data/profiles.json
|
||||
```
|
||||
|
||||
2. **Check JSON syntax**:
|
||||
|
||||
```bash
|
||||
node -e "JSON.parse(require('fs').readFileSync('data/profiles.json'))"
|
||||
```
|
||||
|
||||
3. **Run from correct directory**:
|
||||
```bash
|
||||
cd backend
|
||||
npm run seed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Google Maps API Errors
|
||||
|
||||
**Error:**
|
||||
|
||||
```
|
||||
REQUEST_DENIED: The provided API key is invalid
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Set API key in environment**:
|
||||
|
||||
```bash
|
||||
export GOOGLE_MAPS_API_KEY="your-key-here"
|
||||
```
|
||||
|
||||
2. **Enable APIs** in Google Cloud Console:
|
||||
|
||||
- Geocoding API
|
||||
- Places API (if used)
|
||||
|
||||
3. **Check API restrictions** (IP/domain whitelisting)
|
||||
|
||||
4. **Verify billing enabled** (Google requires it even for free tier)
|
||||
|
||||
---
|
||||
|
||||
### Email Sending Fails
|
||||
|
||||
**Error:**
|
||||
|
||||
```
|
||||
Invalid login: 535-5.7.8 Username and Password not accepted
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Use app-specific password** (Gmail):
|
||||
|
||||
- Go to Google Account → Security → 2-Step Verification → App passwords
|
||||
- Generate password for "Mail"
|
||||
- Use that password in MAIL_PASS
|
||||
|
||||
2. **Verify SMTP settings**:
|
||||
|
||||
```bash
|
||||
echo $MAIL_HOST $MAIL_PORT $MAIL_USER
|
||||
```
|
||||
|
||||
3. **Test SMTP connection**:
|
||||
|
||||
```bash
|
||||
telnet $MAIL_HOST $MAIL_PORT
|
||||
```
|
||||
|
||||
4. **Enable "Less secure app access"** (not recommended, use app passwords instead)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[API Reference](API.md)** - Complete REST API endpoint documentation
|
||||
- **[Database Schema](SCHEMA.md)** - MongoDB collection schemas and relationships
|
||||
- **[Environment Variables](.env.example)** - Configuration template
|
||||
- **[Root README](../README.md)** - Project overview and quick start
|
||||
- **[Frontend README](../app/README.md)** - Ionic app documentation
|
||||
- **[DevContainer Guide](../.devcontainer/README.md)** - Development environment
|
||||
- **[Deployment Guide](../DEPLOYMENT.md)** - Production deployment
|
||||
|
||||
---
|
||||
|
||||
**Need Help?** Check the [root troubleshooting section](../README.md#troubleshooting) or API documentation for endpoint-specific issues.
|
||||
619
backend/SCHEMA.md
Normal file
619
backend/SCHEMA.md
Normal file
@@ -0,0 +1,619 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user