Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5101a05bca | |||
| c82caf6539 | |||
| ae1b508726 | |||
| 26c05912fc | |||
| d1033cb83a | |||
| b7458b5a37 | |||
| face8f1496 | |||
| ba6ee55bf7 | |||
| e343106af5 | |||
| 2f848eb602 | |||
| 1c7fa781ad | |||
| 66940c2f5b | |||
| a3638fe954 |
+28
-20
@@ -1,26 +1,34 @@
|
||||
# JWT Secret for authentication
|
||||
# IMPORTANT: Generate a secure random secret with: openssl rand -hex 32
|
||||
# NEVER use the default value or commit the actual secret to version control
|
||||
JWT_SECRET=CHANGE_ME_TO_A_64_CHARACTER_SECURE_RANDOM_STRING_GENERATED_BY_OPENSSL
|
||||
# Environment Configuration Template
|
||||
# Copy this file to .env and adjust values for your environment
|
||||
|
||||
# URLs
|
||||
ADMIN_URL=https://admin.photos.yourdomain.com
|
||||
FRONTEND_URL=https://photos.yourdomain.com
|
||||
# Development: Use docker-compose.dev.yml
|
||||
# Production: Use docker-compose.prod.yml with .env.production.example
|
||||
|
||||
# Database (for PostgreSQL in production)
|
||||
DB_USER=photoapp
|
||||
DB_PASSWORD=secure-password-here
|
||||
DB_NAME=photo_sharing
|
||||
# JWT Secret (CRITICAL for production)
|
||||
# Generate with: openssl rand -base64 32
|
||||
JWT_SECRET=dev-secret-change-in-production
|
||||
|
||||
# Application URLs
|
||||
ADMIN_URL=http://localhost:3005
|
||||
FRONTEND_URL=http://localhost:3005
|
||||
|
||||
# Database Configuration
|
||||
# SQLite is used for development by default
|
||||
# For production PostgreSQL config, see .env.production.example
|
||||
DATABASE_CLIENT=sqlite3
|
||||
DATABASE_PATH=./data/photo_sharing.db
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
# Development: Uses Mailhog (included in docker-compose.dev.yml)
|
||||
# Production: Configure real SMTP server
|
||||
SMTP_HOST=mailhog
|
||||
SMTP_PORT=1025
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
EMAIL_FROM=noreply@localhost
|
||||
|
||||
# Umami Analytics
|
||||
UMAMI_URL=https://analytics.yourdomain.com
|
||||
UMAMI_WEBSITE_ID=your-website-id
|
||||
UMAMI_HASH_SALT=random-salt-here
|
||||
# Optional: Umami Analytics
|
||||
UMAMI_URL=
|
||||
UMAMI_WEBSITE_ID=
|
||||
UMAMI_HASH_SALT=
|
||||
+18
-46
@@ -1,60 +1,32 @@
|
||||
# Production Environment Configuration Template
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# Application URLs
|
||||
FRONTEND_HOST=photos.yourdomain.com
|
||||
BACKEND_HOST=api.photos.yourdomain.com
|
||||
ADMIN_URL=https://admin.photos.yourdomain.com
|
||||
FRONTEND_URL=https://photos.yourdomain.com
|
||||
ADMIN_URL=https://yourdomain.com
|
||||
FRONTEND_URL=https://yourdomain.com
|
||||
|
||||
# Database Configuration
|
||||
DB_NAME=photo_sharing
|
||||
DB_USER=photoapp
|
||||
DB_PASSWORD=your-secure-password-here
|
||||
# Security - CRITICAL: Generate a secure random JWT secret
|
||||
# You can generate one with: openssl rand -base64 32
|
||||
JWT_SECRET=your-secure-random-jwt-secret-here
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-jwt-secret-here
|
||||
# Database Configuration (PostgreSQL)
|
||||
DB_USER=picpeak
|
||||
DB_PASSWORD=your-secure-database-password
|
||||
DB_NAME=picpeak
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_SECURE=true
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
|
||||
# Umami Analytics
|
||||
# Umami Analytics (Optional)
|
||||
UMAMI_URL=https://analytics.yourdomain.com
|
||||
UMAMI_HOST=analytics.yourdomain.com
|
||||
UMAMI_WEBSITE_ID=your-website-id
|
||||
UMAMI_HASH_SALT=your-random-salt
|
||||
UMAMI_DB_PASSWORD=umami-db-password
|
||||
UMAMI_HASH_SALT=your-random-hash-salt
|
||||
|
||||
# Traefik Configuration
|
||||
TRAEFIK_HOST=traefik.yourdomain.com
|
||||
ACME_EMAIL=admin@yourdomain.com
|
||||
TRAEFIK_DASHBOARD_AUTH=admin:$2y$10$... # Use htpasswd to generate
|
||||
|
||||
# Docker Registry (optional)
|
||||
REGISTRY_URL=registry.yourdomain.com
|
||||
VERSION=latest
|
||||
|
||||
# Monitoring
|
||||
DOMAIN=yourdomain.com
|
||||
GRAFANA_USER=admin
|
||||
GRAFANA_PASSWORD=your-grafana-password
|
||||
|
||||
# OAuth Configuration (optional)
|
||||
OAUTH_AUTH_URL=https://auth.yourdomain.com/oauth2/auth
|
||||
OAUTH_TOKEN_URL=https://auth.yourdomain.com/oauth2/token
|
||||
OAUTH_USER_URL=https://auth.yourdomain.com/oauth2/userinfo
|
||||
OAUTH_CLIENT_ID=photo-sharing
|
||||
OAUTH_CLIENT_SECRET=your-oauth-secret
|
||||
OAUTH_SECRET=your-random-secret
|
||||
COOKIE_DOMAIN=.yourdomain.com
|
||||
OAUTH_WHITELIST=admin@yourdomain.com
|
||||
|
||||
# Backup Configuration (optional)
|
||||
S3_BACKUP_BUCKET=your-backup-bucket
|
||||
|
||||
# Drone CI Configuration
|
||||
DRONE_RPC_SECRET=your-drone-secret
|
||||
DRONE_GITHUB_CLIENT_ID=your-github-client-id
|
||||
DRONE_GITHUB_CLIENT_SECRET=your-github-client-secret
|
||||
# First Admin User (for initial setup)
|
||||
# Run: docker-compose exec backend node scripts/create-admin.js --email admin@yourdomain.com
|
||||
ADMIN_EMAIL=admin@yourdomain.com
|
||||
+287
-424
@@ -1,483 +1,346 @@
|
||||
# Photo Sharing Platform - Production Deployment Guide
|
||||
# PicPeak Deployment Guide
|
||||
|
||||
This guide covers deploying the photo sharing platform using Docker Swarm, Traefik, and Drone CI/CD.
|
||||
This guide covers deploying PicPeak for development and production environments.
|
||||
|
||||
## Table of Contents
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Infrastructure Setup](#infrastructure-setup)
|
||||
- [Docker Swarm Setup](#docker-swarm-setup)
|
||||
- [Traefik Setup](#traefik-setup)
|
||||
- [Application Deployment](#application-deployment)
|
||||
- [CI/CD with Drone](#cicd-with-drone)
|
||||
- [Monitoring](#monitoring)
|
||||
- [Backup and Recovery](#backup-and-recovery)
|
||||
- [Quick Start (Development)](#quick-start-development)
|
||||
- [Production Deployment](#production-deployment)
|
||||
- [Admin User Setup](#admin-user-setup)
|
||||
- [Configuration Reference](#configuration-reference)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Prerequisites
|
||||
## Quick Start (Development)
|
||||
|
||||
### Hardware Requirements
|
||||
- **Manager Node**: 2 CPU cores, 4GB RAM, 50GB storage
|
||||
- **Worker Nodes**: 2 CPU cores, 2GB RAM, 20GB storage
|
||||
- **Storage**: SSD recommended for database and photo storage
|
||||
|
||||
### Software Requirements
|
||||
- Ubuntu 20.04+ or similar Linux distribution
|
||||
- Docker Engine 20.10+
|
||||
- Docker Compose 2.0+
|
||||
- Git
|
||||
- SSL certificates (automated with Let's Encrypt)
|
||||
|
||||
### Network Requirements
|
||||
- Ports 80, 443 open for web traffic
|
||||
- Port 2377 for Swarm management
|
||||
- Ports 7946, 4789 for Swarm networking
|
||||
- Static IP or reliable dynamic DNS
|
||||
|
||||
## Infrastructure Setup
|
||||
|
||||
### 1. Install Docker
|
||||
### 1. Clone and Setup
|
||||
|
||||
```bash
|
||||
# Install Docker
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
git clone https://github.com/yourusername/picpeak.git
|
||||
cd picpeak
|
||||
|
||||
# Add user to docker group
|
||||
sudo usermod -aG docker $USER
|
||||
|
||||
# Enable Docker service
|
||||
sudo systemctl enable docker
|
||||
sudo systemctl start docker
|
||||
```
|
||||
|
||||
### 2. Configure Firewall
|
||||
|
||||
```bash
|
||||
# Allow Docker Swarm ports
|
||||
sudo ufw allow 2377/tcp
|
||||
sudo ufw allow 7946/tcp
|
||||
sudo ufw allow 7946/udp
|
||||
sudo ufw allow 4789/udp
|
||||
|
||||
# Allow web traffic
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 443/tcp
|
||||
```
|
||||
|
||||
## Docker Swarm Setup
|
||||
|
||||
### 1. Initialize Swarm
|
||||
|
||||
On the manager node:
|
||||
|
||||
```bash
|
||||
cd deploy/scripts
|
||||
sudo ./init-swarm.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
- Initialize Docker Swarm
|
||||
- Create overlay networks
|
||||
- Label nodes for service placement
|
||||
- Create required directories
|
||||
|
||||
### 2. Join Worker Nodes
|
||||
|
||||
On each worker node, run the join command displayed by the init script:
|
||||
|
||||
```bash
|
||||
docker swarm join --token SWMTKN-1-xxx... manager-ip:2377
|
||||
```
|
||||
|
||||
### 3. Verify Swarm
|
||||
|
||||
```bash
|
||||
docker node ls
|
||||
```
|
||||
|
||||
## Application Configuration
|
||||
|
||||
### 1. Environment Setup
|
||||
|
||||
```bash
|
||||
# Copy environment template
|
||||
cp .env.production.example .env.production
|
||||
cp .env.example .env
|
||||
|
||||
# Edit with your values
|
||||
nano .env.production
|
||||
```
|
||||
|
||||
Required configurations:
|
||||
- Domain names for frontend, backend, and services
|
||||
- SMTP credentials for email
|
||||
- Database passwords
|
||||
- JWT secrets
|
||||
|
||||
### 2. Create Docker Secrets
|
||||
|
||||
```bash
|
||||
cd deploy/scripts
|
||||
./create-secrets.sh
|
||||
```
|
||||
|
||||
This will create all required secrets in Docker Swarm. Save the generated passwords!
|
||||
|
||||
## Traefik Setup
|
||||
|
||||
### 1. Deploy Traefik
|
||||
|
||||
```bash
|
||||
cd deploy/traefik
|
||||
|
||||
# Create traefik network
|
||||
docker network create --driver overlay traefik-public
|
||||
|
||||
# Deploy Traefik stack
|
||||
docker stack deploy -c docker-compose.traefik.yml traefik
|
||||
```
|
||||
|
||||
### 2. Verify Traefik
|
||||
|
||||
```bash
|
||||
# Check service status
|
||||
docker service ls | grep traefik
|
||||
|
||||
# View logs
|
||||
docker service logs traefik_traefik
|
||||
```
|
||||
|
||||
Access Traefik dashboard at: `https://traefik.yourdomain.com/dashboard/`
|
||||
|
||||
## Application Deployment
|
||||
|
||||
### 1. Build Images (if using local registry)
|
||||
|
||||
```bash
|
||||
# Build frontend
|
||||
cd frontend
|
||||
docker build -t photo-sharing-frontend:latest .
|
||||
|
||||
# Build backend
|
||||
cd ../backend
|
||||
docker build -t photo-sharing-backend:latest .
|
||||
```
|
||||
|
||||
### 2. Deploy Application Stack
|
||||
|
||||
```bash
|
||||
cd deploy/scripts
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--env FILE`: Specify environment file
|
||||
- `--registry URL`: Docker registry URL
|
||||
- `--version VERSION`: Image version to deploy
|
||||
|
||||
### 3. Verify Deployment
|
||||
|
||||
```bash
|
||||
# Check all services
|
||||
docker service ls
|
||||
|
||||
# Check specific service
|
||||
docker service ps photo-sharing_backend
|
||||
|
||||
# View logs
|
||||
docker service logs photo-sharing_backend -f
|
||||
```
|
||||
|
||||
### 4. Run Database Migrations
|
||||
|
||||
The deploy script automatically runs migrations, but you can run manually:
|
||||
|
||||
```bash
|
||||
docker exec $(docker ps -q -f name=photo-sharing_backend) npm run migrate
|
||||
```
|
||||
|
||||
## CI/CD with Drone
|
||||
|
||||
### 1. Drone Server Setup
|
||||
|
||||
Deploy Drone server on your CI infrastructure:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
--volume=/var/lib/drone:/data \
|
||||
--env=DRONE_GITHUB_CLIENT_ID=your-id \
|
||||
--env=DRONE_GITHUB_CLIENT_SECRET=your-secret \
|
||||
--env=DRONE_RPC_SECRET=your-rpc-secret \
|
||||
--env=DRONE_SERVER_HOST=drone.yourdomain.com \
|
||||
--env=DRONE_SERVER_PROTO=https \
|
||||
--publish=80:80 \
|
||||
--publish=443:443 \
|
||||
--restart=always \
|
||||
--detach=true \
|
||||
--name=drone \
|
||||
drone/drone:2
|
||||
```
|
||||
|
||||
### 2. Drone Runner Setup
|
||||
|
||||
On build servers:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e DRONE_RPC_PROTO=https \
|
||||
-e DRONE_RPC_HOST=drone.yourdomain.com \
|
||||
-e DRONE_RPC_SECRET=your-rpc-secret \
|
||||
-e DRONE_RUNNER_CAPACITY=2 \
|
||||
-e DRONE_RUNNER_NAME=runner-1 \
|
||||
-p 3000:3000 \
|
||||
--restart always \
|
||||
--name runner \
|
||||
drone/drone-runner-docker:1
|
||||
```
|
||||
|
||||
### 3. Repository Setup
|
||||
|
||||
1. Enable repository in Drone UI
|
||||
2. Add secrets in Drone:
|
||||
- `docker_username`
|
||||
- `docker_password`
|
||||
- `docker_registry`
|
||||
- `staging_swarm_host`
|
||||
- `staging_swarm_user`
|
||||
- `staging_swarm_key`
|
||||
- `prod_swarm_host`
|
||||
- `prod_swarm_user`
|
||||
- `prod_swarm_key`
|
||||
- `slack_webhook`
|
||||
|
||||
### 4. Deployment Workflow
|
||||
|
||||
- Push to `develop` → Deploy to staging
|
||||
- Create tag → Deploy to production
|
||||
- Automatic rollback on failure
|
||||
|
||||
## Monitoring
|
||||
|
||||
### 1. Deploy Monitoring Stack
|
||||
|
||||
```bash
|
||||
cd deploy/monitoring
|
||||
|
||||
# Deploy monitoring services
|
||||
docker stack deploy -c docker-compose.monitoring.yml monitoring
|
||||
# Start development environment
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
### 2. Access Services
|
||||
|
||||
- Grafana: `https://grafana.yourdomain.com`
|
||||
- Prometheus: `https://prometheus.yourdomain.com`
|
||||
- Alertmanager: `https://alerts.yourdomain.com`
|
||||
- Frontend: http://localhost:3005
|
||||
- Backend API: http://localhost:3001
|
||||
- MailHog (email testing): http://localhost:8025
|
||||
|
||||
### 3. Configure Alerts
|
||||
### 3. Create Admin User
|
||||
|
||||
Create alert rules in `deploy/monitoring/alerts/`:
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml exec backend node scripts/create-admin.js \
|
||||
--email admin@localhost \
|
||||
--username admin \
|
||||
--password admin123
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- Domain with DNS configured
|
||||
- SSL/TLS handled by reverse proxy (Traefik, Nginx, etc.)
|
||||
|
||||
### 1. Environment Setup
|
||||
|
||||
```bash
|
||||
# Copy production template
|
||||
cp .env.production.example .env
|
||||
|
||||
# Generate secure secrets
|
||||
echo "JWT_SECRET=$(openssl rand -base64 32)" >> .env
|
||||
echo "DB_PASSWORD=$(openssl rand -base64 24)" >> .env
|
||||
```
|
||||
|
||||
Edit `.env` with your configuration:
|
||||
|
||||
```env
|
||||
# Your domain
|
||||
ADMIN_URL=https://yourdomain.com
|
||||
FRONTEND_URL=https://yourdomain.com
|
||||
|
||||
# Database (PostgreSQL)
|
||||
DB_USER=picpeak
|
||||
DB_NAME=picpeak
|
||||
# DB_PASSWORD already generated above
|
||||
|
||||
# Email
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=true
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
### 2. Frontend Configuration
|
||||
|
||||
```bash
|
||||
# Configure frontend for production
|
||||
echo "VITE_API_URL=/api" > frontend/.env.production
|
||||
```
|
||||
|
||||
### 3. Deploy with Docker Compose
|
||||
|
||||
```bash
|
||||
# Build and start services
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# Check status
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# View logs
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
```
|
||||
|
||||
### 4. Deploy with Traefik
|
||||
|
||||
If using Traefik, create `docker-compose.override.yml`:
|
||||
|
||||
```yaml
|
||||
groups:
|
||||
- name: photo-sharing
|
||||
rules:
|
||||
- alert: ServiceDown
|
||||
expr: up{job="photo-sharing-backend"} == 0
|
||||
for: 5m
|
||||
annotations:
|
||||
summary: "Photo sharing backend is down"
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.picpeak.rule=Host(`yourdomain.com`)"
|
||||
- "traefik.http.routers.picpeak.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak.loadbalancer.server.port=80"
|
||||
networks:
|
||||
- traefik
|
||||
- picpeak
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
```
|
||||
|
||||
## Backup and Recovery
|
||||
## Admin User Setup
|
||||
|
||||
### 1. Automated Backups
|
||||
### Create First Admin
|
||||
|
||||
Set up cron job for automated backups:
|
||||
After deployment, create your admin user:
|
||||
|
||||
```bash
|
||||
# Edit crontab
|
||||
crontab -e
|
||||
# Production
|
||||
docker-compose -f docker-compose.prod.yml exec backend node scripts/create-admin.js \
|
||||
--email admin@yourdomain.com \
|
||||
--username admin \
|
||||
--password yourSecurePassword
|
||||
|
||||
# Add daily backup at 2 AM
|
||||
0 2 * * * /opt/photo-sharing/deploy/scripts/backup.sh
|
||||
# Auto-generate password
|
||||
docker-compose -f docker-compose.prod.yml exec backend node scripts/create-admin.js \
|
||||
--email admin@yourdomain.com
|
||||
```
|
||||
|
||||
### 2. Manual Backup
|
||||
The script will display:
|
||||
- ✅ Admin user created successfully!
|
||||
- Email: admin@yourdomain.com
|
||||
- Username: admin
|
||||
- Login URL: https://yourdomain.com/admin/login
|
||||
- Password: (save this if auto-generated!)
|
||||
|
||||
### Managing Admin Users
|
||||
|
||||
```bash
|
||||
cd deploy/scripts
|
||||
./backup.sh
|
||||
# List admin users
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||
-c "SELECT id, username, email, is_active, last_login FROM admin_users;"
|
||||
|
||||
# Deactivate user
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||
-c "UPDATE admin_users SET is_active = false WHERE email = 'user@example.com';"
|
||||
```
|
||||
|
||||
### 3. Restore from Backup
|
||||
## Configuration Reference
|
||||
|
||||
### Database Configuration
|
||||
|
||||
PicPeak automatically detects the environment and uses:
|
||||
- **Development**: SQLite (`./data/photo_sharing.db`)
|
||||
- **Production**: PostgreSQL (configured via environment variables)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
#### Required for Production
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `JWT_SECRET` | JWT signing key | `openssl rand -base64 32` |
|
||||
| `DB_PASSWORD` | PostgreSQL password | `openssl rand -base64 24` |
|
||||
| `ADMIN_URL` | Admin panel URL | `https://yourdomain.com` |
|
||||
| `FRONTEND_URL` | Frontend URL | `https://yourdomain.com` |
|
||||
| `EMAIL_FROM` | Sender email | `noreply@yourdomain.com` |
|
||||
|
||||
#### Email Configuration
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `SMTP_HOST` | SMTP server | `smtp.gmail.com` |
|
||||
| `SMTP_PORT` | SMTP port | `587` |
|
||||
| `SMTP_SECURE` | Use TLS | `true` |
|
||||
| `SMTP_USER` | SMTP username | `your-email@gmail.com` |
|
||||
| `SMTP_PASS` | SMTP password | App-specific password |
|
||||
|
||||
### Storage Paths
|
||||
|
||||
- Photos: `./storage/events/active/`
|
||||
- Archives: `./storage/events/archived/`
|
||||
- Thumbnails: `./storage/thumbnails/`
|
||||
- Uploads: `./storage/uploads/`
|
||||
|
||||
## Backup and Restore
|
||||
|
||||
### Backup Database
|
||||
|
||||
```bash
|
||||
# Extract backup
|
||||
tar -xzf backup-20240615-020000.tar.gz
|
||||
# PostgreSQL backup
|
||||
docker-compose -f docker-compose.prod.yml exec db \
|
||||
pg_dump -U picpeak picpeak > backup-$(date +%Y%m%d).sql
|
||||
|
||||
# Restore database
|
||||
docker exec -i $(docker ps -q -f name=photo-sharing_db) \
|
||||
psql -U postgres photo_sharing < backup-20240615-020000/database.sql
|
||||
|
||||
# Restore photos
|
||||
tar -xzf backup-20240615-020000/photos.tar.gz -C /opt/photo-sharing/
|
||||
|
||||
# Restore volumes
|
||||
docker run --rm \
|
||||
-v photo-sharing_app-data:/data \
|
||||
-v $(pwd)/backup-20240615-020000:/backup \
|
||||
alpine tar -xzf /backup/volume-photo-sharing_app-data.tar.gz -C /data
|
||||
# Backup storage
|
||||
tar -czf storage-backup-$(date +%Y%m%d).tar.gz ./storage
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### 1. Scaling Services
|
||||
### Restore Database
|
||||
|
||||
```bash
|
||||
# Scale backend to 5 replicas
|
||||
docker service scale photo-sharing_backend=5
|
||||
# PostgreSQL restore
|
||||
docker-compose -f docker-compose.prod.yml exec -T db \
|
||||
psql -U picpeak picpeak < backup-20240115.sql
|
||||
|
||||
# Scale frontend to 3 replicas
|
||||
docker service scale photo-sharing_frontend=3
|
||||
# Restore storage
|
||||
tar -xzf storage-backup-20240115.tar.gz
|
||||
```
|
||||
|
||||
### 2. Rolling Updates
|
||||
|
||||
```bash
|
||||
# Update backend image
|
||||
docker service update \
|
||||
--image registry.yourdomain.com/photo-sharing-backend:v2.0 \
|
||||
photo-sharing_backend
|
||||
```
|
||||
|
||||
### 3. Drain Node for Maintenance
|
||||
|
||||
```bash
|
||||
# Drain node
|
||||
docker node update --availability drain worker-1
|
||||
|
||||
# Perform maintenance...
|
||||
|
||||
# Activate node
|
||||
docker node update --availability active worker-1
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Service Won't Start
|
||||
```bash
|
||||
# Check service status
|
||||
docker service ps photo-sharing_backend --no-trunc
|
||||
|
||||
# View detailed logs
|
||||
docker service logs photo-sharing_backend --details
|
||||
```
|
||||
|
||||
#### 2. Database Connection Issues
|
||||
```bash
|
||||
# Check database logs
|
||||
docker service logs photo-sharing_db
|
||||
|
||||
# Test connection
|
||||
docker exec $(docker ps -q -f name=photo-sharing_db) \
|
||||
pg_isready -U postgres
|
||||
```
|
||||
|
||||
#### 3. Traefik Certificate Issues
|
||||
```bash
|
||||
# Check Traefik logs
|
||||
docker service logs traefik_traefik | grep acme
|
||||
|
||||
# Remove and regenerate certificates
|
||||
rm -rf /opt/traefik/letsencrypt/acme.json
|
||||
docker service update --force traefik_traefik
|
||||
```
|
||||
|
||||
#### 4. Storage Issues
|
||||
```bash
|
||||
# Check disk usage
|
||||
df -h
|
||||
|
||||
# Clean up Docker
|
||||
docker system prune -a
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging:
|
||||
|
||||
```bash
|
||||
# Update service with debug logging
|
||||
docker service update \
|
||||
--env-add LOG_LEVEL=debug \
|
||||
photo-sharing_backend
|
||||
```
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Check all endpoints
|
||||
curl -f https://photos.yourdomain.com/health
|
||||
curl -f https://api.photos.yourdomain.com/api/health
|
||||
curl -f https://traefik.yourdomain.com/ping
|
||||
# Backend health
|
||||
curl https://yourdomain.com/api/health
|
||||
|
||||
# Frontend health
|
||||
curl https://yourdomain.com/health
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
### Logs
|
||||
|
||||
1. **Regular Updates**
|
||||
- Keep Docker and system packages updated
|
||||
- Update application dependencies regularly
|
||||
- Monitor security advisories
|
||||
```bash
|
||||
# All services
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
|
||||
2. **Access Control**
|
||||
- Use strong passwords for all services
|
||||
- Enable 2FA where possible
|
||||
- Restrict SSH access to specific IPs
|
||||
- Use Docker secrets for sensitive data
|
||||
# Specific service
|
||||
docker-compose -f docker-compose.prod.yml logs -f backend
|
||||
|
||||
3. **Network Security**
|
||||
- Use internal networks for service communication
|
||||
- Enable firewall rules
|
||||
- Use TLS for all external communication
|
||||
- Regular security scans with Trivy
|
||||
# Last 100 lines
|
||||
docker-compose -f docker-compose.prod.yml logs --tail=100 backend
|
||||
```
|
||||
|
||||
4. **Backup Security**
|
||||
- Encrypt backups at rest
|
||||
- Test restore procedures regularly
|
||||
- Store backups in multiple locations
|
||||
- Rotate old backups
|
||||
## Troubleshooting
|
||||
|
||||
## Performance Tuning
|
||||
### Backend Won't Start
|
||||
|
||||
1. **Database Optimization**
|
||||
```sql
|
||||
-- Add indexes for common queries
|
||||
CREATE INDEX idx_photos_event_id ON photos(event_id);
|
||||
CREATE INDEX idx_access_logs_event_id ON access_logs(event_id);
|
||||
1. Check database connection:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml logs db
|
||||
```
|
||||
|
||||
2. **Image Optimization**
|
||||
- Use CDN for static assets
|
||||
- Enable aggressive caching
|
||||
- Optimize image sizes before upload
|
||||
|
||||
3. **Service Limits**
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 1G
|
||||
reservations:
|
||||
cpus: '0.5'
|
||||
memory: 256M
|
||||
2. Verify environment variables:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend env | grep DB_
|
||||
```
|
||||
|
||||
## Support
|
||||
### Can't Login as Admin
|
||||
|
||||
For issues and questions:
|
||||
- Check logs: `docker service logs <service_name>`
|
||||
- Review documentation: [README.md](README.md)
|
||||
- Check monitoring dashboards
|
||||
- Contact: admin@yourdomain.com
|
||||
1. Verify admin user exists:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||
-c "SELECT * FROM admin_users;"
|
||||
```
|
||||
|
||||
2. Reset admin password:
|
||||
```bash
|
||||
# Create new admin with different email
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
node scripts/create-admin.js --email newadmin@yourdomain.com
|
||||
```
|
||||
|
||||
### Photos Not Loading
|
||||
|
||||
1. Check file permissions:
|
||||
```bash
|
||||
ls -la ./storage/events/active/
|
||||
```
|
||||
|
||||
2. Verify nginx proxy configuration:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec frontend \
|
||||
cat /etc/nginx/conf.d/default.conf
|
||||
```
|
||||
|
||||
### Email Not Sending
|
||||
|
||||
1. Check email configuration:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend env | grep SMTP_
|
||||
```
|
||||
|
||||
2. View email queue:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||
-c "SELECT * FROM email_queue WHERE status = 'failed';"
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Update Application
|
||||
|
||||
```bash
|
||||
# Pull latest changes
|
||||
git pull
|
||||
|
||||
# Rebuild images
|
||||
docker-compose -f docker-compose.prod.yml build
|
||||
|
||||
# Restart services
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Clean Up
|
||||
|
||||
```bash
|
||||
# Remove unused images
|
||||
docker image prune -a
|
||||
|
||||
# Clean up logs
|
||||
docker-compose -f docker-compose.prod.yml logs --tail=0 -f
|
||||
|
||||
# Remove old archives
|
||||
find ./storage/events/archived -name "*.zip" -mtime +90 -delete
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Generated secure `JWT_SECRET`
|
||||
- [ ] Generated secure `DB_PASSWORD`
|
||||
- [ ] HTTPS enabled via reverse proxy
|
||||
- [ ] Changed default admin credentials
|
||||
- [ ] Configured real SMTP server
|
||||
- [ ] Set file permissions: `chmod 600 .env`
|
||||
- [ ] Firewall configured
|
||||
- [ ] Regular backups scheduled
|
||||
- [ ] Monitoring enabled
|
||||
@@ -0,0 +1,98 @@
|
||||
# Production Deployment Guide
|
||||
|
||||
This guide explains how to deploy PicPeak in production behind a reverse proxy like Traefik.
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### Frontend Configuration
|
||||
|
||||
For production deployment behind a reverse proxy (Traefik, Nginx, etc.), the frontend should use relative URLs to automatically inherit the protocol (HTTPS) and domain.
|
||||
|
||||
1. Copy the production environment template:
|
||||
```bash
|
||||
cp frontend/.env.production.example frontend/.env.production
|
||||
```
|
||||
|
||||
2. Set the API URL to use relative path:
|
||||
```env
|
||||
# frontend/.env.production
|
||||
VITE_API_URL=/api
|
||||
```
|
||||
|
||||
This ensures all API calls will use the same domain and protocol as the frontend.
|
||||
|
||||
### Backend Configuration
|
||||
|
||||
Ensure your backend `.env` file has the correct URLs:
|
||||
```env
|
||||
# backend/.env
|
||||
FRONTEND_URL=https://yourdomain.com
|
||||
ADMIN_URL=https://yourdomain.com
|
||||
```
|
||||
|
||||
## Docker Compose Production
|
||||
|
||||
When using Docker Compose in production:
|
||||
|
||||
1. Build with production environment:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml build --build-arg NODE_ENV=production
|
||||
```
|
||||
|
||||
2. The frontend nginx configuration already includes proper proxy settings for:
|
||||
- `/api` → Backend API
|
||||
- `/photos` → Protected photo access
|
||||
- `/thumbnails` → Thumbnail images
|
||||
- `/uploads` → Public uploads (logos, favicons)
|
||||
|
||||
## Traefik Configuration
|
||||
|
||||
Example Traefik labels for docker-compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
frontend:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.picpeak.rule=Host(`yourdomain.com`)"
|
||||
- "traefik.http.routers.picpeak.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak.loadbalancer.server.port=80"
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **No Hardcoded URLs**: The application uses environment variables with relative URL fallbacks, making it production-ready.
|
||||
|
||||
2. **HTTPS Only**: When `VITE_API_URL=/api`, all requests will use the same protocol as the page (HTTPS in production).
|
||||
|
||||
3. **CORS Configuration**: The backend CORS is configured to accept requests from the URLs specified in `FRONTEND_URL` and `ADMIN_URL`.
|
||||
|
||||
4. **Static Assets**: All static assets (photos, thumbnails, uploads) are served through the nginx proxy, inheriting authentication headers.
|
||||
|
||||
## Verification
|
||||
|
||||
After deployment, verify:
|
||||
|
||||
1. Check browser console for any localhost URLs (there should be none)
|
||||
2. Verify all API calls use HTTPS
|
||||
3. Check that images load correctly with authentication
|
||||
4. Test favicon and logo display
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you see console errors about localhost:
|
||||
|
||||
1. Ensure `VITE_API_URL=/api` in frontend environment
|
||||
2. Clear browser cache
|
||||
3. Rebuild frontend with production environment:
|
||||
```bash
|
||||
cd frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
If images don't load:
|
||||
|
||||
1. Check that nginx proxy locations are configured
|
||||
2. Verify authentication tokens are being sent
|
||||
3. Check backend logs for authentication errors
|
||||
@@ -0,0 +1,100 @@
|
||||
# Production Deployment Fixes
|
||||
|
||||
This document describes the fixes applied to resolve production deployment issues in Docker.
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
### 1. Database Connection Error: "getaddrinfo ENOTFOUND postgres"
|
||||
**Problem**: The backend was trying to connect to hostname "postgres" but the database service is named "db" in docker-compose.
|
||||
**Solution**:
|
||||
- Updated `knexfile.js` to use correct default host "db" instead of "postgres"
|
||||
- Added `depends_on: db` to backend service in docker-compose.prod.yml
|
||||
|
||||
### 2. Backend Starting Before Database Ready
|
||||
**Problem**: Backend service started before PostgreSQL was ready, causing connection failures.
|
||||
**Solution**:
|
||||
- Created `wait-for-db.sh` script that waits for PostgreSQL to be ready
|
||||
- Updated Dockerfile to install postgresql-client and use the wait script
|
||||
- Script also runs migrations automatically on startup
|
||||
|
||||
### 3. Email Processor Initialization Failure
|
||||
**Problem**: Email processor tried to initialize on module load before database was available.
|
||||
**Solution**:
|
||||
- Modified `emailProcessor.js` to export initialization functions
|
||||
- Updated `server.js` to call initialization after database is ready
|
||||
- Added proper error handling for email service initialization
|
||||
|
||||
### 4. Missing Environment Variables
|
||||
**Problem**: Critical storage path environment variables were missing.
|
||||
**Solution**:
|
||||
- Added STORAGE_PATH, EVENTS_PATH, and ARCHIVE_PATH to docker-compose.prod.yml
|
||||
- Created `.env.example` documenting all required environment variables
|
||||
|
||||
### 5. Enhanced Health Check
|
||||
**Problem**: Basic health check didn't verify database connectivity.
|
||||
**Solution**:
|
||||
- Updated `/api/health` endpoint to check database connection
|
||||
- Returns proper HTTP 503 status when unhealthy
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **backend/knexfile.js** - Fixed production database defaults
|
||||
2. **backend/wait-for-db.sh** - Created database wait script
|
||||
3. **backend/Dockerfile** - Added postgresql-client and wait script
|
||||
4. **docker-compose.prod.yml** - Added dependencies and environment variables
|
||||
5. **backend/src/services/emailProcessor.js** - Disabled auto-initialization
|
||||
6. **backend/server.js** - Added email initialization and improved health check
|
||||
7. **backend/.env.example** - Created environment variable documentation
|
||||
|
||||
## Deployment Steps
|
||||
|
||||
1. Ensure all environment variables are set according to `.env.example`
|
||||
2. Build and deploy with docker-compose:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml build
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
3. The backend will now:
|
||||
- Wait for PostgreSQL to be ready
|
||||
- Run migrations automatically
|
||||
- Initialize all services in proper order
|
||||
- Provide health status at `/api/health`
|
||||
|
||||
## Verification
|
||||
|
||||
Check deployment health:
|
||||
```bash
|
||||
curl http://localhost/api/health
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"database": "connected",
|
||||
"timestamp": "2025-07-13T20:30:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Email Configuration
|
||||
|
||||
Email service requires configuration in the database. If email is not configured:
|
||||
- The service will log a warning but continue running
|
||||
- Emails will be queued but not sent
|
||||
- Configure email settings in the admin panel after deployment
|
||||
|
||||
## PostgreSQL Connection Fix
|
||||
|
||||
### Issue: "no pg_hba.conf entry for host"
|
||||
This error occurs when PostgreSQL requires SSL but the client connects without encryption.
|
||||
|
||||
### Solution:
|
||||
- Disabled SSL requirement for PostgreSQL in Docker environment (`ssl=off`)
|
||||
- Added proper authentication method (`scram-sha-256`)
|
||||
- This is acceptable for internal Docker networks where all traffic is isolated
|
||||
|
||||
### Security Note:
|
||||
For production deployments exposed to the internet:
|
||||
1. Use SSL certificates for PostgreSQL
|
||||
2. Or ensure the database is only accessible within the Docker network
|
||||
3. Never expose PostgreSQL port (5432) directly to the internet
|
||||
+32
-24
@@ -1,33 +1,41 @@
|
||||
NODE_ENV=development
|
||||
# Backend Environment Variables Example
|
||||
# Copy this file to .env and update with your values
|
||||
|
||||
# Application
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
# URLs
|
||||
ADMIN_URL=http://localhost:3000
|
||||
FRONTEND_URL=http://localhost:3001
|
||||
|
||||
# Security
|
||||
JWT_SECRET=dev-secret-key
|
||||
JWT_SECRET=your-very-secure-jwt-secret-at-least-32-characters-long
|
||||
|
||||
# URLs
|
||||
ADMIN_URL=https://yourdomain.com
|
||||
FRONTEND_URL=https://yourdomain.com
|
||||
|
||||
# Database Configuration
|
||||
DATABASE_CLIENT=pg
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
DB_USER=picpeak
|
||||
DB_PASSWORD=your-secure-database-password
|
||||
DB_NAME=picpeak
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=mailhog
|
||||
SMTP_PORT=1025
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
EMAIL_FROM=noreply@localhost
|
||||
SMTP_USER=your-smtp-username
|
||||
SMTP_PASS=your-smtp-password
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
|
||||
# Storage Paths (relative to project root)
|
||||
STORAGE_PATH=./storage
|
||||
EVENTS_PATH=./storage/events
|
||||
ARCHIVE_PATH=./storage/events/archived
|
||||
# Storage Paths (Docker)
|
||||
STORAGE_PATH=/app/storage
|
||||
EVENTS_PATH=/app/storage/events
|
||||
ARCHIVE_PATH=/app/storage/events/archived
|
||||
|
||||
# Analytics (Optional)
|
||||
UMAMI_URL=https://analytics.yourdomain.com
|
||||
UMAMI_WEBSITE_ID=your-website-id
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Database (for production, consider PostgreSQL)
|
||||
DATABASE_CLIENT=sqlite3
|
||||
DATABASE_PATH=./data/photo_sharing.db
|
||||
|
||||
# Umami Analytics (optional)
|
||||
UMAMI_URL=
|
||||
UMAMI_WEBSITE_ID=
|
||||
LOG_LEVEL=info
|
||||
@@ -1,137 +0,0 @@
|
||||
# Quick Guide: Activate Authentication V2 Fixes
|
||||
|
||||
## Step 1: Install Dependency
|
||||
```bash
|
||||
cd backend
|
||||
npm install zxcvbn@4.4.2
|
||||
```
|
||||
|
||||
## Step 2: Add to Docker & Run Migration
|
||||
```bash
|
||||
# Rebuild Docker with new dependency
|
||||
docker-compose down
|
||||
docker-compose up -d --build
|
||||
|
||||
# Run migration for token revocation
|
||||
docker exec wedding-photo-sharing-backend-1 node /app/scripts/add-token-revocation-tables.js
|
||||
```
|
||||
|
||||
## Step 3: Update server.js
|
||||
|
||||
### 3.1 Fix Rate Limiting (Line ~10)
|
||||
```javascript
|
||||
// Add after other requires
|
||||
const { createSecureSkipFunction, logRateLimitHit } = require('./src/utils/rateLimitSecurity');
|
||||
```
|
||||
|
||||
### 3.2 Update Rate Limiter (Line ~59)
|
||||
```javascript
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: process.env.NODE_ENV === 'development' ? 1000 : 100,
|
||||
skip: createSecureSkipFunction(), // CHANGE THIS LINE
|
||||
handler: (req, res) => {
|
||||
logRateLimitHit(req, res); // ADD THIS
|
||||
res.status(429).json({
|
||||
error: 'Too many requests from this IP, please try again later.'
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3.3 Update Auth Limiter (Line ~81)
|
||||
```javascript
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5,
|
||||
skipSuccessfulRequests: true, // ADD THIS
|
||||
handler: (req, res) => {
|
||||
logRateLimitHit(req, res); // ADD THIS
|
||||
res.status(429).json({
|
||||
error: 'Too many login attempts, please try again later.',
|
||||
retryAfter: res.getHeader('Retry-After')
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3.4 Change Auth Routes (Line ~22)
|
||||
```javascript
|
||||
// Change from:
|
||||
const authRoutes = require('./src/routes/auth-enhanced');
|
||||
// To:
|
||||
const authRoutes = require('./src/routes/auth-enhanced-v2');
|
||||
```
|
||||
|
||||
### 3.5 Add Token Revocation (After line ~147)
|
||||
```javascript
|
||||
// After initializeCleanupJob();
|
||||
const { initializeRevocationCleanup } = require('./src/utils/tokenRevocation');
|
||||
initializeRevocationCleanup();
|
||||
```
|
||||
|
||||
## Step 4: Update Middleware Imports
|
||||
|
||||
In files that import adminAuth:
|
||||
```javascript
|
||||
// Change from:
|
||||
const { adminAuth } = require('../middleware/auth-enhanced');
|
||||
// To:
|
||||
const { adminAuth } = require('../middleware/auth-enhanced-v2');
|
||||
```
|
||||
|
||||
## Step 5: Update adminEvents.js
|
||||
|
||||
Add password validation to event creation:
|
||||
```javascript
|
||||
// At top of file
|
||||
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
|
||||
|
||||
// In POST route, after extracting password, add:
|
||||
const passwordValidation = validatePasswordInContext(password, 'gallery', {
|
||||
eventName: event_name
|
||||
});
|
||||
|
||||
if (!passwordValidation.valid) {
|
||||
return res.status(400).json({
|
||||
error: 'Password does not meet security requirements',
|
||||
details: passwordValidation.errors,
|
||||
score: passwordValidation.score,
|
||||
feedback: passwordValidation.feedback
|
||||
});
|
||||
}
|
||||
|
||||
// Change password hashing to:
|
||||
const password_hash = await bcrypt.hash(password, getBcryptRounds());
|
||||
```
|
||||
|
||||
## Step 6: Add Environment Variable
|
||||
```bash
|
||||
# In .env file
|
||||
BCRYPT_ROUNDS=12
|
||||
```
|
||||
|
||||
## Step 7: Restart & Test
|
||||
```bash
|
||||
docker-compose restart backend
|
||||
|
||||
# Test rate limiting
|
||||
curl -H "Authorization: Bearer invalid" http://localhost:3001/api/admin/events
|
||||
|
||||
# Test password validation
|
||||
node scripts/test-auth-v2-fixes.js
|
||||
```
|
||||
|
||||
## Verification Checklist
|
||||
- [ ] zxcvbn installed
|
||||
- [ ] Token revocation tables created
|
||||
- [ ] Rate limiting can't be bypassed
|
||||
- [ ] Weak passwords rejected
|
||||
- [ ] Password change works
|
||||
- [ ] No errors in logs
|
||||
|
||||
## Rollback
|
||||
If issues occur:
|
||||
1. Revert server.js changes
|
||||
2. Restart backend
|
||||
3. All new features are additive, so existing functionality remains
|
||||
@@ -1,64 +0,0 @@
|
||||
# Authentication & Authorization Flaws Analysis
|
||||
|
||||
## Already Fixed ✅
|
||||
|
||||
1. **Missing Token Type Validation** ✅
|
||||
- Fixed in `auth-enhanced.js` line 31
|
||||
- Checks `decoded.type !== 'admin'`
|
||||
- Prevents gallery tokens from accessing admin endpoints
|
||||
|
||||
2. **No Audit Logging** ✅
|
||||
- Added `login_attempts` table
|
||||
- Tracks all login attempts with IP, user agent, timestamp
|
||||
- Automatic cleanup of old records
|
||||
|
||||
3. **Account Lockout Protection** ✅
|
||||
- Lockout after 5 failed attempts
|
||||
- 30-minute lockout duration
|
||||
- Prevents brute force attacks
|
||||
|
||||
4. **Basic Session Management** ✅
|
||||
- Added session timeout middleware
|
||||
- Tracks active sessions
|
||||
- Can invalidate sessions
|
||||
|
||||
## Still Needs Fixing ❌
|
||||
|
||||
### 1. Weak Password Requirements 🔴
|
||||
- **Current**: No minimum length validation
|
||||
- **Required**: Minimum 12 characters + complexity
|
||||
- **Risk**: Vulnerable to brute force
|
||||
|
||||
### 2. Rate Limiting Bypass 🔴
|
||||
- **Current**: Invalid JWT bypasses rate limiting
|
||||
- **Location**: `server.js:64-71`
|
||||
- **Risk**: Attackers can spam with invalid tokens
|
||||
|
||||
### 3. No Password Complexity 🟡
|
||||
- **Current**: Any 6+ character password accepted
|
||||
- **Required**: Upper, lower, number, special char
|
||||
- **Risk**: Weak passwords
|
||||
|
||||
### 4. No Token Revocation 🟡
|
||||
- **Current**: Tokens valid until expiration
|
||||
- **Required**: Blacklist/revocation mechanism
|
||||
- **Risk**: Can't invalidate compromised tokens
|
||||
|
||||
### 5. Fixed Bcrypt Rounds 🟡
|
||||
- **Current**: Hardcoded to 10 rounds
|
||||
- **Required**: Configurable (12-14 recommended)
|
||||
- **Risk**: May become insufficient over time
|
||||
|
||||
### 6. In-Memory Session Storage 🟡
|
||||
- **Current**: Sessions stored in memory
|
||||
- **Required**: Redis or database storage
|
||||
- **Risk**: Lost on restart, not scalable
|
||||
|
||||
## Priority Fixes
|
||||
|
||||
1. **Rate Limiting Bypass** (Critical)
|
||||
2. **Password Requirements** (High)
|
||||
3. **Password Complexity** (High)
|
||||
4. **Token Revocation** (Medium)
|
||||
5. **Bcrypt Rounds** (Medium)
|
||||
6. **Session Storage** (Low - for scalability)
|
||||
@@ -1,216 +0,0 @@
|
||||
# Authentication Security Integration Guide
|
||||
|
||||
## How The Enhanced Security Works
|
||||
|
||||
### 1. Login Flow with Protection
|
||||
|
||||
```
|
||||
User Login Attempt
|
||||
↓
|
||||
Rate Limiter (5 attempts/15 min)
|
||||
↓
|
||||
Account Lockout Check
|
||||
↓
|
||||
reCAPTCHA Verification
|
||||
↓
|
||||
Credentials Validation
|
||||
↓
|
||||
Track Login Attempt
|
||||
↓
|
||||
Generate Enhanced JWT
|
||||
```
|
||||
|
||||
### 2. Token Structure
|
||||
|
||||
**Before** (Basic JWT):
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"type": "admin",
|
||||
"exp": 1234567890
|
||||
}
|
||||
```
|
||||
|
||||
**After** (Enhanced JWT):
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"type": "admin",
|
||||
"ip": "192.168.1.100",
|
||||
"loginTime": 1234567890,
|
||||
"exp": 1234567890,
|
||||
"iss": "picpeak-auth"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Security Layers
|
||||
|
||||
1. **Network Level**:
|
||||
- Rate limiting (express-rate-limit)
|
||||
- CORS restrictions
|
||||
- Helmet security headers
|
||||
|
||||
2. **Application Level**:
|
||||
- Account lockout (5 attempts)
|
||||
- reCAPTCHA validation
|
||||
- Login attempt tracking
|
||||
|
||||
3. **Session Level**:
|
||||
- JWT with expiration
|
||||
- Session timeout tracking
|
||||
- IP validation
|
||||
- Password change detection
|
||||
|
||||
4. **Database Level**:
|
||||
- Bcrypt password hashing
|
||||
- Audit trail (login_attempts)
|
||||
- Secure token storage
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Server.js Changes
|
||||
|
||||
```javascript
|
||||
// Add after database initialization
|
||||
const { initializeCleanupJob } = require('./src/utils/authSecurity');
|
||||
initializeCleanupJob();
|
||||
|
||||
// Update route import (when ready)
|
||||
const authRoutes = require('./src/routes/auth-enhanced');
|
||||
```
|
||||
|
||||
### Middleware Updates
|
||||
|
||||
For routes requiring enhanced security:
|
||||
```javascript
|
||||
// Change from:
|
||||
router.get('/sensitive', adminAuth, handler);
|
||||
|
||||
// To:
|
||||
const { adminAuth } = require('../middleware/auth-enhanced');
|
||||
router.get('/sensitive', adminAuth, handler);
|
||||
```
|
||||
|
||||
### Frontend Integration
|
||||
|
||||
1. **Handle New Error Codes**:
|
||||
```javascript
|
||||
// Lockout error
|
||||
if (error.response?.status === 423) {
|
||||
const retryAfter = error.response.data.retryAfter;
|
||||
showError(`Account locked. Try again in ${retryAfter} seconds`);
|
||||
}
|
||||
|
||||
// Session expired
|
||||
if (error.response?.data?.code === 'SESSION_TIMEOUT') {
|
||||
redirectToLogin();
|
||||
}
|
||||
```
|
||||
|
||||
2. **Implement Logout**:
|
||||
```javascript
|
||||
async function logout() {
|
||||
await api.post('/auth/logout');
|
||||
clearToken();
|
||||
redirectToLogin();
|
||||
}
|
||||
```
|
||||
|
||||
3. **Check Session Status**:
|
||||
```javascript
|
||||
async function checkSession() {
|
||||
const response = await api.get('/auth/session');
|
||||
if (!response.data.valid) {
|
||||
redirectToLogin();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
No new environment variables required. Uses existing:
|
||||
- `JWT_SECRET` - For token signing
|
||||
- `NODE_ENV` - For environment detection
|
||||
|
||||
### Security Settings
|
||||
In `authSecurity.js`:
|
||||
```javascript
|
||||
const MAX_LOGIN_ATTEMPTS = 5; // Attempts before lockout
|
||||
const LOCKOUT_DURATION = 30 * 60 * 1000; // 30 minutes
|
||||
const ATTEMPT_WINDOW = 15 * 60 * 1000; // 15 minute window
|
||||
```
|
||||
|
||||
## Monitoring & Maintenance
|
||||
|
||||
### Daily Monitoring
|
||||
```sql
|
||||
-- Check for brute force attempts
|
||||
SELECT identifier, COUNT(*) as attempts,
|
||||
MAX(attempt_time) as last_attempt
|
||||
FROM login_attempts
|
||||
WHERE success = 0
|
||||
AND attempt_time > datetime('now', '-24 hours')
|
||||
GROUP BY identifier
|
||||
HAVING COUNT(*) > 10
|
||||
ORDER BY attempts DESC;
|
||||
```
|
||||
|
||||
### Weekly Review
|
||||
```sql
|
||||
-- Suspicious activity patterns
|
||||
SELECT DATE(attempt_time) as date,
|
||||
COUNT(DISTINCT identifier) as unique_users,
|
||||
COUNT(DISTINCT ip_address) as unique_ips,
|
||||
COUNT(*) as total_attempts,
|
||||
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failed_attempts
|
||||
FROM login_attempts
|
||||
WHERE attempt_time > datetime('now', '-7 days')
|
||||
GROUP BY DATE(attempt_time)
|
||||
ORDER BY date DESC;
|
||||
```
|
||||
|
||||
### Automated Cleanup
|
||||
The system automatically cleans up login attempts older than 7 days to prevent database bloat.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### User Locked Out
|
||||
```sql
|
||||
-- Check lockout status
|
||||
SELECT * FROM login_attempts
|
||||
WHERE identifier = 'user@example.com'
|
||||
AND attempt_time > datetime('now', '-30 minutes')
|
||||
ORDER BY attempt_time DESC;
|
||||
|
||||
-- Clear lockout
|
||||
DELETE FROM login_attempts
|
||||
WHERE identifier = 'user@example.com'
|
||||
AND success = 0;
|
||||
```
|
||||
|
||||
### Token Issues
|
||||
```javascript
|
||||
// Debug token in browser console
|
||||
const token = localStorage.getItem('token');
|
||||
const decoded = JSON.parse(atob(token.split('.')[1]));
|
||||
console.log('Token expires:', new Date(decoded.exp * 1000));
|
||||
console.log('Token IP:', decoded.ip);
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Monitor Failed Attempts**: Set up alerts for excessive failures
|
||||
2. **Review IP Patterns**: Look for geographic anomalies
|
||||
3. **Rotate JWT Secret**: Periodically update in production
|
||||
4. **Update Dependencies**: Keep auth libraries current
|
||||
5. **Test Lockouts**: Regularly verify protection works
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Two-Factor Authentication**: Database columns already added
|
||||
2. **IP Whitelist**: For admin accounts
|
||||
3. **Device Fingerprinting**: Enhanced session security
|
||||
4. **OAuth Integration**: Social login options
|
||||
5. **WebAuthn/Passkeys**: Passwordless authentication
|
||||
@@ -1,221 +0,0 @@
|
||||
# Authentication Security Enhancement Migration Guide
|
||||
|
||||
## Overview
|
||||
This guide provides a safe migration path to enhance authentication security without disrupting the production system.
|
||||
|
||||
## Security Enhancements Implemented
|
||||
|
||||
### 1. Account Lockout Protection
|
||||
- Locks accounts after 5 failed login attempts within 15 minutes
|
||||
- 30-minute lockout duration
|
||||
- Prevents brute force attacks
|
||||
|
||||
### 2. Login Attempt Tracking
|
||||
- Records all login attempts (success/failure)
|
||||
- Tracks IP addresses and user agents
|
||||
- Enables security monitoring and alerting
|
||||
|
||||
### 3. Enhanced Token Security
|
||||
- Added issuer validation
|
||||
- IP address tracking in tokens
|
||||
- Login time tracking
|
||||
- Password change detection
|
||||
|
||||
### 4. Generic Error Messages
|
||||
- Prevents user enumeration attacks
|
||||
- Returns "Invalid credentials" for all auth failures
|
||||
|
||||
### 5. Logout Endpoint
|
||||
- Properly invalidates sessions
|
||||
- Clears server-side session tracking
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Step 1: Database Migrations (Low Risk)
|
||||
|
||||
First, run the new migrations to add required tables/columns:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# Run new migrations
|
||||
npx knex migrate:latest
|
||||
|
||||
# Verify migrations
|
||||
npx knex migrate:status
|
||||
```
|
||||
|
||||
This adds:
|
||||
- `login_attempts` table
|
||||
- `password_changed_at` column to `admin_users`
|
||||
- `last_login_ip` column to `admin_users`
|
||||
|
||||
### Step 2: Deploy Enhanced Auth Utilities (Low Risk)
|
||||
|
||||
The new files don't affect existing functionality:
|
||||
- `src/utils/authSecurity.js` - New security utilities
|
||||
- `src/middleware/auth-enhanced.js` - Enhanced auth middleware
|
||||
- `src/routes/auth-enhanced.js` - Enhanced auth routes
|
||||
|
||||
### Step 3: Gradual Rollout Plan
|
||||
|
||||
#### Phase 1: Testing (Day 1)
|
||||
1. Deploy code but keep using existing auth routes
|
||||
2. Test enhanced routes in parallel:
|
||||
```bash
|
||||
# Test existing endpoint
|
||||
curl -X POST http://localhost:3001/api/auth/admin/login
|
||||
|
||||
# Test enhanced endpoint (if added to routes)
|
||||
curl -X POST http://localhost:3001/api/auth-enhanced/admin/login
|
||||
```
|
||||
|
||||
#### Phase 2: Monitoring (Days 2-3)
|
||||
1. Add the auth security initialization to server.js:
|
||||
```javascript
|
||||
// In server.js, after database initialization
|
||||
const { initializeCleanupJob } = require('./src/utils/authSecurity');
|
||||
initializeCleanupJob();
|
||||
```
|
||||
|
||||
2. Monitor logs for any issues
|
||||
3. Check login_attempts table is populating
|
||||
|
||||
#### Phase 3: Switch Routes (Day 4)
|
||||
1. Update route imports in server.js:
|
||||
```javascript
|
||||
// Change from:
|
||||
const authRoutes = require('./src/routes/auth');
|
||||
|
||||
// To:
|
||||
const authRoutes = require('./src/routes/auth-enhanced');
|
||||
```
|
||||
|
||||
2. Update middleware imports where needed:
|
||||
```javascript
|
||||
// Change from:
|
||||
const { adminAuth } = require('./src/middleware/auth');
|
||||
|
||||
// To:
|
||||
const { adminAuth } = require('./src/middleware/auth-enhanced');
|
||||
```
|
||||
|
||||
### Step 4: Rollback Plan
|
||||
|
||||
If issues occur at any phase:
|
||||
|
||||
```bash
|
||||
# Quick rollback - revert route imports
|
||||
# In server.js, change back to:
|
||||
const authRoutes = require('./src/routes/auth');
|
||||
const { adminAuth } = require('./src/middleware/auth');
|
||||
|
||||
# Restart application
|
||||
docker-compose restart backend
|
||||
# or
|
||||
pm2 restart picpeak-backend
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Before Production Deployment:
|
||||
|
||||
1. **Test Normal Login Flow**:
|
||||
```bash
|
||||
# Should work normally
|
||||
curl -X POST http://localhost:3001/api/auth/admin/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"correct-password"}'
|
||||
```
|
||||
|
||||
2. **Test Account Lockout**:
|
||||
```bash
|
||||
# Make 5 failed attempts
|
||||
for i in {1..5}; do
|
||||
curl -X POST http://localhost:3001/api/auth/admin/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"wrong-password"}'
|
||||
done
|
||||
|
||||
# 6th attempt should return lockout error
|
||||
```
|
||||
|
||||
3. **Test Logout**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/api/auth/logout \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
4. **Test Session Info**:
|
||||
```bash
|
||||
curl http://localhost:3001/api/auth/session \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Adjusting Security Settings
|
||||
|
||||
In `src/utils/authSecurity.js`, you can adjust:
|
||||
```javascript
|
||||
const MAX_LOGIN_ATTEMPTS = 5; // Number of attempts before lockout
|
||||
const LOCKOUT_DURATION = 30 * 60 * 1000; // Lockout time in ms
|
||||
const ATTEMPT_WINDOW = 15 * 60 * 1000; // Time window for counting attempts
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Check Login Attempts:
|
||||
```sql
|
||||
-- Recent failed attempts
|
||||
SELECT * FROM login_attempts
|
||||
WHERE success = false
|
||||
ORDER BY attempt_time DESC
|
||||
LIMIT 20;
|
||||
|
||||
-- Accounts with multiple failures
|
||||
SELECT identifier, COUNT(*) as failed_attempts
|
||||
FROM login_attempts
|
||||
WHERE success = false
|
||||
AND attempt_time > datetime('now', '-1 hour')
|
||||
GROUP BY identifier
|
||||
HAVING COUNT(*) > 3;
|
||||
```
|
||||
|
||||
### Monitor Locked Accounts:
|
||||
```sql
|
||||
-- Check currently locked accounts
|
||||
SELECT identifier, COUNT(*) as attempts,
|
||||
MAX(attempt_time) as last_attempt
|
||||
FROM login_attempts
|
||||
WHERE success = false
|
||||
AND attempt_time > datetime('now', '-15 minutes')
|
||||
GROUP BY identifier
|
||||
HAVING COUNT(*) >= 5;
|
||||
```
|
||||
|
||||
## Security Benefits
|
||||
|
||||
1. **Prevents Brute Force**: Account lockout after failed attempts
|
||||
2. **Audit Trail**: Complete login history for security analysis
|
||||
3. **Session Security**: Tokens invalidated on password change
|
||||
4. **IP Monitoring**: Detect suspicious login patterns
|
||||
5. **User Privacy**: Generic errors prevent user enumeration
|
||||
|
||||
## Notes
|
||||
|
||||
- Old tokens remain valid until expiration
|
||||
- No immediate user impact
|
||||
- Gradual rollout minimizes risk
|
||||
- Full rollback possible at any stage
|
||||
|
||||
## Support
|
||||
|
||||
Monitor logs after deployment:
|
||||
```bash
|
||||
# Docker
|
||||
docker-compose logs -f backend | grep -E "(auth|login|security)"
|
||||
|
||||
# PM2
|
||||
pm2 logs picpeak-backend | grep -E "(auth|login|security)"
|
||||
```
|
||||
@@ -1,187 +0,0 @@
|
||||
# Authentication Security Enhancement Rollback Plan
|
||||
|
||||
## Quick Rollback Steps
|
||||
|
||||
### Immediate Rollback (< 2 minutes)
|
||||
|
||||
If auth issues occur after deployment, follow these steps:
|
||||
|
||||
```bash
|
||||
# 1. SSH into production server
|
||||
ssh your-server
|
||||
|
||||
# 2. Navigate to backend directory
|
||||
cd /path/to/picpeak/backend
|
||||
|
||||
# 3. Revert route changes in server.js
|
||||
# Change from:
|
||||
# const authRoutes = require('./src/routes/auth-enhanced');
|
||||
# Back to:
|
||||
# const authRoutes = require('./src/routes/auth');
|
||||
|
||||
# 4. Revert middleware if changed
|
||||
# Change from:
|
||||
# const { adminAuth } = require('./src/middleware/auth-enhanced');
|
||||
# Back to:
|
||||
# const { adminAuth } = require('./src/middleware/auth');
|
||||
|
||||
# 5. Restart application
|
||||
docker-compose restart backend
|
||||
# OR
|
||||
pm2 restart picpeak-backend
|
||||
```
|
||||
|
||||
## Rollback Scenarios
|
||||
|
||||
### Scenario 1: Users Can't Login
|
||||
|
||||
**Symptoms**:
|
||||
- All login attempts fail
|
||||
- Generic "Invalid credentials" error
|
||||
- Admin panel inaccessible
|
||||
|
||||
**Quick Fix**:
|
||||
```bash
|
||||
# Revert to original auth routes
|
||||
cd backend
|
||||
git checkout HEAD -- server.js
|
||||
docker-compose restart backend
|
||||
```
|
||||
|
||||
### Scenario 2: Account Lockout Issues
|
||||
|
||||
**Symptoms**:
|
||||
- Legitimate users locked out
|
||||
- "Account temporarily locked" errors
|
||||
|
||||
**Quick Fix**:
|
||||
```sql
|
||||
-- Clear all lockouts
|
||||
DELETE FROM login_attempts WHERE success = false;
|
||||
|
||||
-- Or clear specific user
|
||||
DELETE FROM login_attempts
|
||||
WHERE identifier = 'username_or_email'
|
||||
AND success = false;
|
||||
```
|
||||
|
||||
### Scenario 3: Token Validation Errors
|
||||
|
||||
**Symptoms**:
|
||||
- "Invalid token" errors
|
||||
- Existing sessions broken
|
||||
- API calls failing
|
||||
|
||||
**Quick Fix**:
|
||||
```javascript
|
||||
// In auth middleware, temporarily disable strict validation
|
||||
// Comment out issuer validation:
|
||||
// issuer: 'picpeak-auth'
|
||||
|
||||
// Just use basic verification:
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
```
|
||||
|
||||
### Scenario 4: Database Migration Issues
|
||||
|
||||
**Symptoms**:
|
||||
- Application won't start
|
||||
- Database errors in logs
|
||||
|
||||
**Rollback Migration**:
|
||||
```bash
|
||||
# Rollback last 2 migrations
|
||||
npx knex migrate:rollback --all
|
||||
npx knex migrate:up 014_add_default_welcome_message.js
|
||||
|
||||
# Or manually fix:
|
||||
sqlite3 database.db
|
||||
DROP TABLE IF EXISTS login_attempts;
|
||||
ALTER TABLE admin_users DROP COLUMN password_changed_at;
|
||||
ALTER TABLE admin_users DROP COLUMN last_login_ip;
|
||||
```
|
||||
|
||||
## Verification After Rollback
|
||||
|
||||
1. **Test Admin Login**:
|
||||
```bash
|
||||
curl -X POST http://your-domain/api/auth/admin/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"your-password"}'
|
||||
```
|
||||
|
||||
2. **Test Gallery Access**:
|
||||
```bash
|
||||
curl -X POST http://your-domain/api/auth/gallery/verify \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"slug":"test-gallery","password":"gallery-password"}'
|
||||
```
|
||||
|
||||
3. **Check Logs**:
|
||||
```bash
|
||||
# No auth errors should appear
|
||||
docker-compose logs backend | tail -100 | grep -i error
|
||||
```
|
||||
|
||||
## File Restoration
|
||||
|
||||
If files were modified, restore from backup:
|
||||
|
||||
```bash
|
||||
# List of files that can be safely reverted
|
||||
git checkout HEAD -- src/middleware/auth.js
|
||||
git checkout HEAD -- src/routes/auth.js
|
||||
git checkout HEAD -- server.js
|
||||
|
||||
# Remove new files (safe to delete)
|
||||
rm -f src/utils/authSecurity.js
|
||||
rm -f src/middleware/auth-enhanced.js
|
||||
rm -f src/routes/auth-enhanced.js
|
||||
rm -f migrations/015_add_login_attempts_table.js
|
||||
rm -f migrations/016_add_auth_security_columns.js
|
||||
```
|
||||
|
||||
## Emergency SQL Fixes
|
||||
|
||||
```sql
|
||||
-- Clear all security restrictions
|
||||
DELETE FROM login_attempts;
|
||||
|
||||
-- Reset admin password if locked out
|
||||
UPDATE admin_users
|
||||
SET password_hash = '$2b$10$YourKnownGoodHashHere'
|
||||
WHERE username = 'admin';
|
||||
|
||||
-- Remove security columns if causing issues
|
||||
-- (SQLite doesn't support DROP COLUMN easily, so ignore)
|
||||
```
|
||||
|
||||
## Monitoring After Rollback
|
||||
|
||||
```bash
|
||||
# Watch for stability
|
||||
watch -n 5 'docker-compose logs backend | tail -20'
|
||||
|
||||
# Check active connections
|
||||
netstat -an | grep :3001 | wc -l
|
||||
|
||||
# Monitor CPU/Memory
|
||||
docker stats wedding-photo-sharing-backend-1
|
||||
```
|
||||
|
||||
## Prevention for Next Attempt
|
||||
|
||||
Before re-attempting the security enhancement:
|
||||
|
||||
1. **Test in staging environment first**
|
||||
2. **Implement gradual rollout with feature flags**
|
||||
3. **Add backwards compatibility for tokens**
|
||||
4. **Create admin bypass for lockouts**
|
||||
5. **Set up monitoring alerts**
|
||||
|
||||
## Contact
|
||||
|
||||
If rollback fails:
|
||||
1. Check `backend/logs/error.log`
|
||||
2. Restore from last known good backup
|
||||
3. Use original auth implementation as reference
|
||||
@@ -1,119 +0,0 @@
|
||||
# Authentication Security Enhancement Summary
|
||||
|
||||
## Security Issues Fixed
|
||||
|
||||
### 1. ✅ Account Lockout Protection
|
||||
- **Issue**: No protection against brute force attacks
|
||||
- **Fix**: Lock account after 5 failed attempts in 15 minutes
|
||||
- **Files**: `authSecurity.js`, `login_attempts` table
|
||||
|
||||
### 2. ✅ Login Attempt Tracking
|
||||
- **Issue**: No audit trail for security monitoring
|
||||
- **Fix**: Track all login attempts with IP, user agent, timestamp
|
||||
- **Database**: New `login_attempts` table
|
||||
|
||||
### 3. ✅ Generic Error Messages
|
||||
- **Issue**: Different errors could reveal if username exists
|
||||
- **Fix**: Always return "Invalid credentials"
|
||||
- **Impact**: Prevents user enumeration attacks
|
||||
|
||||
### 4. ✅ Session Management
|
||||
- **Issue**: No way to invalidate tokens/logout
|
||||
- **Fix**: Added `/api/auth/logout` endpoint
|
||||
- **Fix**: Session tracking with timeout
|
||||
|
||||
### 5. ✅ Enhanced Token Security
|
||||
- **Issue**: Basic JWT with minimal claims
|
||||
- **Fix**: Added issuer, IP, loginTime claims
|
||||
- **Fix**: Token invalidation on password change
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### New Files Created
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── utils/
|
||||
│ │ └── authSecurity.js (122 lines)
|
||||
│ ├── middleware/
|
||||
│ │ └── auth-enhanced.js (169 lines)
|
||||
│ └── routes/
|
||||
│ └── auth-enhanced.js (244 lines)
|
||||
├── migrations/
|
||||
│ ├── 015_add_login_attempts_table.js
|
||||
│ └── 016_add_auth_security_columns.js
|
||||
└── scripts/
|
||||
└── test-auth-security.js
|
||||
```
|
||||
|
||||
### Database Changes
|
||||
1. **login_attempts** table:
|
||||
- Tracks all authentication attempts
|
||||
- Enables lockout and monitoring
|
||||
|
||||
2. **admin_users** additions:
|
||||
- `password_changed_at` - Invalidate old tokens
|
||||
- `last_login_ip` - Security monitoring
|
||||
- `two_factor_enabled` - Future 2FA support
|
||||
|
||||
## Security Improvements
|
||||
|
||||
### Before
|
||||
- ❌ Unlimited login attempts
|
||||
- ❌ No audit trail
|
||||
- ❌ User enumeration possible
|
||||
- ❌ No session invalidation
|
||||
- ❌ Basic JWT validation
|
||||
|
||||
### After
|
||||
- ✅ Brute force protection
|
||||
- ✅ Complete audit trail
|
||||
- ✅ Generic error messages
|
||||
- ✅ Logout functionality
|
||||
- ✅ Enhanced token validation
|
||||
- ✅ IP tracking
|
||||
- ✅ Password change detection
|
||||
|
||||
## Deployment Safety
|
||||
|
||||
### Gradual Rollout
|
||||
1. **Phase 1**: Deploy code (no impact)
|
||||
2. **Phase 2**: Run migrations (adds tables only)
|
||||
3. **Phase 3**: Initialize tracking (monitoring only)
|
||||
4. **Phase 4**: Switch routes (activates protection)
|
||||
|
||||
### Risk Mitigation
|
||||
- ✅ Backward compatible
|
||||
- ✅ No breaking changes
|
||||
- ✅ Existing tokens remain valid
|
||||
- ✅ Quick rollback possible
|
||||
- ✅ Comprehensive testing
|
||||
|
||||
## Testing Results
|
||||
```
|
||||
✅ All 10 security tests passed
|
||||
✅ Generic errors working
|
||||
✅ Lockout logic verified
|
||||
✅ Token enhancements tested
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Deploy database migrations** (safe)
|
||||
2. **Deploy new files** (no impact)
|
||||
3. **Test in staging** if available
|
||||
4. **Gradual production rollout**
|
||||
5. **Monitor login_attempts table**
|
||||
|
||||
## Monitoring Commands
|
||||
|
||||
```bash
|
||||
# Check failed login attempts
|
||||
sqlite3 database.db "SELECT identifier, COUNT(*) as attempts FROM login_attempts WHERE success = 0 AND attempt_time > datetime('now', '-1 hour') GROUP BY identifier"
|
||||
|
||||
# View recent login activity
|
||||
sqlite3 database.db "SELECT * FROM login_attempts ORDER BY attempt_time DESC LIMIT 10"
|
||||
|
||||
# Check locked accounts
|
||||
sqlite3 database.db "SELECT identifier FROM login_attempts WHERE success = 0 GROUP BY identifier HAVING COUNT(*) >= 5"
|
||||
```
|
||||
@@ -1,232 +0,0 @@
|
||||
# Authentication Security V2 Deployment Plan
|
||||
|
||||
## Overview
|
||||
This deployment adds remaining authentication security fixes identified in the security scan.
|
||||
|
||||
## New Security Features
|
||||
|
||||
### 1. Rate Limiting Bypass Fix ✅
|
||||
- **File**: `src/utils/rateLimitSecurity.js`
|
||||
- **Fix**: Properly validates JWT before skipping rate limit
|
||||
- **Impact**: Prevents attackers from bypassing with invalid tokens
|
||||
|
||||
### 2. Password Complexity Requirements ✅
|
||||
- **File**: `src/utils/passwordValidation.js`
|
||||
- **Features**:
|
||||
- Minimum 12 characters (up from 6)
|
||||
- Must contain: uppercase, lowercase, numbers, special chars
|
||||
- Password strength scoring (zxcvbn)
|
||||
- Context-aware validation (admin vs gallery)
|
||||
- Configurable bcrypt rounds
|
||||
|
||||
### 3. Token Revocation System ✅
|
||||
- **Files**: `src/utils/tokenRevocation.js`, migration
|
||||
- **Features**:
|
||||
- Revoke individual tokens
|
||||
- Revoke all user tokens
|
||||
- Automatic cleanup of expired revocations
|
||||
- Check on every auth request
|
||||
|
||||
### 4. Enhanced Auth Routes ✅
|
||||
- **File**: `src/routes/auth-enhanced-v2.js`
|
||||
- **Features**:
|
||||
- Password change endpoint with validation
|
||||
- Real-time password strength checking
|
||||
- Better error responses with feedback
|
||||
|
||||
## Dependencies to Install
|
||||
|
||||
```bash
|
||||
npm install zxcvbn@4.4.2
|
||||
```
|
||||
|
||||
## Database Migrations
|
||||
|
||||
```sql
|
||||
-- Token revocation tables
|
||||
CREATE TABLE revoked_tokens (
|
||||
id INTEGER PRIMARY KEY,
|
||||
token_id TEXT UNIQUE NOT NULL,
|
||||
user_id INTEGER,
|
||||
token_type TEXT,
|
||||
revoked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
reason TEXT,
|
||||
metadata TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE user_token_revocations (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
revoked_at TIMESTAMP NOT NULL,
|
||||
reason TEXT
|
||||
);
|
||||
```
|
||||
|
||||
## Deployment Steps
|
||||
|
||||
### Phase 1: Preparation (Day 1)
|
||||
|
||||
1. **Install Dependencies**
|
||||
```bash
|
||||
cd backend
|
||||
npm install zxcvbn@4.4.2
|
||||
```
|
||||
|
||||
2. **Run Migrations**
|
||||
```bash
|
||||
docker exec wedding-photo-sharing-backend-1 node scripts/add-token-revocation-tables.js
|
||||
```
|
||||
|
||||
3. **Deploy New Files** (No impact yet)
|
||||
- `rateLimitSecurity.js`
|
||||
- `passwordValidation.js`
|
||||
- `tokenRevocation.js`
|
||||
- `auth-enhanced-v2.js`
|
||||
|
||||
### Phase 2: Testing (Day 2)
|
||||
|
||||
1. **Test Rate Limiting Fix**
|
||||
```bash
|
||||
# Try with invalid token
|
||||
curl -H "Authorization: Bearer invalid-token" \
|
||||
http://localhost:3001/api/admin/events
|
||||
# Should apply rate limiting
|
||||
```
|
||||
|
||||
2. **Test Password Validation**
|
||||
```bash
|
||||
node -e "
|
||||
const {validatePassword} = require('./src/utils/passwordValidation');
|
||||
console.log(validatePassword('weak'));
|
||||
console.log(validatePassword('StrongP@ssw0rd123'));
|
||||
"
|
||||
```
|
||||
|
||||
### Phase 3: Gradual Activation (Day 3)
|
||||
|
||||
#### Step 1: Update Server.js for Rate Limiting
|
||||
```javascript
|
||||
// Replace in server.js
|
||||
const { createSecureSkipFunction, logRateLimitHit } = require('./src/utils/rateLimitSecurity');
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: process.env.NODE_ENV === 'development' ? 1000 : 100,
|
||||
skip: createSecureSkipFunction(), // NEW: Secure skip function
|
||||
handler: (req, res) => {
|
||||
logRateLimitHit(req, res); // NEW: Logging
|
||||
res.status(429).json({
|
||||
error: 'Too many requests from this IP, please try again later.'
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Step 2: Update Auth Routes
|
||||
```javascript
|
||||
// In server.js, change to v2
|
||||
const authRoutes = require('./src/routes/auth-enhanced-v2');
|
||||
```
|
||||
|
||||
#### Step 3: Update Middleware
|
||||
```javascript
|
||||
// Update imports to use v2
|
||||
const { adminAuth } = require('./src/middleware/auth-enhanced-v2');
|
||||
```
|
||||
|
||||
#### Step 4: Update Event Creation
|
||||
```javascript
|
||||
// In adminEvents.js, add password validation
|
||||
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
|
||||
|
||||
// In the POST route, add validation before hashing
|
||||
```
|
||||
|
||||
#### Step 5: Initialize Token Revocation
|
||||
```javascript
|
||||
// In server.js, after initializeCleanupJob()
|
||||
const { initializeRevocationCleanup } = require('./src/utils/tokenRevocation');
|
||||
initializeRevocationCleanup();
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Add to `.env`:
|
||||
```bash
|
||||
# Bcrypt rounds (12-14 recommended)
|
||||
BCRYPT_ROUNDS=12
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Invalid tokens can't bypass rate limiting
|
||||
- [ ] Weak passwords are rejected
|
||||
- [ ] Password change requires strong password
|
||||
- [ ] Tokens can be revoked
|
||||
- [ ] Revoked tokens are rejected
|
||||
- [ ] Admin passwords require higher strength
|
||||
- [ ] Gallery passwords check for event name
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
### Quick Rollback
|
||||
```bash
|
||||
# Revert server.js changes
|
||||
git checkout HEAD -- server.js
|
||||
|
||||
# Restart
|
||||
docker-compose restart backend
|
||||
```
|
||||
|
||||
### Rollback Specific Features
|
||||
|
||||
1. **Rate Limiting**: Revert to old skip function
|
||||
2. **Password Validation**: Remove validation calls
|
||||
3. **Token Revocation**: Skip revocation checks
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Check Password Validation Failures
|
||||
```bash
|
||||
docker-compose logs backend | grep "Password validation failed"
|
||||
```
|
||||
|
||||
### Check Rate Limiting
|
||||
```bash
|
||||
docker-compose logs backend | grep "Rate limit"
|
||||
```
|
||||
|
||||
### Check Token Revocations
|
||||
```bash
|
||||
docker exec wedding-photo-sharing-backend-1 node -e "
|
||||
const {db} = require('./src/database/db');
|
||||
db('revoked_tokens').count().first()
|
||||
.then(r => console.log('Revoked tokens:', r['count(*)'] || 0))
|
||||
.then(() => db.destroy());
|
||||
"
|
||||
```
|
||||
|
||||
## Security Improvements
|
||||
|
||||
| Feature | Before | After |
|
||||
|---------|---------|--------|
|
||||
| Rate Limiting | Can bypass with invalid token | Properly validated |
|
||||
| Password Length | 6 chars | 12 chars minimum |
|
||||
| Password Complexity | None | Upper+lower+number+special |
|
||||
| Password Strength | Not checked | zxcvbn scoring |
|
||||
| Token Revocation | Not possible | Full revocation system |
|
||||
| Bcrypt Rounds | Fixed (10) | Configurable (12) |
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Password Validation**: ~50ms per check (zxcvbn)
|
||||
2. **Token Revocation**: Adds 1 DB query per request
|
||||
3. **Bcrypt Rounds**: 12 rounds = ~250ms (vs 100ms for 10)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- ✅ No invalid tokens bypass rate limiting
|
||||
- ✅ All new passwords meet complexity requirements
|
||||
- ✅ Password change works with validation
|
||||
- ✅ Tokens can be revoked on logout
|
||||
- ✅ No performance degradation > 100ms
|
||||
@@ -1,114 +0,0 @@
|
||||
# Authentication V2 Security Fixes Summary
|
||||
|
||||
## What We Fixed
|
||||
|
||||
### 1. ✅ Rate Limiting Bypass (CRITICAL)
|
||||
**Issue**: Invalid JWT tokens could bypass rate limiting
|
||||
**Fix**: Created `rateLimitSecurity.js` that properly validates tokens
|
||||
**Impact**: Attackers can no longer spam requests with invalid tokens
|
||||
|
||||
### 2. ✅ Weak Password Requirements (HIGH)
|
||||
**Issue**: Only 6 character minimum, no complexity
|
||||
**Fix**: Created `passwordValidation.js` with:
|
||||
- 12 character minimum
|
||||
- Must have: uppercase, lowercase, numbers, special chars
|
||||
- Password strength scoring (zxcvbn)
|
||||
- Context-aware validation (prevents username/event name in password)
|
||||
- Configurable bcrypt rounds (default 12)
|
||||
**Impact**: Much stronger passwords, resistant to brute force
|
||||
|
||||
### 3. ✅ Token Revocation (MEDIUM)
|
||||
**Issue**: No way to invalidate tokens before expiration
|
||||
**Fix**: Created `tokenRevocation.js` with full revocation system
|
||||
- Individual token revocation
|
||||
- User-level revocation (all tokens)
|
||||
- Automatic cleanup
|
||||
- Database tables for tracking
|
||||
**Impact**: Can now invalidate compromised tokens
|
||||
|
||||
### 4. ✅ Enhanced Authentication Routes
|
||||
**Fix**: Created `auth-enhanced-v2.js` with:
|
||||
- Password change endpoint with validation
|
||||
- Real-time password strength API
|
||||
- Better error messages with feedback
|
||||
**Impact**: Users get helpful password feedback
|
||||
|
||||
## Files Created
|
||||
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── utils/
|
||||
│ │ ├── rateLimitSecurity.js (118 lines)
|
||||
│ │ ├── passwordValidation.js (267 lines)
|
||||
│ │ └── tokenRevocation.js (127 lines)
|
||||
│ ├── routes/
|
||||
│ │ ├── auth-enhanced-v2.js (332 lines)
|
||||
│ │ └── adminEvents-enhanced.js (partial)
|
||||
│ └── middleware/
|
||||
│ └── auth-enhanced-v2.js (updated)
|
||||
├── migrations/
|
||||
│ └── 017_add_token_revocation_tables.js
|
||||
├── scripts/
|
||||
│ ├── add-token-revocation-tables.js
|
||||
│ └── test-auth-v2-fixes.js
|
||||
└── server-enhanced.js (partial)
|
||||
```
|
||||
|
||||
## Deployment Status
|
||||
|
||||
### Ready to Deploy ✅
|
||||
- All code written and tested
|
||||
- Migration scripts ready
|
||||
- Test scripts available
|
||||
- Rollback plan documented
|
||||
|
||||
### Required Actions
|
||||
1. Install `zxcvbn` dependency
|
||||
2. Run token revocation migration
|
||||
3. Update server.js with new imports
|
||||
4. Update auth routes to v2
|
||||
5. Test thoroughly before production
|
||||
|
||||
## Security Improvements Summary
|
||||
|
||||
| Vulnerability | Severity | Status | Fix |
|
||||
|--------------|----------|---------|-----|
|
||||
| Rate Limiting Bypass | 🔴 Critical | ✅ Fixed | Proper token validation |
|
||||
| Weak Passwords | 🔴 High | ✅ Fixed | 12 chars + complexity |
|
||||
| No Token Revocation | 🟡 Medium | ✅ Fixed | Full revocation system |
|
||||
| Fixed Bcrypt Rounds | 🟡 Medium | ✅ Fixed | Configurable (env var) |
|
||||
| No Password Feedback | 🟡 Low | ✅ Fixed | Strength API endpoint |
|
||||
|
||||
## What's Still Pending
|
||||
|
||||
From the original auth flaws, these remain lower priority:
|
||||
1. **In-memory session storage** - Works fine for single instance
|
||||
2. **No refresh tokens** - 24h tokens are reasonable for this use case
|
||||
3. **Fixed token expiration** - Could make configurable later
|
||||
|
||||
## Testing Commands
|
||||
|
||||
```bash
|
||||
# Test rate limiting fix
|
||||
node scripts/test-auth-v2-fixes.js
|
||||
|
||||
# Test password validation
|
||||
node -e "
|
||||
const {validatePassword} = require('./src/utils/passwordValidation');
|
||||
console.log(validatePassword('Test123!Pass'));
|
||||
"
|
||||
|
||||
# Check if tables exist
|
||||
docker exec wedding-photo-sharing-backend-1 node scripts/add-token-revocation-tables.js
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Review `AUTH_V2_DEPLOYMENT_PLAN.md`
|
||||
2. Install zxcvbn: `npm install zxcvbn@4.4.2`
|
||||
3. Run migrations
|
||||
4. Deploy incrementally
|
||||
5. Monitor for issues
|
||||
|
||||
All critical authentication vulnerabilities have been addressed with production-ready fixes!
|
||||
+6
-3
@@ -16,8 +16,8 @@ FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dumb-init for proper signal handling
|
||||
RUN apk add --no-cache dumb-init
|
||||
# Install dumb-init for proper signal handling and postgresql-client for database checks
|
||||
RUN apk add --no-cache dumb-init postgresql-client
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
|
||||
@@ -26,6 +26,9 @@ RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
|
||||
COPY --chown=nodejs:nodejs . .
|
||||
|
||||
# Make wait script executable
|
||||
RUN chmod +x wait-for-db.sh
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p storage/events/active storage/events/archived storage/thumbnails data logs && \
|
||||
chown -R nodejs:nodejs storage data logs
|
||||
@@ -35,4 +38,4 @@ USER nodejs
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
CMD ["node", "server.js"]
|
||||
CMD ["./wait-for-db.sh", "node", "server.js"]
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
# Safe Authentication Security Activation Plan
|
||||
|
||||
## Current Situation Analysis
|
||||
|
||||
### ✅ What's Already Protected:
|
||||
- **SQL Injection**: Fully protected with parameterized queries
|
||||
- **Rate Limiting**: Basic rate limiting active (5 attempts/15 min on /auth)
|
||||
- **Password Hashing**: Bcrypt in use
|
||||
- **CORS**: Properly configured
|
||||
|
||||
### ❌ What's NOT Protected:
|
||||
- **No Account Lockout**: After rate limit, users can keep trying
|
||||
- **No Audit Trail**: Can't track attack patterns
|
||||
- **No Session Invalidation**: Can't force logout
|
||||
- **Limited Token Security**: Basic JWT validation only
|
||||
|
||||
## Potential Problems & Solutions
|
||||
|
||||
### Problem 1: Existing User Sessions
|
||||
**Risk**: Users might get logged out unexpectedly
|
||||
**Solution**:
|
||||
- Enhanced auth accepts old tokens (backward compatible)
|
||||
- Tokens remain valid until natural expiration
|
||||
- Only new features (IP check, password change detection) are additions
|
||||
|
||||
### Problem 2: Accidental Lockouts
|
||||
**Risk**: Legitimate users locked out due to typos
|
||||
**Solution**:
|
||||
- 5 attempts is reasonable (not too strict)
|
||||
- 30-minute lockout (not permanent)
|
||||
- Clear lockout message with retry time
|
||||
- Admin bypass SQL query ready
|
||||
|
||||
### Problem 3: Database Migration Failure
|
||||
**Risk**: Schema changes could fail
|
||||
**Solution**:
|
||||
- Migrations only ADD tables/columns (no modifications)
|
||||
- Automatic backup before migration
|
||||
- Rollback plan ready
|
||||
- SQLite is forgiving with schema changes
|
||||
|
||||
### Problem 4: Performance Impact
|
||||
**Risk**: Login tracking could slow down auth
|
||||
**Solution**:
|
||||
- Indexed columns for performance
|
||||
- Automatic cleanup of old records
|
||||
- Async logging (non-blocking)
|
||||
|
||||
## Step-by-Step Activation Plan
|
||||
|
||||
### Phase 1: Pre-Flight Checks (NOW)
|
||||
```bash
|
||||
# Run safety check script
|
||||
cd backend
|
||||
node scripts/safe-auth-deployment.js
|
||||
```
|
||||
This will:
|
||||
- ✓ Check database health
|
||||
- ✓ Count active sessions
|
||||
- ✓ Create backup
|
||||
- ✓ Test enhanced auth modules
|
||||
|
||||
### Phase 2: Database Preparation (SAFE)
|
||||
```bash
|
||||
# Run in Docker
|
||||
docker exec wedding-photo-sharing-backend-1 npx knex migrate:latest
|
||||
```
|
||||
Creates:
|
||||
- `login_attempts` table (new)
|
||||
- Security columns in `admin_users` (nullable)
|
||||
|
||||
### Phase 3: Test Without Activation
|
||||
```bash
|
||||
# Test enhanced auth endpoints
|
||||
chmod +x scripts/test-auth-deployment.sh
|
||||
./scripts/test-auth-deployment.sh
|
||||
```
|
||||
Verifies enhanced auth works before switching
|
||||
|
||||
### Phase 4: Gradual Activation
|
||||
|
||||
#### Option A: Canary Deployment (SAFEST)
|
||||
Add temporary route to test:
|
||||
```javascript
|
||||
// In server.js, add both temporarily
|
||||
app.use('/api/auth', authRoutes); // Original
|
||||
app.use('/api/auth-new', authEnhancedRoutes); // Test enhanced
|
||||
```
|
||||
|
||||
Test with `/api/auth-new/admin/login` first
|
||||
|
||||
#### Option B: Feature Flag (RECOMMENDED)
|
||||
```javascript
|
||||
// In server.js
|
||||
const useEnhancedAuth = process.env.USE_ENHANCED_AUTH === 'true';
|
||||
const authRoutes = useEnhancedAuth
|
||||
? require('./src/routes/auth-enhanced')
|
||||
: require('./src/routes/auth');
|
||||
```
|
||||
|
||||
Then activate with environment variable
|
||||
|
||||
#### Option C: Direct Switch (FASTER)
|
||||
```javascript
|
||||
// Change in server.js
|
||||
const authRoutes = require('./src/routes/auth-enhanced');
|
||||
|
||||
// Add after DB init
|
||||
const { initializeCleanupJob } = require('./src/utils/authSecurity');
|
||||
initializeCleanupJob();
|
||||
```
|
||||
|
||||
### Phase 5: Monitor After Activation
|
||||
```bash
|
||||
# Run monitoring script
|
||||
node scripts/monitor-auth-health.js
|
||||
```
|
||||
|
||||
Watch for:
|
||||
- Sudden spike in failures
|
||||
- Multiple lockouts
|
||||
- Low success rate
|
||||
|
||||
## Rollback Procedures
|
||||
|
||||
### Quick Rollback (< 30 seconds):
|
||||
```bash
|
||||
# In server.js, revert to:
|
||||
const authRoutes = require('./src/routes/auth');
|
||||
|
||||
# Restart
|
||||
docker-compose restart backend
|
||||
```
|
||||
|
||||
### Clear All Lockouts:
|
||||
```bash
|
||||
docker exec wedding-photo-sharing-backend-1 node -e "
|
||||
const {db} = require('./src/database/db');
|
||||
db('login_attempts').where('success', false).delete()
|
||||
.then(() => console.log('Lockouts cleared'))
|
||||
.then(() => db.destroy());
|
||||
"
|
||||
```
|
||||
|
||||
### Emergency Admin Access:
|
||||
```sql
|
||||
-- If admin is locked out
|
||||
DELETE FROM login_attempts WHERE identifier = 'admin';
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
|
||||
After activation, you should see:
|
||||
1. ✅ Failed login attempts recorded in database
|
||||
2. ✅ Account lockout after 5 failures
|
||||
3. ✅ Logout endpoint working
|
||||
4. ✅ No increase in auth errors
|
||||
5. ✅ Existing users still able to login
|
||||
|
||||
## Timeline Recommendation
|
||||
|
||||
**Day 1 (Now)**:
|
||||
- Run migrations ✓
|
||||
- Deploy code ✓
|
||||
- Test endpoints
|
||||
|
||||
**Day 2**:
|
||||
- Monitor current auth patterns
|
||||
- Run test script during low traffic
|
||||
|
||||
**Day 3**:
|
||||
- Activate with feature flag
|
||||
- Monitor closely for 2 hours
|
||||
- Full activation if stable
|
||||
|
||||
**Day 4+**:
|
||||
- Review login_attempts data
|
||||
- Adjust thresholds if needed
|
||||
- Plan 2FA implementation
|
||||
|
||||
## Commands Reference
|
||||
|
||||
```bash
|
||||
# Activate enhanced auth
|
||||
docker exec -it wedding-photo-sharing-backend-1 /bin/sh
|
||||
vi server.js # Make changes
|
||||
exit
|
||||
docker-compose restart backend
|
||||
|
||||
# Monitor
|
||||
docker-compose logs -f backend | grep -i auth
|
||||
|
||||
# Check lockouts
|
||||
docker exec wedding-photo-sharing-backend-1 node -e "
|
||||
const {db} = require('./src/database/db');
|
||||
db('login_attempts')
|
||||
.select('identifier')
|
||||
.where('success', false)
|
||||
.where('attempt_time', '>', new Date(Date.now() - 15*60*1000).toISOString())
|
||||
.groupBy('identifier')
|
||||
.havingRaw('COUNT(*) >= 5')
|
||||
.then(locked => console.log('Locked accounts:', locked))
|
||||
.then(() => db.destroy());
|
||||
"
|
||||
```
|
||||
|
||||
## Final Safety Notes
|
||||
|
||||
1. **It's been tested**: 10/10 unit tests pass
|
||||
2. **It's backward compatible**: Old tokens work
|
||||
3. **It's gradual**: Can activate features separately
|
||||
4. **It's reversible**: Quick rollback available
|
||||
5. **It's monitored**: Health checking included
|
||||
|
||||
The enhanced auth is designed to be transparent to users while significantly improving security. The only visible change is lockout messages after failed attempts.
|
||||
@@ -1,167 +0,0 @@
|
||||
# Security Fixes Deployment Complete ✅
|
||||
|
||||
## Current Protection Status
|
||||
|
||||
### 🛡️ FULLY PROTECTED Against:
|
||||
|
||||
1. **SQL Injection** ✅
|
||||
- All `whereRaw` queries replaced with parameterized queries
|
||||
- LIKE patterns properly escaped
|
||||
- Input validation for all user inputs
|
||||
- **Status**: ACTIVE & PROTECTING
|
||||
|
||||
2. **Brute Force Attacks** ✅
|
||||
- Account lockout after 5 failed attempts
|
||||
- 30-minute lockout duration
|
||||
- IP and user agent tracking
|
||||
- **Status**: ACTIVE & PROTECTING
|
||||
|
||||
3. **User Enumeration** ✅
|
||||
- Generic error messages for all auth failures
|
||||
- Returns "Invalid credentials" consistently
|
||||
- **Status**: ACTIVE & PROTECTING
|
||||
|
||||
4. **Session Security** ✅
|
||||
- Enhanced JWT with issuer validation
|
||||
- IP tracking in tokens
|
||||
- Password change detection
|
||||
- Logout endpoint functional
|
||||
- **Status**: ACTIVE & PROTECTING
|
||||
|
||||
5. **Audit Trail** ✅
|
||||
- All login attempts tracked in database
|
||||
- Success/failure logging with timestamps
|
||||
- IP address and user agent recording
|
||||
- **Status**: ACTIVE & LOGGING
|
||||
|
||||
## What Was Done
|
||||
|
||||
### Database Changes
|
||||
- ✅ Created `login_attempts` table for tracking
|
||||
- ✅ Added security columns to `admin_users`:
|
||||
- `password_changed_at`
|
||||
- `last_login_ip`
|
||||
- `two_factor_enabled`
|
||||
- `two_factor_secret`
|
||||
|
||||
### Code Changes
|
||||
- ✅ SQL injection fixes in 3 files
|
||||
- ✅ Enhanced auth middleware deployed
|
||||
- ✅ Enhanced auth routes active
|
||||
- ✅ Security utilities in place
|
||||
- ✅ Cleanup job running
|
||||
|
||||
### Files Modified/Created
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── utils/
|
||||
│ │ ├── sqlSecurity.js ✅
|
||||
│ │ └── authSecurity.js ✅
|
||||
│ ├── middleware/
|
||||
│ │ └── auth-enhanced.js ✅
|
||||
│ └── routes/
|
||||
│ ├── auth-enhanced.js ✅
|
||||
│ ├── adminDashboard.js ✅ (SQL fixes)
|
||||
│ ├── adminEvents.js ✅ (SQL fixes)
|
||||
│ └── adminPhotos.js ✅ (SQL fixes)
|
||||
└── server.js ✅ (using enhanced auth)
|
||||
```
|
||||
|
||||
## Monitoring Commands
|
||||
|
||||
### Check Login Attempts
|
||||
```bash
|
||||
docker exec wedding-photo-sharing-backend-1 node -e "
|
||||
const {db} = require('./src/database/db');
|
||||
db('login_attempts')
|
||||
.orderBy('attempt_time', 'desc')
|
||||
.limit(10)
|
||||
.then(attempts => {
|
||||
console.log('Recent login attempts:');
|
||||
attempts.forEach(a => {
|
||||
console.log(\`\${a.attempt_time} - \${a.identifier} - \${a.success ? 'SUCCESS' : 'FAILED'}\`);
|
||||
});
|
||||
})
|
||||
.then(() => db.destroy());
|
||||
"
|
||||
```
|
||||
|
||||
### Check Locked Accounts
|
||||
```bash
|
||||
docker exec wedding-photo-sharing-backend-1 node -e "
|
||||
const {db} = require('./src/database/db');
|
||||
db('login_attempts')
|
||||
.select('identifier')
|
||||
.where('success', false)
|
||||
.where('attempt_time', '>', new Date(Date.now() - 15*60*1000).toISOString())
|
||||
.groupBy('identifier')
|
||||
.havingRaw('COUNT(*) >= 5')
|
||||
.then(locked => console.log('Locked accounts:', locked))
|
||||
.then(() => db.destroy());
|
||||
"
|
||||
```
|
||||
|
||||
### Monitor Health
|
||||
```bash
|
||||
node scripts/monitor-auth-health.js
|
||||
```
|
||||
|
||||
## Rollback Plan (If Needed)
|
||||
|
||||
### Quick Rollback
|
||||
```bash
|
||||
# Restore original server.js
|
||||
cp server.js.backup.1752359680463 server.js
|
||||
|
||||
# Restart
|
||||
docker-compose restart backend
|
||||
```
|
||||
|
||||
### Clear Lockouts
|
||||
```bash
|
||||
docker exec wedding-photo-sharing-backend-1 node -e "
|
||||
const {db} = require('./src/database/db');
|
||||
db('login_attempts').where('success', false).delete()
|
||||
.then(() => console.log('All lockouts cleared'))
|
||||
.then(() => db.destroy());
|
||||
"
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Immediate
|
||||
1. Monitor logs for any auth errors
|
||||
2. Watch for excessive lockouts
|
||||
3. Review login attempts daily
|
||||
|
||||
### Short Term (1-2 weeks)
|
||||
1. Analyze login patterns
|
||||
2. Adjust lockout thresholds if needed
|
||||
3. Set up alerts for suspicious activity
|
||||
|
||||
### Long Term
|
||||
1. Implement 2FA (columns already added)
|
||||
2. Add IP whitelisting for admins
|
||||
3. Implement password complexity requirements
|
||||
4. Add password expiration policies
|
||||
|
||||
## Security Improvements Summary
|
||||
|
||||
| Vulnerability | Before | After | Impact |
|
||||
|--------------|---------|--------|---------|
|
||||
| SQL Injection | ❌ Direct interpolation | ✅ Parameterized queries | Critical fix |
|
||||
| Brute Force | ❌ Unlimited attempts | ✅ 5 attempt lockout | High impact |
|
||||
| User Enum | ❌ Different errors | ✅ Generic errors | Medium impact |
|
||||
| Audit Trail | ❌ No tracking | ✅ Complete logging | High value |
|
||||
| Session Mgmt | ❌ Basic JWT | ✅ Enhanced validation | Medium impact |
|
||||
|
||||
## Final Notes
|
||||
|
||||
- All fixes are backward compatible
|
||||
- Existing sessions remain valid
|
||||
- No user impact expected
|
||||
- Quick rollback available
|
||||
- Monitoring in place
|
||||
|
||||
The application is now significantly more secure with protection against common attack vectors. The enhanced authentication system provides defense-in-depth with multiple layers of security.
|
||||
@@ -1,158 +0,0 @@
|
||||
# SQL Injection Fix Migration Guide
|
||||
|
||||
## Overview
|
||||
This document describes the SQL injection vulnerability fixes applied to the PicPeak backend and the migration process for deploying these fixes to production.
|
||||
|
||||
## Vulnerabilities Fixed
|
||||
|
||||
### 1. WhereRaw Date Queries (High Risk)
|
||||
**Location**: `adminDashboard.js`
|
||||
- **Issue**: Direct string interpolation in SQL date calculations
|
||||
- **Example**: `.whereRaw(\`timestamp >= datetime("now", "-${days} days")\`)`
|
||||
- **Fix**: Replaced with parameterized queries using ISO date strings
|
||||
|
||||
### 2. LIKE Pattern Injection (Medium Risk)
|
||||
**Locations**: `adminEvents.js`, `adminPhotos.js`
|
||||
- **Issue**: Unescaped user input in LIKE queries
|
||||
- **Example**: `.where('event_name', 'like', \`%${search}%\`)`
|
||||
- **Fix**: Added proper escaping for LIKE special characters (%, _, \)
|
||||
|
||||
### 3. Dynamic Column/Order Injection (Low Risk)
|
||||
**Locations**: Various sorting operations
|
||||
- **Issue**: Unvalidated column names in ORDER BY
|
||||
- **Fix**: Whitelist validation for sort columns and orders
|
||||
|
||||
## Files Changed
|
||||
|
||||
1. **Created**: `backend/src/utils/sqlSecurity.js`
|
||||
- Central security utility functions
|
||||
- `sanitizeDays()` - Validates numeric input
|
||||
- `escapeLikePattern()` - Escapes LIKE wildcards
|
||||
- `validateSortColumn()` - Whitelist validation
|
||||
- `validateSortOrder()` - Ensures only 'asc' or 'desc'
|
||||
|
||||
2. **Modified**: `backend/src/routes/adminDashboard.js`
|
||||
- Lines 21-24, 39-41, 45-47, 57-61, 65-68: Replaced whereRaw with parameterized queries
|
||||
- Line 4: Added security utility imports
|
||||
- Line 198: Added sanitizeDays for analytics
|
||||
|
||||
3. **Modified**: `backend/src/routes/adminEvents.js`
|
||||
- Line 11: Added escapeLikePattern import
|
||||
- Lines 156-161: Escaped search patterns in LIKE queries
|
||||
|
||||
4. **Modified**: `backend/src/routes/adminPhotos.js`
|
||||
- Line 9: Added escapeLikePattern import
|
||||
- Lines 477-478: Escaped search patterns in LIKE queries
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### 1. Pre-Deployment Testing
|
||||
|
||||
```bash
|
||||
# Run security utility tests
|
||||
cd backend
|
||||
node scripts/test-sql-security.js
|
||||
|
||||
# Run verification script
|
||||
node scripts/verify-sql-fixes.js
|
||||
```
|
||||
|
||||
### 2. Development Environment Testing
|
||||
|
||||
```bash
|
||||
# Start development server
|
||||
npm run dev
|
||||
|
||||
# Test key endpoints:
|
||||
curl http://localhost:3001/api/admin/dashboard/stats -H "Authorization: Bearer YOUR_TOKEN"
|
||||
curl http://localhost:3001/api/admin/events?search=test -H "Authorization: Bearer YOUR_TOKEN"
|
||||
curl http://localhost:3001/api/admin/dashboard/analytics?days=7 -H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
### 3. Production Deployment
|
||||
|
||||
#### Option A: Docker Deployment
|
||||
```bash
|
||||
# Pull latest changes
|
||||
git pull
|
||||
|
||||
# Rebuild and restart
|
||||
docker-compose down
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
#### Option B: PM2 Deployment
|
||||
```bash
|
||||
# Pull latest changes
|
||||
git pull
|
||||
|
||||
# Install dependencies (if any)
|
||||
cd backend
|
||||
npm install
|
||||
|
||||
# Restart with PM2
|
||||
pm2 restart picpeak-backend
|
||||
```
|
||||
|
||||
### 4. Post-Deployment Verification
|
||||
|
||||
1. **Monitor Logs**:
|
||||
```bash
|
||||
# Docker
|
||||
docker-compose logs -f backend
|
||||
|
||||
# PM2
|
||||
pm2 logs picpeak-backend
|
||||
```
|
||||
|
||||
2. **Test Critical Functions**:
|
||||
- Admin dashboard loads correctly
|
||||
- Event search works with special characters
|
||||
- Analytics charts display properly
|
||||
- Photo search functions normally
|
||||
|
||||
3. **Check Error Rates**:
|
||||
- Monitor for any 500 errors
|
||||
- Check database query logs for errors
|
||||
|
||||
## Testing Special Characters
|
||||
|
||||
After deployment, test these scenarios:
|
||||
|
||||
1. **Search with wildcards**: Search for "50%" or "user_name"
|
||||
2. **Search with quotes**: Search for "O'Brien"
|
||||
3. **Date range**: Change analytics to different day ranges
|
||||
4. **Malicious input**: Try "'; DROP TABLE --" (should return no results)
|
||||
|
||||
## Rollback Instructions
|
||||
|
||||
If issues occur, see `SQL_INJECTION_FIX_ROLLBACK.md` for immediate rollback steps.
|
||||
|
||||
## Performance Impact
|
||||
|
||||
- Minimal performance impact expected
|
||||
- Date calculations now use ISO strings instead of SQLite functions
|
||||
- LIKE pattern escaping adds negligible overhead
|
||||
- All changes maintain existing query optimization
|
||||
|
||||
## Security Improvements
|
||||
|
||||
1. **Eliminated SQL Injection Vectors**: No more direct string interpolation
|
||||
2. **Input Validation**: All user inputs are validated/sanitized
|
||||
3. **Parameterized Queries**: Using Knex's built-in parameterization
|
||||
4. **Defense in Depth**: Multiple layers of protection
|
||||
|
||||
## Future Recommendations
|
||||
|
||||
1. Add request validation middleware
|
||||
2. Implement rate limiting on search endpoints
|
||||
3. Add SQL query logging for security auditing
|
||||
4. Consider using prepared statements for complex queries
|
||||
|
||||
## Questions/Support
|
||||
|
||||
If you encounter any issues during migration:
|
||||
1. Check the rollback plan first
|
||||
2. Review error logs for specific issues
|
||||
3. Test individual endpoints to isolate problems
|
||||
4. Contact development team if needed
|
||||
@@ -1,94 +0,0 @@
|
||||
# SQL Injection Fix Rollback Plan
|
||||
|
||||
## Overview
|
||||
This document provides a rollback plan in case the SQL injection fixes cause issues in production.
|
||||
|
||||
## Changes Made
|
||||
1. **Created**: `backend/src/utils/sqlSecurity.js` - Central security utilities
|
||||
2. **Modified**: `backend/src/routes/adminDashboard.js` - Replaced whereRaw with parameterized queries
|
||||
3. **Modified**: `backend/src/routes/adminPhotos.js` - Added LIKE pattern escaping
|
||||
4. **Modified**: `backend/src/routes/adminEvents.js` - Added LIKE pattern escaping
|
||||
|
||||
## Quick Rollback Steps
|
||||
|
||||
### Step 1: Revert Code Changes
|
||||
If issues occur, run these commands to revert:
|
||||
|
||||
```bash
|
||||
# Navigate to backend directory
|
||||
cd backend
|
||||
|
||||
# Revert specific files
|
||||
git checkout HEAD -- src/routes/adminDashboard.js
|
||||
git checkout HEAD -- src/routes/adminPhotos.js
|
||||
git checkout HEAD -- src/routes/adminEvents.js
|
||||
|
||||
# Remove the new security utility file
|
||||
rm src/utils/sqlSecurity.js
|
||||
```
|
||||
|
||||
### Step 2: Restart Services
|
||||
```bash
|
||||
# If using Docker
|
||||
docker-compose restart backend
|
||||
|
||||
# If using PM2
|
||||
pm2 restart picpeak-backend
|
||||
```
|
||||
|
||||
## Verification After Rollback
|
||||
|
||||
1. Check admin dashboard loads: `/admin/dashboard`
|
||||
2. Test event search functionality
|
||||
3. Test photo search functionality
|
||||
4. Verify analytics charts display correctly
|
||||
|
||||
## Symptoms That May Require Rollback
|
||||
|
||||
1. **Dashboard Statistics Not Loading**
|
||||
- Empty or NaN values in stats
|
||||
- Analytics charts not rendering
|
||||
|
||||
2. **Search Features Broken**
|
||||
- Event search returns no results
|
||||
- Photo search returns errors
|
||||
- Special characters in search causing issues
|
||||
|
||||
3. **Date Filtering Issues**
|
||||
- Activity logs not showing correct date ranges
|
||||
- Analytics showing incorrect time periods
|
||||
|
||||
## Safe Testing Before Production
|
||||
|
||||
1. **Test in Development First**:
|
||||
```bash
|
||||
cd backend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
2. **Test Key Features**:
|
||||
- Admin dashboard stats: `http://localhost:3001/api/admin/dashboard/stats`
|
||||
- Analytics: `http://localhost:3001/api/admin/dashboard/analytics?days=7`
|
||||
- Event search: `http://localhost:3001/api/admin/events?search=test`
|
||||
- Photo search: `http://localhost:3001/api/admin/events/1/photos?search=test`
|
||||
|
||||
3. **Monitor Logs**:
|
||||
```bash
|
||||
# Docker logs
|
||||
docker-compose logs -f backend
|
||||
|
||||
# PM2 logs
|
||||
pm2 logs picpeak-backend
|
||||
```
|
||||
|
||||
## Emergency Contacts
|
||||
- Keep database backups before deploying
|
||||
- Have monitoring alerts for 500 errors
|
||||
- Document any custom SQL queries in use
|
||||
|
||||
## Post-Rollback Actions
|
||||
If rollback is needed:
|
||||
1. Document the specific issue encountered
|
||||
2. Create test cases for the failure scenario
|
||||
3. Fix the issue in development
|
||||
4. Re-test thoroughly before re-deploying
|
||||
@@ -1,64 +0,0 @@
|
||||
# SQL Injection Fix Summary
|
||||
|
||||
## Quick Overview
|
||||
Fixed SQL injection vulnerabilities in the admin panel endpoints by:
|
||||
1. Replacing dangerous `whereRaw` queries with parameterized queries
|
||||
2. Escaping special characters in LIKE patterns
|
||||
3. Validating sort columns and orders
|
||||
|
||||
## Test Results
|
||||
✅ All 31 security tests passed
|
||||
✅ Verification script confirms fixes working
|
||||
✅ No breaking changes to API functionality
|
||||
|
||||
## Changed Files
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── utils/
|
||||
│ │ └── sqlSecurity.js (NEW - 117 lines)
|
||||
│ └── routes/
|
||||
│ ├── adminDashboard.js (6 changes)
|
||||
│ ├── adminEvents.js (2 changes)
|
||||
│ └── adminPhotos.js (2 changes)
|
||||
└── scripts/
|
||||
├── test-sql-security.js (NEW)
|
||||
└── verify-sql-fixes.js (NEW)
|
||||
```
|
||||
|
||||
## Before & After Examples
|
||||
|
||||
### Date Range Queries
|
||||
```javascript
|
||||
// ❌ BEFORE (Vulnerable)
|
||||
.whereRaw(`timestamp >= datetime("now", "-${days} days")`)
|
||||
|
||||
// ✅ AFTER (Safe)
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - sanitizeDays(days));
|
||||
.where('timestamp', '>=', startDate.toISOString())
|
||||
```
|
||||
|
||||
### LIKE Queries
|
||||
```javascript
|
||||
// ❌ BEFORE (Vulnerable)
|
||||
.where('event_name', 'like', `%${search}%`)
|
||||
|
||||
// ✅ AFTER (Safe)
|
||||
const escapedSearch = escapeLikePattern(search);
|
||||
.where('event_name', 'like', `%${escapedSearch}%`)
|
||||
```
|
||||
|
||||
## Deployment Checklist
|
||||
- [ ] Run `node scripts/test-sql-security.js` (should show 31/31 passed)
|
||||
- [ ] Test in development environment
|
||||
- [ ] Review rollback plan (`SQL_INJECTION_FIX_ROLLBACK.md`)
|
||||
- [ ] Deploy to production
|
||||
- [ ] Monitor logs for errors
|
||||
- [ ] Test search functionality with special characters
|
||||
|
||||
## Risk Assessment
|
||||
- **Risk Level**: Low (with proper testing)
|
||||
- **Breaking Changes**: None
|
||||
- **Performance Impact**: Minimal
|
||||
- **Rollback Time**: < 2 minutes
|
||||
Binary file not shown.
@@ -0,0 +1,47 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const path = require('path');
|
||||
|
||||
// Database configuration for different environments
|
||||
const config = {
|
||||
development: {
|
||||
client: process.env.DATABASE_CLIENT || 'sqlite3',
|
||||
connection: process.env.DATABASE_CLIENT === 'pg' ? {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
database: process.env.DB_NAME || 'photo_sharing'
|
||||
} : {
|
||||
filename: path.join(__dirname, process.env.DATABASE_PATH || './data/photo_sharing.db')
|
||||
},
|
||||
useNullAsDefault: process.env.DATABASE_CLIENT !== 'pg',
|
||||
migrations: {
|
||||
directory: './migrations'
|
||||
},
|
||||
seeds: {
|
||||
directory: './seeds'
|
||||
}
|
||||
},
|
||||
|
||||
production: {
|
||||
client: process.env.DATABASE_CLIENT || 'pg',
|
||||
connection: {
|
||||
host: process.env.DB_HOST || 'db',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
user: process.env.DB_USER || 'picpeak',
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME || 'picpeak',
|
||||
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false
|
||||
},
|
||||
pool: {
|
||||
min: 2,
|
||||
max: 10
|
||||
},
|
||||
migrations: {
|
||||
directory: './migrations'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config[process.env.NODE_ENV || 'development'];
|
||||
@@ -1,25 +0,0 @@
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function addMustChangePasswordColumn() {
|
||||
try {
|
||||
// Check if the column already exists
|
||||
const hasMustChangePassword = await db.schema.hasColumn('admin_users', 'must_change_password');
|
||||
|
||||
if (!hasMustChangePassword) {
|
||||
await db.schema.table('admin_users', (table) => {
|
||||
table.boolean('must_change_password').defaultTo(false);
|
||||
});
|
||||
|
||||
console.log('✅ Added must_change_password column to admin_users table');
|
||||
} else {
|
||||
console.log('ℹ️ must_change_password column already exists');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
addMustChangePasswordColumn();
|
||||
Generated
+140
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.8",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^5.3.1",
|
||||
@@ -29,6 +29,7 @@
|
||||
"multer": "^2.0.1",
|
||||
"node-cron": "^3.0.2",
|
||||
"nodemailer": "^6.9.1",
|
||||
"pg": "^8.16.3",
|
||||
"react-i18next": "^15.6.0",
|
||||
"sharp": "^0.32.0",
|
||||
"sqlite3": "^5.1.6",
|
||||
@@ -6471,12 +6472,101 @@
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.16.3",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
|
||||
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.9.1",
|
||||
"pg-pool": "^3.10.1",
|
||||
"pg-protocol": "^1.10.3",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
|
||||
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz",
|
||||
"integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
|
||||
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
|
||||
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pg/node_modules/pg-connection-string": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
|
||||
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -6575,6 +6665,45 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
@@ -7494,6 +7623,15 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.8",
|
||||
"description": "Backend for PicPeak event photo sharing platform",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
@@ -32,6 +32,7 @@
|
||||
"multer": "^2.0.1",
|
||||
"node-cron": "^3.0.2",
|
||||
"nodemailer": "^6.9.1",
|
||||
"pg": "^8.16.3",
|
||||
"react-i18next": "^15.6.0",
|
||||
"sharp": "^0.32.0",
|
||||
"sqlite3": "^5.1.6",
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Activate enhanced authentication in server.js
|
||||
* This script safely updates the server configuration
|
||||
*/
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
async function activateEnhancedAuth() {
|
||||
console.log('=== Activating Enhanced Authentication ===\n');
|
||||
|
||||
try {
|
||||
const serverPath = path.join(__dirname, '../server.js');
|
||||
|
||||
// Read current server.js
|
||||
let serverContent = await fs.readFile(serverPath, 'utf8');
|
||||
|
||||
// Backup current server.js
|
||||
const backupPath = `${serverPath}.backup.${Date.now()}`;
|
||||
await fs.writeFile(backupPath, serverContent);
|
||||
console.log(`✓ Created backup: ${path.basename(backupPath)}`);
|
||||
|
||||
// Check current state
|
||||
if (serverContent.includes("require('./src/routes/auth-enhanced')")) {
|
||||
console.log('! Enhanced auth already active');
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace auth routes import
|
||||
const originalLine = "const authRoutes = require('./src/routes/auth');";
|
||||
const enhancedLine = "const authRoutes = require('./src/routes/auth-enhanced');";
|
||||
|
||||
if (!serverContent.includes(originalLine)) {
|
||||
console.log('✗ Could not find original auth import line');
|
||||
console.log('Please manually update server.js');
|
||||
return;
|
||||
}
|
||||
|
||||
serverContent = serverContent.replace(originalLine, enhancedLine);
|
||||
console.log('✓ Updated auth routes import');
|
||||
|
||||
// Add cleanup job initialization after database init
|
||||
const dbInitLine = 'initializeDatabase()';
|
||||
const cleanupAddition = `
|
||||
// Initialize auth security cleanup job
|
||||
const { initializeCleanupJob } = require('./src/utils/authSecurity');
|
||||
initializeCleanupJob();
|
||||
`;
|
||||
|
||||
if (!serverContent.includes('initializeCleanupJob')) {
|
||||
const dbInitIndex = serverContent.indexOf(dbInitLine);
|
||||
if (dbInitIndex !== -1) {
|
||||
const insertPoint = serverContent.indexOf('\n', dbInitIndex) + 1;
|
||||
serverContent = serverContent.slice(0, insertPoint) + cleanupAddition + serverContent.slice(insertPoint);
|
||||
console.log('✓ Added cleanup job initialization');
|
||||
}
|
||||
}
|
||||
|
||||
// Write updated server.js
|
||||
await fs.writeFile(serverPath, serverContent);
|
||||
console.log('✓ Updated server.js');
|
||||
|
||||
console.log('\n✅ Enhanced authentication activated!');
|
||||
console.log('\nNext steps:');
|
||||
console.log('1. Restart the backend:');
|
||||
console.log(' docker-compose restart backend');
|
||||
console.log('\n2. Monitor auth health:');
|
||||
console.log(' node scripts/monitor-auth-health.js');
|
||||
console.log('\n3. To rollback if needed:');
|
||||
console.log(` cp ${path.basename(backupPath)} server.js`);
|
||||
console.log(' docker-compose restart backend');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error activating enhanced auth:', error);
|
||||
}
|
||||
}
|
||||
|
||||
activateEnhancedAuth();
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Add authentication security tables to existing database
|
||||
*/
|
||||
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function addAuthTables() {
|
||||
console.log('Adding authentication security tables...\n');
|
||||
|
||||
try {
|
||||
// 1. Create login_attempts table
|
||||
const hasLoginAttempts = await db.schema.hasTable('login_attempts');
|
||||
if (!hasLoginAttempts) {
|
||||
await db.schema.createTable('login_attempts', table => {
|
||||
table.increments('id').primary();
|
||||
table.string('identifier').notNullable();
|
||||
table.string('ip_address', 45).notNullable();
|
||||
table.text('user_agent');
|
||||
table.timestamp('attempt_time').defaultTo(db.fn.now());
|
||||
table.boolean('success').defaultTo(false);
|
||||
|
||||
// Indexes for performance
|
||||
table.index('identifier');
|
||||
table.index('attempt_time');
|
||||
table.index(['identifier', 'success', 'attempt_time']);
|
||||
});
|
||||
console.log('✓ Created login_attempts table');
|
||||
} else {
|
||||
console.log('! login_attempts table already exists');
|
||||
}
|
||||
|
||||
// 2. Add columns to admin_users
|
||||
const hasPasswordChangedAt = await db.schema.hasColumn('admin_users', 'password_changed_at');
|
||||
if (!hasPasswordChangedAt) {
|
||||
await db.schema.table('admin_users', table => {
|
||||
table.timestamp('password_changed_at').nullable();
|
||||
});
|
||||
console.log('✓ Added password_changed_at column');
|
||||
}
|
||||
|
||||
const hasLastLoginIp = await db.schema.hasColumn('admin_users', 'last_login_ip');
|
||||
if (!hasLastLoginIp) {
|
||||
await db.schema.table('admin_users', table => {
|
||||
table.string('last_login_ip', 45).nullable();
|
||||
});
|
||||
console.log('✓ Added last_login_ip column');
|
||||
}
|
||||
|
||||
const hasTwoFactorEnabled = await db.schema.hasColumn('admin_users', 'two_factor_enabled');
|
||||
if (!hasTwoFactorEnabled) {
|
||||
await db.schema.table('admin_users', table => {
|
||||
table.boolean('two_factor_enabled').defaultTo(false);
|
||||
});
|
||||
console.log('✓ Added two_factor_enabled column');
|
||||
}
|
||||
|
||||
const hasTwoFactorSecret = await db.schema.hasColumn('admin_users', 'two_factor_secret');
|
||||
if (!hasTwoFactorSecret) {
|
||||
await db.schema.table('admin_users', table => {
|
||||
table.string('two_factor_secret').nullable();
|
||||
});
|
||||
console.log('✓ Added two_factor_secret column');
|
||||
}
|
||||
|
||||
// 3. Verify everything
|
||||
console.log('\nVerifying tables...');
|
||||
|
||||
const loginAttemptsInfo = await db('login_attempts').columnInfo();
|
||||
console.log('✓ login_attempts columns:', Object.keys(loginAttemptsInfo).join(', '));
|
||||
|
||||
const adminUsersInfo = await db('admin_users').columnInfo();
|
||||
const securityColumns = ['password_changed_at', 'last_login_ip', 'two_factor_enabled', 'two_factor_secret'];
|
||||
const hasAllColumns = securityColumns.every(col => adminUsersInfo[col]);
|
||||
|
||||
if (hasAllColumns) {
|
||||
console.log('✓ All security columns present in admin_users');
|
||||
} else {
|
||||
console.log('✗ Some security columns missing from admin_users');
|
||||
}
|
||||
|
||||
console.log('\n✅ Authentication security tables ready!');
|
||||
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('\n❌ Error adding auth tables:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
addAuthTables();
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Add token revocation tables to existing database
|
||||
*/
|
||||
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function addTokenRevocationTables() {
|
||||
console.log('Adding token revocation tables...\n');
|
||||
|
||||
try {
|
||||
// 1. Create revoked_tokens table
|
||||
const hasRevokedTokens = await db.schema.hasTable('revoked_tokens');
|
||||
if (!hasRevokedTokens) {
|
||||
await db.schema.createTable('revoked_tokens', table => {
|
||||
table.increments('id').primary();
|
||||
table.string('token_id').notNullable().unique();
|
||||
table.integer('user_id').nullable();
|
||||
table.string('token_type', 20);
|
||||
table.timestamp('revoked_at').defaultTo(db.fn.now());
|
||||
table.timestamp('expires_at').notNullable();
|
||||
table.string('reason', 100);
|
||||
table.text('metadata');
|
||||
|
||||
// Indexes
|
||||
table.index('token_id');
|
||||
table.index('user_id');
|
||||
table.index('expires_at');
|
||||
});
|
||||
console.log('✓ Created revoked_tokens table');
|
||||
} else {
|
||||
console.log('! revoked_tokens table already exists');
|
||||
}
|
||||
|
||||
// 2. Create user_token_revocations table
|
||||
const hasUserRevocations = await db.schema.hasTable('user_token_revocations');
|
||||
if (!hasUserRevocations) {
|
||||
await db.schema.createTable('user_token_revocations', table => {
|
||||
table.integer('user_id').primary();
|
||||
table.timestamp('revoked_at').notNullable();
|
||||
table.string('reason', 100);
|
||||
|
||||
table.index('revoked_at');
|
||||
});
|
||||
console.log('✓ Created user_token_revocations table');
|
||||
} else {
|
||||
console.log('! user_token_revocations table already exists');
|
||||
}
|
||||
|
||||
// 3. Verify tables
|
||||
console.log('\nVerifying tables...');
|
||||
|
||||
const revokedTokensInfo = await db('revoked_tokens').columnInfo();
|
||||
console.log('✓ revoked_tokens columns:', Object.keys(revokedTokensInfo).join(', '));
|
||||
|
||||
const userRevocationsInfo = await db('user_token_revocations').columnInfo();
|
||||
console.log('✓ user_token_revocations columns:', Object.keys(userRevocationsInfo).join(', '));
|
||||
|
||||
console.log('\n✅ Token revocation tables ready!');
|
||||
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('\n❌ Error adding token revocation tables:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
addTokenRevocationTables();
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Check Docker deployment status for security fixes
|
||||
*/
|
||||
|
||||
console.log('=== Docker Deployment Status Check ===\n');
|
||||
|
||||
// Color codes
|
||||
const GREEN = '\x1b[32m';
|
||||
const RED = '\x1b[31m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
let allGood = true;
|
||||
|
||||
// Check SQL Security
|
||||
console.log('1. SQL Injection Fixes:');
|
||||
try {
|
||||
const { sanitizeDays, escapeLikePattern } = require('../src/utils/sqlSecurity');
|
||||
console.log(`${GREEN}✓${RESET} sqlSecurity.js exists`);
|
||||
console.log(`${GREEN}✓${RESET} Security functions available`);
|
||||
} catch (e) {
|
||||
console.log(`${RED}✗${RESET} sqlSecurity.js missing`);
|
||||
allGood = false;
|
||||
}
|
||||
|
||||
// Check Auth Security
|
||||
console.log('\n2. Authentication Security:');
|
||||
try {
|
||||
const authSec = require('../src/utils/authSecurity');
|
||||
console.log(`${GREEN}✓${RESET} authSecurity.js exists`);
|
||||
|
||||
const authEnhanced = require('../src/middleware/auth-enhanced');
|
||||
console.log(`${GREEN}✓${RESET} auth-enhanced middleware exists`);
|
||||
|
||||
const authRoutes = require('../src/routes/auth-enhanced');
|
||||
console.log(`${GREEN}✓${RESET} auth-enhanced routes exist`);
|
||||
} catch (e) {
|
||||
console.log(`${YELLOW}!${RESET} Auth security files exist but not active`);
|
||||
}
|
||||
|
||||
// Check Database
|
||||
console.log('\n3. Database Status:');
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function checkDatabase() {
|
||||
try {
|
||||
// Check login_attempts table
|
||||
await db('login_attempts').count();
|
||||
console.log(`${GREEN}✓${RESET} login_attempts table exists`);
|
||||
} catch (e) {
|
||||
console.log(`${YELLOW}!${RESET} login_attempts table not created (run migrations)`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check admin_users columns
|
||||
await db('admin_users').select('password_changed_at').limit(1);
|
||||
console.log(`${GREEN}✓${RESET} Auth security columns exist`);
|
||||
} catch (e) {
|
||||
console.log(`${YELLOW}!${RESET} Auth security columns missing (run migrations)`);
|
||||
}
|
||||
|
||||
// Close database connection
|
||||
await db.destroy();
|
||||
}
|
||||
|
||||
// Check server configuration
|
||||
console.log('\n4. Server Configuration:');
|
||||
const fs = require('fs');
|
||||
const serverContent = fs.readFileSync('./server.js', 'utf8');
|
||||
|
||||
if (serverContent.includes("require('./src/routes/auth-enhanced')")) {
|
||||
console.log(`${GREEN}✓${RESET} Using enhanced auth routes`);
|
||||
} else if (serverContent.includes("require('./src/routes/auth')")) {
|
||||
console.log(`${YELLOW}!${RESET} Using original auth routes (enhanced not active)`);
|
||||
}
|
||||
|
||||
if (serverContent.includes('initializeCleanupJob')) {
|
||||
console.log(`${GREEN}✓${RESET} Auth cleanup job initialized`);
|
||||
} else {
|
||||
console.log(`${YELLOW}!${RESET} Auth cleanup job not initialized`);
|
||||
}
|
||||
|
||||
// Run async checks
|
||||
checkDatabase().then(() => {
|
||||
console.log('\n=== Summary ===');
|
||||
if (allGood) {
|
||||
console.log(`${GREEN}All security fixes are deployed!${RESET}`);
|
||||
} else {
|
||||
console.log(`${YELLOW}Some security features need activation:${RESET}`);
|
||||
console.log('1. Run migrations: npx knex migrate:latest');
|
||||
console.log('2. Update server.js to use auth-enhanced routes');
|
||||
console.log('3. Restart the container');
|
||||
}
|
||||
});
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script to create an admin user
|
||||
* Usage: node scripts/create-admin.js --email admin@example.com --username admin --password yourpassword
|
||||
*
|
||||
* If no password is provided, a random one will be generated and displayed
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { db } = require('../src/database/db');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const getArg = (name) => {
|
||||
const index = args.findIndex(arg => arg === `--${name}`);
|
||||
return index !== -1 && args[index + 1] ? args[index + 1] : null;
|
||||
};
|
||||
|
||||
const email = getArg('email');
|
||||
const username = getArg('username') || email?.split('@')[0] || 'admin';
|
||||
let password = getArg('password');
|
||||
|
||||
// Validate email
|
||||
if (!email) {
|
||||
console.error('Error: Email is required. Use --email admin@example.com');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate password if not provided
|
||||
if (!password) {
|
||||
password = crypto.randomBytes(12).toString('base64').slice(0, 16);
|
||||
console.log(`Generated password: ${password}`);
|
||||
console.log('Please save this password securely!');
|
||||
}
|
||||
|
||||
async function createAdmin() {
|
||||
try {
|
||||
// Check if user already exists
|
||||
const existingUser = await db('admin_users')
|
||||
.where('email', email)
|
||||
.orWhere('username', username)
|
||||
.first();
|
||||
|
||||
if (existingUser) {
|
||||
console.error(`Error: User with email "${email}" or username "${username}" already exists`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
|
||||
// Create admin user
|
||||
await db('admin_users').insert({
|
||||
username,
|
||||
email,
|
||||
password_hash: passwordHash,
|
||||
is_active: true,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
console.log(`✅ Admin user created successfully!`);
|
||||
console.log(` Email: ${email}`);
|
||||
console.log(` Username: ${username}`);
|
||||
console.log(` Login URL: ${process.env.ADMIN_URL || 'http://localhost:3000'}/admin/login`);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error creating admin user:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
createAdmin();
|
||||
@@ -1,56 +0,0 @@
|
||||
const knex = require('knex')({
|
||||
client: 'sqlite3',
|
||||
connection: { filename: '/app/data/photo_sharing.db' },
|
||||
useNullAsDefault: true
|
||||
});
|
||||
|
||||
async function debugEventPhotos() {
|
||||
try {
|
||||
// Get all photos for event 12
|
||||
const photos = await knex('photos')
|
||||
.where('event_id', 12)
|
||||
.select('id', 'filename', 'path', 'thumbnail_path')
|
||||
.orderBy('id');
|
||||
|
||||
console.log('Total photos for event 12:', photos.length);
|
||||
console.log('\nSample photos:');
|
||||
|
||||
// Show first few and specific IDs that were failing
|
||||
const sampleIds = [1686, 1687, 1688, 1689, 1715, 1717, 1718, 1719];
|
||||
const samples = photos.filter(p => sampleIds.includes(p.id));
|
||||
|
||||
samples.forEach(p => {
|
||||
console.log(`\nID ${p.id}: ${p.filename}`);
|
||||
console.log(` Path: ${p.path}`);
|
||||
console.log(` Thumbnail: ${p.thumbnail_path}`);
|
||||
});
|
||||
|
||||
// Check for any photos without thumbnails
|
||||
const noThumbs = photos.filter(p => !p.thumbnail_path);
|
||||
if (noThumbs.length > 0) {
|
||||
console.log(`\nPhotos without thumbnails: ${noThumbs.length}`);
|
||||
noThumbs.forEach(p => console.log(` ID ${p.id}: ${p.filename}`));
|
||||
}
|
||||
|
||||
// Check file existence for failing photos
|
||||
const fs = require('fs').promises;
|
||||
console.log('\nChecking file existence for samples:');
|
||||
|
||||
for (const photo of samples) {
|
||||
const thumbPath = `/app/storage/${photo.thumbnail_path}`;
|
||||
try {
|
||||
await fs.access(thumbPath);
|
||||
console.log(`✓ ID ${photo.id}: Thumbnail exists at ${thumbPath}`);
|
||||
} catch (err) {
|
||||
console.log(`✗ ID ${photo.id}: Thumbnail NOT FOUND at ${thumbPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
knex.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
debugEventPhotos();
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Connect to the database
|
||||
const dbPath = '/app/data/photo_sharing.db';
|
||||
console.log(`Connecting to database at: ${dbPath}`);
|
||||
|
||||
const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
|
||||
if (err) {
|
||||
console.error('Error opening database:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Connected to the SQLite database.\n');
|
||||
});
|
||||
|
||||
// Query for photos with IDs 1688 and 1689 where event_id = 12
|
||||
const query = `
|
||||
SELECT p.id, p.filename, p.path, p.thumbnail_path, p.event_id,
|
||||
e.slug as event_slug, e.is_active, e.is_archived
|
||||
FROM photos p
|
||||
JOIN events e ON p.event_id = e.id
|
||||
WHERE p.id IN (1688, 1689) AND p.event_id = 12
|
||||
`;
|
||||
|
||||
console.log('Executing query to get photo details with event information...\n');
|
||||
|
||||
db.all(query, [], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error executing query:', err.message);
|
||||
db.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Found ${rows.length} photo(s):\n`);
|
||||
|
||||
if (rows.length === 0) {
|
||||
console.log('No photos found matching the criteria.');
|
||||
} else {
|
||||
rows.forEach((row) => {
|
||||
console.log('=== Photo ID:', row.id, '===');
|
||||
console.log('Filename:', row.filename);
|
||||
console.log('DB Path:', row.path);
|
||||
console.log('DB Thumbnail Path:', row.thumbnail_path);
|
||||
console.log('Event ID:', row.event_id);
|
||||
console.log('Event Slug:', row.event_slug);
|
||||
console.log('Event is_active:', row.is_active);
|
||||
console.log('Event is_archived:', row.is_archived);
|
||||
|
||||
// Check file existence
|
||||
const storageBase = '/app/storage';
|
||||
const eventStatusDir = row.is_active ? 'active' : 'archived';
|
||||
|
||||
// Check full image path
|
||||
const fullImagePath1 = path.join(storageBase, row.path);
|
||||
const fullImagePath2 = path.join(storageBase, 'events', eventStatusDir, row.path);
|
||||
|
||||
console.log('\nChecking full image paths:');
|
||||
console.log(` Path 1: ${fullImagePath1} - ${fs.existsSync(fullImagePath1) ? 'EXISTS' : 'NOT FOUND'}`);
|
||||
console.log(` Path 2: ${fullImagePath2} - ${fs.existsSync(fullImagePath2) ? 'EXISTS' : 'NOT FOUND'}`);
|
||||
|
||||
// Check thumbnail path
|
||||
const thumbnailPath = path.join(storageBase, row.thumbnail_path);
|
||||
console.log('\nChecking thumbnail path:');
|
||||
console.log(` ${thumbnailPath} - ${fs.existsSync(thumbnailPath) ? 'EXISTS' : 'NOT FOUND'}`);
|
||||
|
||||
console.log('\n---\n');
|
||||
});
|
||||
}
|
||||
|
||||
// Close the database connection
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing database:', err.message);
|
||||
} else {
|
||||
console.log('Database connection closed.');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
// Connect to the database
|
||||
const dbPath = '/app/data/photo_sharing.db';
|
||||
console.log(`Connecting to database at: ${dbPath}`);
|
||||
|
||||
const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
|
||||
if (err) {
|
||||
console.error('Error opening database:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Connected to the SQLite database.');
|
||||
});
|
||||
|
||||
// Query for photos with IDs 1688 and 1689 where event_id = 12
|
||||
const query = `
|
||||
SELECT id, filename, path, thumbnail_path
|
||||
FROM photos
|
||||
WHERE id IN (1688, 1689) AND event_id = 12
|
||||
`;
|
||||
|
||||
console.log('\nExecuting query:', query);
|
||||
|
||||
db.all(query, [], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error executing query:', err.message);
|
||||
db.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\nFound ${rows.length} photo(s):\n`);
|
||||
|
||||
if (rows.length === 0) {
|
||||
console.log('No photos found matching the criteria.');
|
||||
} else {
|
||||
rows.forEach((row) => {
|
||||
console.log('Photo ID:', row.id);
|
||||
console.log('Filename:', row.filename);
|
||||
console.log('Path:', row.path);
|
||||
console.log('Thumbnail Path:', row.thumbnail_path);
|
||||
console.log('---');
|
||||
});
|
||||
}
|
||||
|
||||
// Close the database connection
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing database:', err.message);
|
||||
} else {
|
||||
console.log('\nDatabase connection closed.');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,132 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Authentication Security Enhancement Deployment Script
|
||||
# This script helps safely deploy auth security enhancements
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== PicPeak Authentication Security Deployment ==="
|
||||
echo ""
|
||||
|
||||
# Color codes
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Check if we're in the backend directory
|
||||
if [ ! -f "package.json" ] || [ ! -d "src" ]; then
|
||||
echo -e "${RED}Error: Must run from backend directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to prompt for confirmation
|
||||
confirm() {
|
||||
read -p "$1 (y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}Deployment cancelled${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "This script will help deploy authentication security enhancements"
|
||||
echo ""
|
||||
echo "Current deployment phase options:"
|
||||
echo "1. Run database migrations only (safe)"
|
||||
echo "2. Test enhanced auth endpoints"
|
||||
echo "3. Switch to enhanced auth (full deployment)"
|
||||
echo "4. Rollback to original auth"
|
||||
echo ""
|
||||
|
||||
read -p "Select phase (1-4): " PHASE
|
||||
|
||||
case $PHASE in
|
||||
1)
|
||||
echo -e "${GREEN}Phase 1: Running database migrations${NC}"
|
||||
confirm "Run migrations?"
|
||||
|
||||
echo "Creating backup..."
|
||||
cp database.db database.db.backup.$(date +%Y%m%d_%H%M%S) 2>/dev/null || true
|
||||
|
||||
echo "Running migrations..."
|
||||
npx knex migrate:latest
|
||||
|
||||
echo -e "${GREEN}✓ Migrations completed${NC}"
|
||||
echo "New tables added: login_attempts"
|
||||
echo "New columns added to admin_users: password_changed_at, last_login_ip"
|
||||
;;
|
||||
|
||||
2)
|
||||
echo -e "${GREEN}Phase 2: Testing enhanced auth${NC}"
|
||||
|
||||
# Check if server is running
|
||||
if ! curl -s http://localhost:3001/health > /dev/null; then
|
||||
echo -e "${RED}Server not running on port 3001${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running auth security tests..."
|
||||
node scripts/test-auth-security.js
|
||||
|
||||
echo ""
|
||||
echo "Test endpoints manually:"
|
||||
echo "- Login: POST /api/auth/admin/login"
|
||||
echo "- Logout: POST /api/auth/logout"
|
||||
echo "- Session: GET /api/auth/session"
|
||||
;;
|
||||
|
||||
3)
|
||||
echo -e "${YELLOW}Phase 3: Full deployment${NC}"
|
||||
echo "This will switch to enhanced authentication"
|
||||
confirm "Deploy enhanced auth?"
|
||||
|
||||
# Check if migrations are run
|
||||
if ! npx knex migrate:status | grep -q "015_add_login_attempts_table"; then
|
||||
echo -e "${RED}Error: Migrations not run. Run phase 1 first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Updating server.js to use enhanced auth..."
|
||||
# This is where you'd update the imports
|
||||
# For safety, we'll just show what needs to be done
|
||||
|
||||
echo -e "${YELLOW}Manual steps required:${NC}"
|
||||
echo "1. Edit server.js"
|
||||
echo "2. Change: const authRoutes = require('./src/routes/auth');"
|
||||
echo " To: const authRoutes = require('./src/routes/auth-enhanced');"
|
||||
echo "3. Restart the application"
|
||||
echo ""
|
||||
echo "After restart, the enhanced auth will be active with:"
|
||||
echo "- Account lockout protection"
|
||||
echo "- Login attempt tracking"
|
||||
echo "- Enhanced security logging"
|
||||
;;
|
||||
|
||||
4)
|
||||
echo -e "${RED}Phase 4: Rollback${NC}"
|
||||
confirm "Rollback auth changes?"
|
||||
|
||||
echo "Rolling back to original auth..."
|
||||
echo ""
|
||||
echo -e "${YELLOW}Manual steps required:${NC}"
|
||||
echo "1. Edit server.js"
|
||||
echo "2. Change: const authRoutes = require('./src/routes/auth-enhanced');"
|
||||
echo " To: const authRoutes = require('./src/routes/auth');"
|
||||
echo "3. Restart the application"
|
||||
echo ""
|
||||
echo "Optional: Clear lockouts"
|
||||
echo "sqlite3 database.db \"DELETE FROM login_attempts WHERE success = 0\""
|
||||
;;
|
||||
|
||||
*)
|
||||
echo -e "${RED}Invalid option${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Done!${NC}"
|
||||
echo ""
|
||||
echo "Monitor logs after any changes:"
|
||||
echo "docker-compose logs -f backend | grep -i auth"
|
||||
@@ -1,37 +0,0 @@
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function fixDefaultThemes() {
|
||||
try {
|
||||
console.log('Fixing events with "default" theme...');
|
||||
|
||||
// Find all events with "default" as color_theme
|
||||
const eventsToFix = await db('events')
|
||||
.where('color_theme', 'default')
|
||||
.select('id', 'event_name');
|
||||
|
||||
console.log(`Found ${eventsToFix.length} events to fix`);
|
||||
|
||||
if (eventsToFix.length > 0) {
|
||||
// Update them to null so they use the global theme
|
||||
await db('events')
|
||||
.where('color_theme', 'default')
|
||||
.update({ color_theme: null });
|
||||
|
||||
console.log('Updated events to use global theme');
|
||||
|
||||
eventsToFix.forEach(event => {
|
||||
console.log(`- Fixed event: ${event.event_name} (ID: ${event.id})`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Theme fix completed successfully');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error fixing themes:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fixDefaultThemes();
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Monitor authentication health after deployment
|
||||
* Run this after activating enhanced auth to watch for issues
|
||||
*/
|
||||
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
console.log('=== Authentication Health Monitor ===\n');
|
||||
console.log('Monitoring auth system... (Press Ctrl+C to stop)\n');
|
||||
|
||||
// Color codes
|
||||
const GREEN = '\x1b[32m';
|
||||
const RED = '\x1b[31m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
let previousStats = {
|
||||
totalAttempts: 0,
|
||||
failedAttempts: 0,
|
||||
lockedAccounts: 0
|
||||
};
|
||||
|
||||
async function getAuthStats() {
|
||||
try {
|
||||
const stats = {};
|
||||
|
||||
// Total login attempts in last hour
|
||||
const totalAttempts = await db('login_attempts')
|
||||
.where('attempt_time', '>', new Date(Date.now() - 60 * 60 * 1000).toISOString())
|
||||
.count('id as count')
|
||||
.first();
|
||||
stats.totalAttempts = totalAttempts.count || 0;
|
||||
|
||||
// Failed attempts in last hour
|
||||
const failedAttempts = await db('login_attempts')
|
||||
.where('attempt_time', '>', new Date(Date.now() - 60 * 60 * 1000).toISOString())
|
||||
.where('success', false)
|
||||
.count('id as count')
|
||||
.first();
|
||||
stats.failedAttempts = failedAttempts.count || 0;
|
||||
|
||||
// Currently locked accounts
|
||||
const recentWindow = new Date(Date.now() - 15 * 60 * 1000);
|
||||
const lockedAccounts = await db('login_attempts')
|
||||
.select('identifier')
|
||||
.where('success', false)
|
||||
.where('attempt_time', '>=', recentWindow.toISOString())
|
||||
.groupBy('identifier')
|
||||
.havingRaw('COUNT(*) >= 5');
|
||||
stats.lockedAccounts = lockedAccounts.length;
|
||||
|
||||
// Success rate
|
||||
stats.successRate = stats.totalAttempts > 0
|
||||
? ((stats.totalAttempts - stats.failedAttempts) / stats.totalAttempts * 100).toFixed(1)
|
||||
: 100;
|
||||
|
||||
// Recent failures (last 5 minutes)
|
||||
const recentFailures = await db('login_attempts')
|
||||
.where('success', false)
|
||||
.where('attempt_time', '>', new Date(Date.now() - 5 * 60 * 1000).toISOString())
|
||||
.orderBy('attempt_time', 'desc')
|
||||
.limit(5)
|
||||
.select('identifier', 'ip_address', 'attempt_time');
|
||||
stats.recentFailures = recentFailures;
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function displayStats() {
|
||||
const stats = await getAuthStats();
|
||||
|
||||
if (stats.error) {
|
||||
console.log(`${RED}Error: ${stats.error}${RESET}`);
|
||||
console.log('Enhanced auth might not be active yet.\n');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear console for clean display
|
||||
console.clear();
|
||||
console.log('=== Authentication Health Monitor ===\n');
|
||||
console.log(new Date().toLocaleString());
|
||||
console.log('─'.repeat(50));
|
||||
|
||||
// Display metrics
|
||||
console.log(`\n📊 Last Hour Statistics:`);
|
||||
console.log(` Total Login Attempts: ${stats.totalAttempts}`);
|
||||
console.log(` Failed Attempts: ${stats.failedAttempts}`);
|
||||
console.log(` Success Rate: ${stats.successRate}%`);
|
||||
console.log(` Currently Locked: ${stats.lockedAccounts} accounts`);
|
||||
|
||||
// Alerts
|
||||
if (stats.failedAttempts > previousStats.failedAttempts + 10) {
|
||||
console.log(`\n${RED}⚠️ ALERT: Spike in failed login attempts!${RESET}`);
|
||||
}
|
||||
|
||||
if (stats.lockedAccounts > 5) {
|
||||
console.log(`\n${YELLOW}⚠️ WARNING: Multiple accounts locked (${stats.lockedAccounts})${RESET}`);
|
||||
}
|
||||
|
||||
if (stats.successRate < 50) {
|
||||
console.log(`\n${RED}⚠️ ALERT: Low success rate (${stats.successRate}%)${RESET}`);
|
||||
}
|
||||
|
||||
// Recent failures
|
||||
if (stats.recentFailures && stats.recentFailures.length > 0) {
|
||||
console.log(`\n📋 Recent Failed Attempts (last 5 min):`);
|
||||
stats.recentFailures.forEach(failure => {
|
||||
const time = new Date(failure.attempt_time).toLocaleTimeString();
|
||||
console.log(` ${time} - ${failure.identifier} from ${failure.ip_address}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Health status
|
||||
console.log(`\n✅ Status: ${stats.failedAttempts === 0 ? 'Healthy' : 'Active'}`);
|
||||
console.log('\nPress Ctrl+C to stop monitoring\n');
|
||||
|
||||
previousStats = stats;
|
||||
}
|
||||
|
||||
// Monitor every 10 seconds
|
||||
setInterval(displayStats, 10000);
|
||||
|
||||
// Initial display
|
||||
displayStats();
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\nStopping monitor...');
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -1,269 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Safe Authentication Deployment Script
|
||||
* Carefully activates auth security with multiple safety checks
|
||||
*/
|
||||
|
||||
const { db } = require('../src/database/db');
|
||||
const logger = require('../src/utils/logger');
|
||||
|
||||
console.log('=== Safe Authentication Security Deployment ===\n');
|
||||
|
||||
// Color codes
|
||||
const GREEN = '\x1b[32m';
|
||||
const RED = '\x1b[31m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const BLUE = '\x1b[34m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
async function runSafetyChecks() {
|
||||
console.log(`${BLUE}Running pre-deployment safety checks...${RESET}\n`);
|
||||
|
||||
const checks = {
|
||||
databaseConnected: false,
|
||||
adminUsersExist: false,
|
||||
activeSessionsExist: false,
|
||||
migrationsReady: true,
|
||||
diskSpace: true
|
||||
};
|
||||
|
||||
try {
|
||||
// Check 1: Database connection
|
||||
await db.raw('SELECT 1');
|
||||
checks.databaseConnected = true;
|
||||
console.log(`${GREEN}✓${RESET} Database connection healthy`);
|
||||
} catch (e) {
|
||||
console.log(`${RED}✗${RESET} Database connection failed`);
|
||||
return checks;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check 2: Admin users exist
|
||||
const adminCount = await db('admin_users').count('id as count').first();
|
||||
checks.adminUsersExist = adminCount.count > 0;
|
||||
console.log(`${GREEN}✓${RESET} Found ${adminCount.count} admin users`);
|
||||
} catch (e) {
|
||||
console.log(`${RED}✗${RESET} Could not check admin users`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check 3: Check for active sessions (optional warning)
|
||||
const recentLogins = await db('access_logs')
|
||||
.where('action', 'login_success')
|
||||
.where('timestamp', '>', new Date(Date.now() - 60 * 60 * 1000).toISOString())
|
||||
.count('id as count')
|
||||
.first();
|
||||
|
||||
if (recentLogins.count > 0) {
|
||||
checks.activeSessionsExist = true;
|
||||
console.log(`${YELLOW}!${RESET} Warning: ${recentLogins.count} active sessions in last hour`);
|
||||
} else {
|
||||
console.log(`${GREEN}✓${RESET} No recent active sessions`);
|
||||
}
|
||||
} catch (e) {
|
||||
// Table might not exist yet, that's ok
|
||||
console.log(`${GREEN}✓${RESET} No access logs table yet (expected)`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check 4: Check if migrations would conflict
|
||||
const tables = await db.raw(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name IN ('login_attempts')
|
||||
`);
|
||||
|
||||
if (tables.length > 0) {
|
||||
console.log(`${YELLOW}!${RESET} login_attempts table already exists`);
|
||||
checks.migrationsReady = false;
|
||||
} else {
|
||||
console.log(`${GREEN}✓${RESET} Ready to create login_attempts table`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`${RED}✗${RESET} Could not check existing tables`);
|
||||
checks.migrationsReady = false;
|
||||
}
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
async function backupDatabase() {
|
||||
console.log(`\n${BLUE}Creating database backup...${RESET}`);
|
||||
|
||||
try {
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = process.env.DB_PATH || './data/database.db';
|
||||
const backupPath = `${dbPath}.backup.${Date.now()}`;
|
||||
|
||||
await fs.copyFile(dbPath, backupPath);
|
||||
console.log(`${GREEN}✓${RESET} Database backed up to: ${path.basename(backupPath)}`);
|
||||
return backupPath;
|
||||
} catch (e) {
|
||||
console.log(`${YELLOW}!${RESET} Could not create backup: ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function runMigrations() {
|
||||
console.log(`\n${BLUE}Running database migrations...${RESET}`);
|
||||
|
||||
try {
|
||||
// Run migrations
|
||||
const knex = db;
|
||||
await knex.migrate.latest();
|
||||
|
||||
console.log(`${GREEN}✓${RESET} Migrations completed successfully`);
|
||||
|
||||
// Verify tables exist
|
||||
const loginAttempts = await db('login_attempts').count().first();
|
||||
console.log(`${GREEN}✓${RESET} login_attempts table created`);
|
||||
|
||||
const adminColumns = await db('admin_users').columnInfo();
|
||||
if (adminColumns.password_changed_at) {
|
||||
console.log(`${GREEN}✓${RESET} Security columns added to admin_users`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(`${RED}✗${RESET} Migration failed: ${e.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testEnhancedAuth() {
|
||||
console.log(`\n${BLUE}Testing enhanced authentication (without activating)...${RESET}`);
|
||||
|
||||
try {
|
||||
// Test that enhanced modules load correctly
|
||||
const authSecurity = require('../src/utils/authSecurity');
|
||||
const authEnhanced = require('../src/middleware/auth-enhanced');
|
||||
const authRoutes = require('../src/routes/auth-enhanced');
|
||||
|
||||
console.log(`${GREEN}✓${RESET} Enhanced auth modules load correctly`);
|
||||
|
||||
// Test lockout logic (without real data)
|
||||
const lockoutStatus = await authSecurity.checkAccountLockout('test-user-that-doesnt-exist');
|
||||
console.log(`${GREEN}✓${RESET} Account lockout check works: ${lockoutStatus.isLocked ? 'locked' : 'not locked'}`);
|
||||
|
||||
// Test generic error
|
||||
const error = authSecurity.getGenericAuthError();
|
||||
console.log(`${GREEN}✓${RESET} Generic error message: "${error}"`);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(`${RED}✗${RESET} Enhanced auth test failed: ${e.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createDeploymentInstructions() {
|
||||
console.log(`\n${BLUE}Deployment Instructions:${RESET}\n`);
|
||||
|
||||
const instructions = `
|
||||
${GREEN}Step 1: Update server.js${RESET}
|
||||
Change:
|
||||
${YELLOW}const authRoutes = require('./src/routes/auth');${RESET}
|
||||
To:
|
||||
${GREEN}const authRoutes = require('./src/routes/auth-enhanced');${RESET}
|
||||
|
||||
Add after database initialization:
|
||||
${GREEN}const { initializeCleanupJob } = require('./src/utils/authSecurity');
|
||||
initializeCleanupJob();${RESET}
|
||||
|
||||
${GREEN}Step 2: Update middleware imports (if needed)${RESET}
|
||||
In files using adminAuth, change:
|
||||
${YELLOW}const { adminAuth } = require('../middleware/auth');${RESET}
|
||||
To:
|
||||
${GREEN}const { adminAuth } = require('../middleware/auth-enhanced');${RESET}
|
||||
|
||||
${GREEN}Step 3: Restart the application${RESET}
|
||||
${BLUE}docker-compose restart backend${RESET}
|
||||
or
|
||||
${BLUE}pm2 restart picpeak-backend${RESET}
|
||||
|
||||
${GREEN}Step 4: Monitor logs${RESET}
|
||||
${BLUE}docker-compose logs -f backend | grep -i auth${RESET}
|
||||
|
||||
${YELLOW}Rollback if needed:${RESET}
|
||||
Revert server.js changes and restart
|
||||
`;
|
||||
|
||||
console.log(instructions);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Step 1: Run safety checks
|
||||
const checks = await runSafetyChecks();
|
||||
|
||||
if (!checks.databaseConnected) {
|
||||
console.log(`\n${RED}Cannot proceed: Database not connected${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (checks.activeSessionsExist) {
|
||||
console.log(`\n${YELLOW}Warning: There are active user sessions.`);
|
||||
console.log(`Consider deploying during low-traffic period.${RESET}`);
|
||||
}
|
||||
|
||||
// Step 2: Backup database
|
||||
const backupPath = await backupDatabase();
|
||||
|
||||
// Step 3: Run migrations
|
||||
console.log(`\n${YELLOW}Ready to run migrations. This will:`);
|
||||
console.log(`- Create login_attempts table`);
|
||||
console.log(`- Add security columns to admin_users`);
|
||||
console.log(`No existing data will be modified.${RESET}\n`);
|
||||
|
||||
const readline = require('readline').createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
readline.question('Continue with migrations? (y/n): ', async (answer) => {
|
||||
if (answer.toLowerCase() !== 'y') {
|
||||
console.log(`${YELLOW}Deployment cancelled${RESET}`);
|
||||
readline.close();
|
||||
await db.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const migrationSuccess = await runMigrations();
|
||||
|
||||
if (!migrationSuccess) {
|
||||
console.log(`\n${RED}Migrations failed. Database backup available at: ${backupPath}${RESET}`);
|
||||
readline.close();
|
||||
await db.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 4: Test enhanced auth
|
||||
const authTestSuccess = await testEnhancedAuth();
|
||||
|
||||
if (!authTestSuccess) {
|
||||
console.log(`\n${YELLOW}Enhanced auth tests failed, but migrations succeeded.`);
|
||||
console.log(`Review the errors before activating enhanced auth.${RESET}`);
|
||||
}
|
||||
|
||||
// Step 5: Show deployment instructions
|
||||
await createDeploymentInstructions();
|
||||
|
||||
console.log(`\n${GREEN}✓ Pre-deployment complete!${RESET}`);
|
||||
console.log(`${YELLOW}Enhanced auth is ready but NOT YET ACTIVE.${RESET}`);
|
||||
console.log(`Follow the instructions above to activate when ready.\n`);
|
||||
|
||||
readline.close();
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`${RED}Deployment script error:${RESET}`, error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the deployment
|
||||
main();
|
||||
@@ -1,47 +0,0 @@
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function seedCategories() {
|
||||
try {
|
||||
console.log('Seeding default categories...');
|
||||
|
||||
const defaultCategories = [
|
||||
{ name: 'Portraits', slug: 'portraits' },
|
||||
{ name: 'Group Photos', slug: 'group-photos' },
|
||||
{ name: 'Ceremony', slug: 'ceremony' },
|
||||
{ name: 'Reception', slug: 'reception' },
|
||||
{ name: 'Dancing', slug: 'dancing' },
|
||||
{ name: 'Candids', slug: 'candids' },
|
||||
{ name: 'Details', slug: 'details' },
|
||||
{ name: 'Getting Ready', slug: 'getting-ready' }
|
||||
];
|
||||
|
||||
for (const category of defaultCategories) {
|
||||
// Check if category already exists
|
||||
const existing = await db('photo_categories')
|
||||
.where({ slug: category.slug, is_global: true })
|
||||
.first();
|
||||
|
||||
if (!existing) {
|
||||
await db('photo_categories').insert({
|
||||
name: category.name,
|
||||
slug: category.slug,
|
||||
is_global: true,
|
||||
event_id: null
|
||||
});
|
||||
console.log(`Created category: ${category.name}`);
|
||||
} else {
|
||||
console.log(`Category already exists: ${category.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Default categories seeded successfully');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error seeding categories:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
seedCategories();
|
||||
@@ -1,55 +0,0 @@
|
||||
const axios = require('axios');
|
||||
|
||||
async function testAdminPhotoEndpoint() {
|
||||
try {
|
||||
// First login
|
||||
console.log('1. Logging in as admin...');
|
||||
const loginResponse = await axios.post('http://localhost:3000/api/admin/auth/login', {
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
});
|
||||
|
||||
const token = loginResponse.data.token;
|
||||
console.log('✓ Login successful, got token');
|
||||
|
||||
// Test thumbnail endpoint
|
||||
console.log('\n2. Testing thumbnail endpoint for photo 1688...');
|
||||
try {
|
||||
const thumbResponse = await axios.get('http://localhost:3000/api/admin/events/12/thumbnail/1688', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
responseType: 'arraybuffer'
|
||||
});
|
||||
|
||||
console.log('✓ Thumbnail request successful');
|
||||
console.log(' Response headers:', thumbResponse.headers);
|
||||
console.log(' Data size:', thumbResponse.data.length, 'bytes');
|
||||
} catch (error) {
|
||||
console.error('✗ Thumbnail request failed:', error.response?.status, error.response?.data?.toString());
|
||||
}
|
||||
|
||||
// Test from frontend proxy port
|
||||
console.log('\n3. Testing through nginx proxy (port 3001)...');
|
||||
try {
|
||||
const proxyResponse = await axios.get('http://localhost:3001/api/admin/events/12/thumbnail/1688', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Origin: 'http://localhost:3005'
|
||||
},
|
||||
responseType: 'arraybuffer'
|
||||
});
|
||||
|
||||
console.log('✓ Proxy request successful');
|
||||
console.log(' Response headers:', proxyResponse.headers);
|
||||
console.log(' Data size:', proxyResponse.data.length, 'bytes');
|
||||
} catch (error) {
|
||||
console.error('✗ Proxy request failed:', error.response?.status, error.response?.data?.toString());
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testAdminPhotoEndpoint();
|
||||
@@ -1,146 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test authentication deployment
|
||||
# This script tests the enhanced auth without affecting production
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== Testing Authentication Deployment ==="
|
||||
echo ""
|
||||
|
||||
# Color codes
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
API_URL="http://localhost:3001/api"
|
||||
TEST_USER="admin"
|
||||
TEST_PASS="wrong-password"
|
||||
|
||||
echo -e "${BLUE}This script will test the authentication system${NC}"
|
||||
echo "It will make failed login attempts to test lockout"
|
||||
echo ""
|
||||
|
||||
# Check if server is running
|
||||
echo -e "${BLUE}Checking server status...${NC}"
|
||||
if curl -s -f "$API_URL/../health" > /dev/null; then
|
||||
echo -e "${GREEN}✓ Server is running${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Server not accessible at $API_URL${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to make login attempt
|
||||
make_login_attempt() {
|
||||
local username=$1
|
||||
local password=$2
|
||||
local expected_status=$3
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" -X POST "$API_URL/auth/admin/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\":\"$username\",\"password\":\"$password\"}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | head -n-1)
|
||||
|
||||
if [ "$http_code" -eq "$expected_status" ]; then
|
||||
echo -e "${GREEN}✓${NC} Got expected status $http_code"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗${NC} Expected $expected_status, got $http_code"
|
||||
echo "Response: $body"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 1: Normal failed login
|
||||
echo -e "\n${BLUE}Test 1: Normal failed login${NC}"
|
||||
make_login_attempt "$TEST_USER" "$TEST_PASS" 401
|
||||
|
||||
# Test 2: Multiple failed attempts (testing lockout)
|
||||
echo -e "\n${BLUE}Test 2: Testing account lockout (5 attempts)${NC}"
|
||||
echo "Making 4 more failed attempts..."
|
||||
|
||||
for i in {2..5}; do
|
||||
echo -n "Attempt $i: "
|
||||
make_login_attempt "$TEST_USER" "$TEST_PASS" 401
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Test 3: 6th attempt should be locked
|
||||
echo -e "\n${BLUE}Test 3: 6th attempt (should be locked if enhanced auth active)${NC}"
|
||||
echo -n "Attempt 6: "
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" -X POST "$API_URL/auth/admin/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\":\"$TEST_USER\",\"password\":\"$TEST_PASS\"}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | head -n-1)
|
||||
|
||||
if [ "$http_code" -eq "423" ]; then
|
||||
echo -e "${GREEN}✓ Account locked as expected!${NC}"
|
||||
echo -e "${GREEN}Enhanced auth is ACTIVE${NC}"
|
||||
echo "Lockout message: $(echo $body | jq -r '.error')"
|
||||
ENHANCED_ACTIVE=true
|
||||
elif [ "$http_code" -eq "401" ]; then
|
||||
echo -e "${YELLOW}! Still got 401 - Enhanced auth NOT active${NC}"
|
||||
echo "Original auth is still in use"
|
||||
ENHANCED_ACTIVE=false
|
||||
else
|
||||
echo -e "${RED}✗ Unexpected status: $http_code${NC}"
|
||||
echo "Response: $body"
|
||||
fi
|
||||
|
||||
# Test 4: Check if we can query login attempts
|
||||
echo -e "\n${BLUE}Test 4: Checking login attempts table${NC}"
|
||||
|
||||
if [ "$ENHANCED_ACTIVE" = true ]; then
|
||||
# This would need database access, so we'll check via API behavior
|
||||
echo -e "${GREEN}✓ Login tracking is active${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}! Login tracking not active (migrations might not be run)${NC}"
|
||||
fi
|
||||
|
||||
# Test 5: Test logout endpoint
|
||||
echo -e "\n${BLUE}Test 5: Testing logout endpoint${NC}"
|
||||
|
||||
# First need a valid token (this assumes you have one for testing)
|
||||
# For now, just check if endpoint exists
|
||||
logout_response=$(curl -s -w "\n%{http_code}" -X POST "$API_URL/auth/logout" \
|
||||
-H "Authorization: Bearer invalid-token")
|
||||
|
||||
logout_code=$(echo "$logout_response" | tail -n1)
|
||||
|
||||
if [ "$logout_code" -eq "200" ] || [ "$logout_code" -eq "401" ]; then
|
||||
echo -e "${GREEN}✓ Logout endpoint exists${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}! Logout endpoint might not be active${NC}"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo -e "\n${BLUE}=== Summary ===${NC}"
|
||||
if [ "$ENHANCED_ACTIVE" = true ]; then
|
||||
echo -e "${GREEN}✅ Enhanced authentication is ACTIVE${NC}"
|
||||
echo "- Account lockout protection: Working"
|
||||
echo "- Login attempt tracking: Active"
|
||||
echo "- Enhanced security: Enabled"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Note: Test account might be locked for 30 minutes${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Enhanced authentication is NOT ACTIVE${NC}"
|
||||
echo "- Using original auth system"
|
||||
echo "- No lockout protection"
|
||||
echo "- No login tracking"
|
||||
echo ""
|
||||
echo "To activate:"
|
||||
echo "1. Run migrations: docker exec wedding-photo-sharing-backend-1 npx knex migrate:latest"
|
||||
echo "2. Update server.js to use auth-enhanced routes"
|
||||
echo "3. Restart: docker-compose restart backend"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Test complete!"
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test script to verify authentication security enhancements
|
||||
*/
|
||||
|
||||
console.log('=== Testing Authentication Security Enhancements ===\n');
|
||||
|
||||
const {
|
||||
checkAccountLockout,
|
||||
getGenericAuthError,
|
||||
MAX_LOGIN_ATTEMPTS,
|
||||
LOCKOUT_DURATION
|
||||
} = require('../src/utils/authSecurity');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(description, fn) {
|
||||
try {
|
||||
const result = fn();
|
||||
if (result || result === undefined) {
|
||||
console.log(`✅ ${description}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(`❌ ${description}`);
|
||||
failed++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ ${description} - Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Test generic error message
|
||||
console.log('Testing generic error messages:');
|
||||
test('Generic error prevents user enumeration', () => {
|
||||
const error = getGenericAuthError();
|
||||
return error === 'Invalid credentials';
|
||||
});
|
||||
|
||||
// Test constants
|
||||
console.log('\nTesting security constants:');
|
||||
test('Max login attempts is reasonable', () => MAX_LOGIN_ATTEMPTS === 5);
|
||||
test('Lockout duration is 30 minutes', () => LOCKOUT_DURATION === 30 * 60 * 1000);
|
||||
|
||||
// Test JWT structure
|
||||
console.log('\nTesting JWT token claims:');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const testToken = jwt.sign({
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
type: 'admin',
|
||||
ip: '127.0.0.1',
|
||||
loginTime: Date.now()
|
||||
}, 'test-secret', {
|
||||
expiresIn: '24h',
|
||||
issuer: 'picpeak-auth'
|
||||
});
|
||||
|
||||
const decoded = jwt.verify(testToken, 'test-secret', { complete: true });
|
||||
test('Token has issuer claim', () => decoded.payload.iss === 'picpeak-auth');
|
||||
test('Token has IP claim', () => decoded.payload.ip === '127.0.0.1');
|
||||
test('Token has loginTime claim', () => typeof decoded.payload.loginTime === 'number');
|
||||
test('Token expires in 24 hours', () => {
|
||||
const exp = decoded.payload.exp;
|
||||
const iat = decoded.payload.iat;
|
||||
return (exp - iat) === 24 * 60 * 60;
|
||||
});
|
||||
|
||||
// Test auth middleware logic
|
||||
console.log('\nTesting auth middleware logic:');
|
||||
test('Token type validation works', () => {
|
||||
const adminToken = { type: 'admin' };
|
||||
const galleryToken = { type: 'gallery' };
|
||||
const invalidToken = { type: 'invalid' };
|
||||
|
||||
return adminToken.type === 'admin' &&
|
||||
galleryToken.type === 'gallery' &&
|
||||
invalidToken.type !== 'admin' &&
|
||||
invalidToken.type !== 'gallery';
|
||||
});
|
||||
|
||||
// Test IP validation logic
|
||||
console.log('\nTesting IP validation:');
|
||||
test('IP mismatch is detected', () => {
|
||||
const tokenIp = '192.168.1.100';
|
||||
const currentIp = '10.0.0.50';
|
||||
return tokenIp !== currentIp;
|
||||
});
|
||||
|
||||
// Test password change detection
|
||||
console.log('\nTesting password change detection:');
|
||||
test('Token issued before password change is invalid', () => {
|
||||
const tokenIssuedAt = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago
|
||||
const passwordChangedAt = Math.floor(Date.now() / 1000) - 1800; // 30 minutes ago
|
||||
return tokenIssuedAt < passwordChangedAt;
|
||||
});
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Test Summary ===');
|
||||
console.log(`Total tests: ${passed + failed}`);
|
||||
console.log(`Passed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('\n✅ All authentication security tests passed!');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n❌ Some tests failed. Review the implementation.');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test Authentication V2 Security Fixes
|
||||
*/
|
||||
|
||||
console.log('=== Testing Authentication V2 Fixes ===\n');
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(description, fn) {
|
||||
try {
|
||||
const result = fn();
|
||||
if (result || result === undefined) {
|
||||
console.log(`✅ ${description}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(`❌ ${description}`);
|
||||
failed++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ ${description} - Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
// Test 1: Rate Limiting Security
|
||||
console.log('1. Testing Rate Limiting Security:');
|
||||
const { hasValidAdminToken } = require('../src/utils/rateLimitSecurity');
|
||||
|
||||
// Mock requests
|
||||
const validAdminReq = {
|
||||
path: '/api/admin/events',
|
||||
headers: {
|
||||
authorization: 'Bearer ' + jwt.sign({ id: 1, type: 'admin' }, process.env.JWT_SECRET || 'test')
|
||||
},
|
||||
ip: '127.0.0.1'
|
||||
};
|
||||
|
||||
const invalidTokenReq = {
|
||||
path: '/api/admin/events',
|
||||
headers: {
|
||||
authorization: 'Bearer invalid.token.here'
|
||||
},
|
||||
ip: '127.0.0.1'
|
||||
};
|
||||
|
||||
const galleryTokenReq = {
|
||||
path: '/api/admin/events',
|
||||
headers: {
|
||||
authorization: 'Bearer ' + jwt.sign({ id: 1, type: 'gallery' }, process.env.JWT_SECRET || 'test')
|
||||
},
|
||||
ip: '127.0.0.1'
|
||||
};
|
||||
|
||||
test('Valid admin token skips rate limit', () => hasValidAdminToken(validAdminReq) === true);
|
||||
test('Invalid token applies rate limit', () => hasValidAdminToken(invalidTokenReq) === false);
|
||||
test('Gallery token cannot bypass admin rate limit', () => hasValidAdminToken(galleryTokenReq) === false);
|
||||
|
||||
// Test 2: Password Validation
|
||||
console.log('\n2. Testing Password Validation:');
|
||||
const { validatePassword, validatePasswordInContext } = require('../src/utils/passwordValidation');
|
||||
|
||||
const weakPassword = validatePassword('weak123');
|
||||
test('Weak password is rejected', () => !weakPassword.valid);
|
||||
test('Weak password has errors', () => weakPassword.errors.length > 0);
|
||||
|
||||
const strongPassword = validatePassword('Str0ng!P@ssw0rd123');
|
||||
test('Strong password is accepted', () => strongPassword.valid);
|
||||
test('Strong password has good score', () => strongPassword.score >= 3);
|
||||
|
||||
const shortPassword = validatePassword('Short!1');
|
||||
test('Short password is rejected', () => !shortPassword.valid &&
|
||||
shortPassword.errors.some(e => e.includes('12 characters')));
|
||||
|
||||
const noSpecialChar = validatePassword('NoSpecialChar123');
|
||||
test('Password without special char is rejected', () => !noSpecialChar.valid &&
|
||||
noSpecialChar.errors.some(e => e.includes('special character')));
|
||||
|
||||
// Context validation
|
||||
const adminContext = validatePasswordInContext('Admin123!Pass', 'admin', { username: 'admin' });
|
||||
test('Admin password with username is rejected', () => !adminContext.valid);
|
||||
|
||||
const galleryContext = validatePasswordInContext('Event123!Pass', 'gallery', { eventName: 'event' });
|
||||
test('Gallery password with event name is rejected', () => !galleryContext.valid);
|
||||
|
||||
// Test 3: Token Revocation
|
||||
console.log('\n3. Testing Token Revocation:');
|
||||
const { isTokenRevoked } = require('../src/utils/tokenRevocation');
|
||||
|
||||
const testToken = {
|
||||
jti: 'test-123',
|
||||
id: 1,
|
||||
type: 'admin',
|
||||
iat: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
|
||||
// This would need database setup to fully test
|
||||
test('Token revocation check runs', async () => {
|
||||
try {
|
||||
await isTokenRevoked(testToken);
|
||||
return true;
|
||||
} catch (e) {
|
||||
// Expected if tables don't exist yet
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Test 4: Bcrypt Rounds
|
||||
console.log('\n4. Testing Configurable Bcrypt:');
|
||||
const { getBcryptRounds, PASSWORD_CONFIG } = require('../src/utils/passwordValidation');
|
||||
|
||||
test('Bcrypt rounds are configurable', () => {
|
||||
const rounds = getBcryptRounds();
|
||||
return rounds >= 10 && rounds <= 14;
|
||||
});
|
||||
|
||||
test('Default bcrypt rounds is 12', () => {
|
||||
return PASSWORD_CONFIG.bcryptRounds === 12 ||
|
||||
PASSWORD_CONFIG.bcryptRounds === parseInt(process.env.BCRYPT_ROUNDS);
|
||||
});
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Test Summary ===');
|
||||
console.log(`Total tests: ${passed + failed}`);
|
||||
console.log(`Passed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('\n✅ All authentication V2 tests passed!');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n❌ Some tests failed. Review the implementation.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests
|
||||
runTests().catch(error => {
|
||||
console.error('Test error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test enhanced authentication features in Docker
|
||||
*/
|
||||
|
||||
const { db } = require('../src/database/db');
|
||||
const {
|
||||
checkAccountLockout,
|
||||
trackFailedAttempt,
|
||||
trackSuccessfulLogin
|
||||
} = require('../src/utils/authSecurity');
|
||||
|
||||
console.log('=== Testing Enhanced Auth Features ===\n');
|
||||
|
||||
async function testAuthFeatures() {
|
||||
try {
|
||||
// Test 1: Check if tables exist
|
||||
console.log('1. Checking database tables...');
|
||||
const loginAttempts = await db('login_attempts').count().first();
|
||||
console.log('✓ login_attempts table exists');
|
||||
|
||||
// Test 2: Test failed attempt tracking
|
||||
console.log('\n2. Testing failed attempt tracking...');
|
||||
await trackFailedAttempt('test-user', '127.0.0.1', 'Test User Agent');
|
||||
const attempts = await db('login_attempts')
|
||||
.where('identifier', 'test-user')
|
||||
.count()
|
||||
.first();
|
||||
console.log(`✓ Failed attempt tracked (${attempts.count} total)`);
|
||||
|
||||
// Test 3: Test lockout check
|
||||
console.log('\n3. Testing lockout detection...');
|
||||
|
||||
// Add 4 more failures to trigger lockout
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await trackFailedAttempt('test-user', '127.0.0.1', 'Test User Agent');
|
||||
}
|
||||
|
||||
const lockoutStatus = await checkAccountLockout('test-user');
|
||||
console.log(`✓ Lockout check works: ${lockoutStatus.isLocked ? 'LOCKED' : 'NOT LOCKED'}`);
|
||||
|
||||
if (lockoutStatus.isLocked) {
|
||||
console.log(` Remaining lockout time: ${lockoutStatus.remainingTime} seconds`);
|
||||
}
|
||||
|
||||
// Test 4: Test successful login tracking
|
||||
console.log('\n4. Testing successful login tracking...');
|
||||
await trackSuccessfulLogin('test-user', '127.0.0.1', 'Test User Agent');
|
||||
console.log('✓ Successful login tracked');
|
||||
|
||||
// Test 5: Check if auth routes load
|
||||
console.log('\n5. Testing enhanced auth routes...');
|
||||
try {
|
||||
const authRoutes = require('../src/routes/auth-enhanced');
|
||||
console.log('✓ Enhanced auth routes load successfully');
|
||||
} catch (e) {
|
||||
console.log('✗ Error loading enhanced auth routes:', e.message);
|
||||
}
|
||||
|
||||
// Clean up test data
|
||||
await db('login_attempts').where('identifier', 'test-user').delete();
|
||||
console.log('\n✓ Test data cleaned up');
|
||||
|
||||
console.log('\n✅ Enhanced auth features are working correctly!');
|
||||
console.log('\nNext step: Update server.js to use enhanced auth routes');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Test failed:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
testAuthFeatures();
|
||||
@@ -1,98 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test script to verify JWT_SECRET validation works correctly
|
||||
* This ensures our security fix doesn't break production
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
console.log('=== Testing JWT_SECRET Validation ===\n');
|
||||
|
||||
// Test 1: Server should fail to start without JWT_SECRET
|
||||
console.log('Test 1: Starting server without JWT_SECRET...');
|
||||
const test1 = spawn('node', [path.join(__dirname, '..', 'server.js')], {
|
||||
env: { ...process.env, JWT_SECRET: '' },
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
let test1Output = '';
|
||||
test1.stderr.on('data', (data) => {
|
||||
test1Output += data.toString();
|
||||
});
|
||||
|
||||
test1.on('close', (code) => {
|
||||
if (code === 1 && test1Output.includes('Missing required environment variable: JWT_SECRET')) {
|
||||
console.log('✅ Test 1 PASSED: Server correctly refuses to start without JWT_SECRET\n');
|
||||
runTest2();
|
||||
} else {
|
||||
console.log('❌ Test 1 FAILED: Server should have failed to start');
|
||||
console.log('Exit code:', code);
|
||||
console.log('Output:', test1Output);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 2: Server should fail with insecure default value
|
||||
function runTest2() {
|
||||
console.log('Test 2: Starting server with insecure JWT_SECRET...');
|
||||
const test2 = spawn('node', [path.join(__dirname, '..', 'server.js')], {
|
||||
env: { ...process.env, JWT_SECRET: 'your-secret-key' },
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
let test2Output = '';
|
||||
test2.stderr.on('data', (data) => {
|
||||
test2Output += data.toString();
|
||||
});
|
||||
|
||||
test2.on('close', (code) => {
|
||||
if (code === 1 && test2Output.includes('JWT_SECRET is set to the insecure default value')) {
|
||||
console.log('✅ Test 2 PASSED: Server correctly refuses insecure JWT_SECRET\n');
|
||||
runTest3();
|
||||
} else {
|
||||
console.log('❌ Test 2 FAILED: Server should have rejected insecure JWT_SECRET');
|
||||
console.log('Exit code:', code);
|
||||
console.log('Output:', test2Output);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test 3: Verify protectedImages functions work with valid JWT_SECRET
|
||||
function runTest3() {
|
||||
console.log('Test 3: Testing protectedImages functions...');
|
||||
|
||||
// Set a valid JWT_SECRET for this test
|
||||
process.env.JWT_SECRET = 'test-secret-key-that-is-long-enough-for-security';
|
||||
|
||||
try {
|
||||
// Load the module to test
|
||||
const protectedImagesPath = path.join(__dirname, '..', 'src', 'routes', 'protectedImages.js');
|
||||
delete require.cache[protectedImagesPath]; // Clear cache to ensure fresh load
|
||||
|
||||
// This will throw if JWT_SECRET is not available
|
||||
require(protectedImagesPath);
|
||||
|
||||
console.log('✅ Test 3 PASSED: protectedImages module loads successfully with valid JWT_SECRET\n');
|
||||
|
||||
console.log('=== All Tests Passed! ===');
|
||||
console.log('\nThe JWT_SECRET validation is working correctly.');
|
||||
console.log('Production systems must have JWT_SECRET set to a secure value.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.log('❌ Test 3 FAILED: Error loading protectedImages module');
|
||||
console.log('Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Give the first test some time to complete
|
||||
setTimeout(() => {
|
||||
if (test1.exitCode === null) {
|
||||
console.log('❌ Test 1 TIMEOUT: Server did not exit as expected');
|
||||
test1.kill();
|
||||
process.exit(1);
|
||||
}
|
||||
}, 5000);
|
||||
@@ -1,171 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test script to verify routes work correctly after SQL security fixes
|
||||
* Run this before deploying to production
|
||||
*/
|
||||
|
||||
const request = require('supertest');
|
||||
const app = require('../src/app');
|
||||
const { db } = require('../src/database/db');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// Generate admin token for testing
|
||||
const adminToken = jwt.sign(
|
||||
{ id: 1, username: 'admin', role: 'admin' },
|
||||
process.env.JWT_SECRET || 'test-secret'
|
||||
);
|
||||
|
||||
console.log('=== Testing Routes After SQL Security Fixes ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
async function testRoute(description, testFn) {
|
||||
try {
|
||||
await testFn();
|
||||
console.log(`✅ ${description}`);
|
||||
passed++;
|
||||
} catch (error) {
|
||||
console.log(`❌ ${description}`);
|
||||
console.error(` Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
// Test Dashboard Stats (uses whereRaw fixes)
|
||||
await testRoute('Dashboard stats endpoint', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/dashboard/stats')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
if (!res.body.hasOwnProperty('activeEvents')) {
|
||||
throw new Error('Missing activeEvents in response');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Analytics with days parameter (uses sanitizeDays)
|
||||
await testRoute('Analytics with valid days parameter', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/dashboard/analytics?days=7')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
if (!res.body.chartData || res.body.chartData.length !== 7) {
|
||||
throw new Error('Invalid chart data');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Analytics with SQL injection attempt in days
|
||||
await testRoute('Analytics rejects SQL injection in days parameter', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/dashboard/analytics?days=7; DROP TABLE events; --')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
// Should default to 7 days
|
||||
if (res.body.chartData.length !== 7) {
|
||||
throw new Error('Days parameter not properly sanitized');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Event search with normal text (uses escapeLikePattern)
|
||||
await testRoute('Event search with normal text', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/events?search=test')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
if (!res.body.hasOwnProperty('events')) {
|
||||
throw new Error('Missing events in response');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Event search with special characters
|
||||
await testRoute('Event search with special characters', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/events?search=50%_test')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
// Should handle special chars safely
|
||||
if (!res.body.hasOwnProperty('events')) {
|
||||
throw new Error('Failed to handle special characters');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Event search with SQL injection attempt
|
||||
await testRoute('Event search prevents SQL injection', async () => {
|
||||
const res = await request(app)
|
||||
.get("/api/admin/events?search=' OR 1=1 --")
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
// Should return empty results, not all events
|
||||
if (!res.body.hasOwnProperty('events')) {
|
||||
throw new Error('SQL injection may not be prevented');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Photo search (if event exists)
|
||||
await testRoute('Photo search functionality', async () => {
|
||||
// First check if we have any events
|
||||
const event = await db('events').first();
|
||||
if (event) {
|
||||
const res = await request(app)
|
||||
.get(`/api/admin/events/${event.id}/photos?search=test`)
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
if (!res.body.hasOwnProperty('photos')) {
|
||||
throw new Error('Missing photos in response');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test Activity endpoint
|
||||
await testRoute('Activity log endpoint', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/dashboard/activity?limit=10')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
if (!Array.isArray(res.body)) {
|
||||
throw new Error('Activity should return array');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Health endpoint
|
||||
await testRoute('Health check endpoint', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/dashboard/health')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
if (!res.body.hasOwnProperty('overall')) {
|
||||
throw new Error('Missing overall health status');
|
||||
}
|
||||
});
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Test Summary ===');
|
||||
console.log(`Total tests: ${passed + failed}`);
|
||||
console.log(`Passed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('\n✅ All route tests passed! Safe to deploy.');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n❌ Some tests failed. Review the fixes before deploying.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests
|
||||
runTests().catch(error => {
|
||||
console.error('Test runner error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test script to verify SQL security fixes work correctly
|
||||
* Tests both functionality and security of the fixes
|
||||
*/
|
||||
|
||||
const {
|
||||
sanitizeDays,
|
||||
escapeLikePattern,
|
||||
validateSortColumn,
|
||||
validateSortOrder
|
||||
} = require('../src/utils/sqlSecurity');
|
||||
|
||||
console.log('=== Testing SQL Security Utilities ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(description, fn) {
|
||||
try {
|
||||
const result = fn();
|
||||
if (result) {
|
||||
console.log(`✅ ${description}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(`❌ ${description}`);
|
||||
failed++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ ${description} - Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Test sanitizeDays
|
||||
console.log('Testing sanitizeDays function:');
|
||||
test('Valid number returns same number', () => sanitizeDays(7) === 7);
|
||||
test('String number is parsed correctly', () => sanitizeDays('30') === 30);
|
||||
test('Invalid input returns default 7', () => sanitizeDays('abc') === 7);
|
||||
test('Negative number returns 1', () => sanitizeDays(-5) === 1);
|
||||
test('Zero returns 1', () => sanitizeDays(0) === 1);
|
||||
test('Large number is capped at 365', () => sanitizeDays(500) === 365);
|
||||
test('NaN returns default 7', () => sanitizeDays(NaN) === 7);
|
||||
test('Null returns default 7', () => sanitizeDays(null) === 7);
|
||||
test('Undefined returns default 7', () => sanitizeDays(undefined) === 7);
|
||||
|
||||
// Test escapeLikePattern
|
||||
console.log('\nTesting escapeLikePattern function:');
|
||||
test('Normal text unchanged', () => escapeLikePattern('hello world') === 'hello world');
|
||||
test('Percent sign escaped', () => escapeLikePattern('50%') === '50\\%');
|
||||
test('Underscore escaped', () => escapeLikePattern('user_name') === 'user\\_name');
|
||||
test('Backslash escaped', () => escapeLikePattern('path\\to\\file') === 'path\\\\to\\\\file');
|
||||
test('Multiple special chars escaped', () => escapeLikePattern('50%_test\\') === '50\\%\\_test\\\\');
|
||||
test('Empty string returns empty', () => escapeLikePattern('') === '');
|
||||
test('Null returns empty string', () => escapeLikePattern(null) === '');
|
||||
test('Undefined returns empty string', () => escapeLikePattern(undefined) === '');
|
||||
test('Single quotes escaped', () => escapeLikePattern("O'Brien") === "O''Brien");
|
||||
|
||||
// Test SQL injection attempts
|
||||
console.log('\nTesting SQL injection prevention:');
|
||||
test('SQL injection attempt with quotes', () => {
|
||||
const malicious = "'; DROP TABLE users; --";
|
||||
const escaped = escapeLikePattern(malicious);
|
||||
return escaped === "''; DROP TABLE users; --" && escaped.includes("''");
|
||||
});
|
||||
|
||||
test('SQL injection with LIKE wildcards', () => {
|
||||
const malicious = "%' OR 1=1 --";
|
||||
const escaped = escapeLikePattern(malicious);
|
||||
return escaped === "\\%'' OR 1=1 --";
|
||||
});
|
||||
|
||||
test('Days parameter injection attempt', () => {
|
||||
const malicious = "7; DROP TABLE events; --";
|
||||
return sanitizeDays(malicious) === 7;
|
||||
});
|
||||
|
||||
// Test validateSortColumn
|
||||
console.log('\nTesting validateSortColumn function:');
|
||||
const allowedColumns = ['name', 'date', 'size'];
|
||||
test('Valid column accepted', () => validateSortColumn('name', allowedColumns, 'date') === 'name');
|
||||
test('Invalid column returns default', () => validateSortColumn('price', allowedColumns, 'date') === 'date');
|
||||
test('Null returns default', () => validateSortColumn(null, allowedColumns, 'date') === 'date');
|
||||
test('Empty string returns default', () => validateSortColumn('', allowedColumns, 'date') === 'date');
|
||||
|
||||
// Test validateSortOrder
|
||||
console.log('\nTesting validateSortOrder function:');
|
||||
test('Valid asc accepted', () => validateSortOrder('asc') === 'asc');
|
||||
test('Valid ASC accepted', () => validateSortOrder('ASC') === 'asc');
|
||||
test('Valid desc accepted', () => validateSortOrder('desc') === 'desc');
|
||||
test('Invalid order returns desc', () => validateSortOrder('random') === 'desc');
|
||||
test('Null returns desc', () => validateSortOrder(null) === 'desc');
|
||||
test('Empty returns desc', () => validateSortOrder('') === 'desc');
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Test Summary ===');
|
||||
console.log(`Total tests: ${passed + failed}`);
|
||||
console.log(`Passed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('\n✅ All tests passed! SQL security utilities are working correctly.');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n❌ Some tests failed. Please check the implementation.');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
const axios = require('axios');
|
||||
const FormData = require('form-data');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function testUpload() {
|
||||
try {
|
||||
// First login
|
||||
console.log('1. Logging in as admin...');
|
||||
const loginResponse = await axios.post('http://localhost:3000/api/admin/auth/login', {
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
});
|
||||
|
||||
const token = loginResponse.data.token;
|
||||
console.log('✓ Login successful');
|
||||
|
||||
// Create a test image file
|
||||
const testImagePath = path.join(__dirname, 'test-image.png');
|
||||
const imageBuffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', 'base64');
|
||||
fs.writeFileSync(testImagePath, imageBuffer);
|
||||
|
||||
// Test upload
|
||||
console.log('\n2. Testing upload with category_id=7...');
|
||||
const form = new FormData();
|
||||
form.append('photos', fs.createReadStream(testImagePath), 'test-image.png');
|
||||
form.append('category_id', '7');
|
||||
|
||||
console.log('Form data headers:', form.getHeaders());
|
||||
|
||||
try {
|
||||
const uploadResponse = await axios.post(
|
||||
'http://localhost:3000/api/admin/events/12/upload',
|
||||
form,
|
||||
{
|
||||
headers: {
|
||||
...form.getHeaders(),
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log('✓ Upload successful:', uploadResponse.data);
|
||||
} catch (error) {
|
||||
console.error('✗ Upload failed:', error.response?.status, error.response?.data);
|
||||
if (error.response?.data) {
|
||||
console.error('Error details:', JSON.stringify(error.response.data, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
fs.unlinkSync(testImagePath);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testUpload();
|
||||
@@ -1,106 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Verify enhanced authentication is active and working
|
||||
*/
|
||||
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
console.log('=== Verifying Enhanced Authentication Activation ===\n');
|
||||
|
||||
async function verifyAuth() {
|
||||
const results = {
|
||||
databaseTables: false,
|
||||
serverConfig: false,
|
||||
lockoutActive: false,
|
||||
cleanupActive: false
|
||||
};
|
||||
|
||||
try {
|
||||
// 1. Check database tables
|
||||
console.log('1. Checking database tables...');
|
||||
const hasLoginAttempts = await db.schema.hasTable('login_attempts');
|
||||
const hasSecurityColumns = await db.schema.hasColumn('admin_users', 'password_changed_at');
|
||||
|
||||
if (hasLoginAttempts && hasSecurityColumns) {
|
||||
results.databaseTables = true;
|
||||
console.log('✓ Auth tables and columns exist');
|
||||
|
||||
// Count attempts
|
||||
const attempts = await db('login_attempts').count().first();
|
||||
console.log(` Total login attempts tracked: ${attempts['count(*)'] || 0}`);
|
||||
} else {
|
||||
console.log('✗ Auth tables missing');
|
||||
}
|
||||
|
||||
// 2. Check server configuration
|
||||
console.log('\n2. Checking server configuration...');
|
||||
const fs = require('fs');
|
||||
const serverContent = fs.readFileSync('./server.js', 'utf8');
|
||||
|
||||
if (serverContent.includes("require('./src/routes/auth-enhanced')")) {
|
||||
results.serverConfig = true;
|
||||
console.log('✓ Server using enhanced auth routes');
|
||||
} else {
|
||||
console.log('✗ Server using original auth routes');
|
||||
}
|
||||
|
||||
if (serverContent.includes('initializeCleanupJob')) {
|
||||
results.cleanupActive = true;
|
||||
console.log('✓ Cleanup job initialized');
|
||||
} else {
|
||||
console.log('✗ Cleanup job not initialized');
|
||||
}
|
||||
|
||||
// 3. Test lockout functionality
|
||||
console.log('\n3. Testing lockout functionality...');
|
||||
const { checkAccountLockout } = require('../src/utils/authSecurity');
|
||||
|
||||
// Check a test account
|
||||
const lockoutTest = await checkAccountLockout('lockout-test-user');
|
||||
console.log(`✓ Lockout check functional: ${lockoutTest.isLocked ? 'locked' : 'not locked'}`);
|
||||
results.lockoutActive = true;
|
||||
|
||||
// 4. Check recent activity
|
||||
console.log('\n4. Recent authentication activity...');
|
||||
const recentAttempts = await db('login_attempts')
|
||||
.orderBy('attempt_time', 'desc')
|
||||
.limit(5);
|
||||
|
||||
if (recentAttempts.length > 0) {
|
||||
console.log('Recent login attempts:');
|
||||
recentAttempts.forEach(attempt => {
|
||||
const time = new Date(attempt.attempt_time).toLocaleString();
|
||||
console.log(` ${time} - ${attempt.identifier} - ${attempt.success ? 'SUCCESS' : 'FAILED'}`);
|
||||
});
|
||||
} else {
|
||||
console.log('No login attempts recorded yet');
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Summary ===');
|
||||
const allGood = Object.values(results).every(v => v === true);
|
||||
|
||||
if (allGood) {
|
||||
console.log('✅ Enhanced authentication is FULLY ACTIVE!');
|
||||
console.log('\nFeatures enabled:');
|
||||
console.log('- Account lockout protection (5 attempts)');
|
||||
console.log('- Login attempt tracking');
|
||||
console.log('- Enhanced token validation');
|
||||
console.log('- Session management');
|
||||
console.log('- Automatic cleanup of old records');
|
||||
} else {
|
||||
console.log('⚠️ Some features not active:');
|
||||
Object.entries(results).forEach(([key, value]) => {
|
||||
console.log(` ${key}: ${value ? '✓' : '✗'}`);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Verification error:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
verifyAuth();
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Verification script to check SQL queries are built correctly
|
||||
* This simulates the query building without running the full app
|
||||
*/
|
||||
|
||||
const {
|
||||
sanitizeDays,
|
||||
escapeLikePattern,
|
||||
validateSortColumn,
|
||||
validateSortOrder
|
||||
} = require('../src/utils/sqlSecurity');
|
||||
|
||||
console.log('=== Verifying SQL Security Fixes ===\n');
|
||||
|
||||
// Test 1: Verify date range queries
|
||||
console.log('1. Testing date range query building:');
|
||||
console.log(' Input days: "7; DROP TABLE events; --"');
|
||||
const safeDays = sanitizeDays("7; DROP TABLE events; --");
|
||||
console.log(' Sanitized days:', safeDays);
|
||||
console.log(' ✅ SQL injection attempt neutralized\n');
|
||||
|
||||
// Test 2: Verify LIKE pattern escaping
|
||||
console.log('2. Testing LIKE pattern escaping:');
|
||||
const testPatterns = [
|
||||
"normal search",
|
||||
"50%_wildcard",
|
||||
"'; DROP TABLE users; --",
|
||||
"test\\path",
|
||||
"O'Brien"
|
||||
];
|
||||
|
||||
testPatterns.forEach(pattern => {
|
||||
const escaped = escapeLikePattern(pattern);
|
||||
console.log(` "${pattern}" → "${escaped}"`);
|
||||
});
|
||||
console.log(' ✅ All patterns safely escaped\n');
|
||||
|
||||
// Test 3: Simulate date query building
|
||||
console.log('3. Simulating safe date query:');
|
||||
const days = 7;
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - days);
|
||||
console.log(` WHERE timestamp >= '${startDate.toISOString()}'`);
|
||||
console.log(' ✅ Using parameterized date instead of whereRaw\n');
|
||||
|
||||
// Test 4: Verify sort validation
|
||||
console.log('4. Testing sort column/order validation:');
|
||||
const allowedColumns = ['created_at', 'event_name', 'expires_at'];
|
||||
console.log(' Allowed columns:', allowedColumns);
|
||||
|
||||
const testSorts = [
|
||||
{ column: 'created_at', order: 'desc' },
|
||||
{ column: 'invalid_column', order: 'asc' },
|
||||
{ column: '; DROP TABLE --', order: 'random' }
|
||||
];
|
||||
|
||||
testSorts.forEach(({ column, order }) => {
|
||||
const safeColumn = validateSortColumn(column, allowedColumns, 'created_at');
|
||||
const safeOrder = validateSortOrder(order);
|
||||
console.log(` "${column}" ${order} → "${safeColumn}" ${safeOrder}`);
|
||||
});
|
||||
console.log(' ✅ Invalid columns/orders rejected\n');
|
||||
|
||||
// Test 5: Show example of safe query patterns
|
||||
console.log('5. Safe Query Patterns Used:');
|
||||
console.log(' ❌ OLD: .whereRaw(`timestamp >= datetime("now", "-${days} days")`)')
|
||||
console.log(' ✅ NEW: .where("timestamp", ">=", startDate.toISOString())\n');
|
||||
|
||||
console.log(' ❌ OLD: .where("event_name", "like", `%${search}%`)')
|
||||
console.log(' ✅ NEW: .where("event_name", "like", `%${escapeLikePattern(search)}%`)\n');
|
||||
|
||||
console.log('=== Verification Complete ===');
|
||||
console.log('All SQL injection vulnerabilities have been addressed.');
|
||||
console.log('\nNext steps:');
|
||||
console.log('1. Test in development environment');
|
||||
console.log('2. Monitor logs during testing');
|
||||
console.log('3. Deploy with rollback plan ready');
|
||||
console.log('4. Monitor production logs after deployment');
|
||||
@@ -1,32 +0,0 @@
|
||||
// This is a partial server.js showing the enhanced rate limiting
|
||||
// Only the relevant parts are shown - merge with existing server.js
|
||||
|
||||
const { createSecureSkipFunction, logRateLimitHit } = require('./src/utils/rateLimitSecurity');
|
||||
|
||||
// Enhanced rate limiting with secure skip function
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: process.env.NODE_ENV === 'development' ? 1000 : 100,
|
||||
skip: createSecureSkipFunction(), // Use secure skip function
|
||||
handler: (req, res) => {
|
||||
logRateLimitHit(req, res);
|
||||
res.status(429).json({
|
||||
error: 'Too many requests from this IP, please try again later.'
|
||||
});
|
||||
},
|
||||
standardHeaders: true, // Return rate limit info in headers
|
||||
legacyHeaders: false, // Disable X-RateLimit headers
|
||||
});
|
||||
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5, // limit auth attempts
|
||||
skipSuccessfulRequests: true, // Don't count successful logins
|
||||
handler: (req, res) => {
|
||||
logRateLimitHit(req, res);
|
||||
res.status(429).json({
|
||||
error: 'Too many login attempts, please try again later.',
|
||||
retryAfter: res.getHeader('Retry-After')
|
||||
});
|
||||
}
|
||||
});
|
||||
+22
-4
@@ -13,7 +13,7 @@ const path = require('path');
|
||||
const { initializeDatabase } = require('./src/database/db');
|
||||
const { startFileWatcher } = require('./src/services/fileWatcher');
|
||||
const { startExpirationChecker } = require('./src/services/expirationChecker');
|
||||
const { startEmailQueueProcessor } = require('./src/services/emailProcessor');
|
||||
const { initializeTransporter, startEmailQueueProcessor } = require('./src/services/emailProcessor');
|
||||
const { maintenanceMiddleware } = require('./src/middleware/maintenance');
|
||||
const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout');
|
||||
const logger = require('./src/utils/logger');
|
||||
@@ -152,8 +152,25 @@ app.use('/thumbnails', require('./src/middleware/photoAuth'), setCorsHeaders, se
|
||||
app.use('/uploads', setCorsHeaders, secureStatic(path.join(__dirname, 'storage/uploads')));
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
app.get('/api/health', async (req, res) => {
|
||||
try {
|
||||
// Check database connectivity
|
||||
await db.raw('SELECT 1');
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
database: 'connected',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Health check failed:', error);
|
||||
res.status(503).json({
|
||||
status: 'error',
|
||||
database: 'disconnected',
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Routes
|
||||
@@ -189,7 +206,8 @@ async function startServer() {
|
||||
// Start expiration checker
|
||||
startExpirationChecker();
|
||||
|
||||
// Start email queue processor
|
||||
// Initialize email transporter and start queue processor
|
||||
await initializeTransporter();
|
||||
startEmailQueueProcessor();
|
||||
|
||||
app.listen(PORT, () => {
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
require('dotenv').config();
|
||||
|
||||
// Validate critical environment variables before proceeding
|
||||
const { validateEnvironment } = require('./src/config/validateEnv');
|
||||
validateEnvironment();
|
||||
|
||||
const express = require('express');
|
||||
const helmet = require('helmet');
|
||||
const cors = require('cors');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const path = require('path');
|
||||
const { initializeDatabase } = require('./src/database/db');
|
||||
const { startFileWatcher } = require('./src/services/fileWatcher');
|
||||
const { startExpirationChecker } = require('./src/services/expirationChecker');
|
||||
const { startEmailQueueProcessor } = require('./src/services/emailProcessor');
|
||||
const { maintenanceMiddleware } = require('./src/middleware/maintenance');
|
||||
const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout');
|
||||
const logger = require('./src/utils/logger');
|
||||
|
||||
// Import routes
|
||||
const authRoutes = require('./src/routes/auth');
|
||||
const eventRoutes = require('./src/routes/events');
|
||||
const galleryRoutes = require('./src/routes/gallery');
|
||||
const adminRoutes = require('./src/routes/admin');
|
||||
const adminAuthRoutes = require('./src/routes/adminAuth');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
|
||||
// CORS configuration
|
||||
const corsOptions = {
|
||||
origin: function (origin, callback) {
|
||||
const allowedOrigins = [
|
||||
process.env.FRONTEND_URL || 'http://localhost:3005',
|
||||
process.env.ADMIN_URL || 'http://localhost:3005',
|
||||
'http://localhost:5173', // Vite dev server
|
||||
'http://localhost:3002', // Backend server
|
||||
'http://localhost:3001', // For API testing
|
||||
'http://localhost:3000' // Direct backend access
|
||||
];
|
||||
|
||||
// Allow requests with no origin (like mobile apps or curl)
|
||||
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
}
|
||||
},
|
||||
credentials: true
|
||||
};
|
||||
|
||||
app.use(cors(corsOptions));
|
||||
|
||||
// Rate limiting with admin bypass
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: process.env.NODE_ENV === 'development' ? 1000 : 100, // More lenient in development
|
||||
skip: (req) => {
|
||||
// Skip rate limiting for authenticated admin users
|
||||
if (req.path.startsWith('/api/admin/') && req.headers.authorization) {
|
||||
const token = req.headers.authorization.replace('Bearer ', '');
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
return decoded.type === 'admin';
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Also skip rate limiting for public settings endpoint in development
|
||||
if (process.env.NODE_ENV === 'development' && req.path === '/api/public/settings') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5 // limit auth attempts
|
||||
});
|
||||
|
||||
// Apply rate limiting - admin routes check will skip for valid admin tokens
|
||||
app.use('/api/', limiter);
|
||||
app.use('/api/auth', authLimiter);
|
||||
|
||||
// Body parsing middleware
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Maintenance mode middleware - add after body parsing but before routes
|
||||
app.use(maintenanceMiddleware);
|
||||
|
||||
// Session timeout middleware for admin routes
|
||||
app.use('/api/admin', sessionTimeoutMiddleware);
|
||||
|
||||
// Middleware to set CORS headers for static files
|
||||
const setCorsHeaders = (req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', req.headers.origin || '*');
|
||||
res.header('Access-Control-Allow-Credentials', 'true');
|
||||
res.header('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
next();
|
||||
};
|
||||
|
||||
// Static file serving for photos (protected)
|
||||
app.use('/photos', require('./src/middleware/photoAuth'), setCorsHeaders, express.static(path.join(__dirname, 'storage/events/active')));
|
||||
|
||||
// Static file serving for thumbnails (protected)
|
||||
app.use('/thumbnails', require('./src/middleware/photoAuth'), setCorsHeaders, express.static(path.join(__dirname, 'storage/thumbnails')));
|
||||
|
||||
// Static file serving for uploads (public - logos, favicons)
|
||||
app.use('/uploads', setCorsHeaders, express.static(path.join(__dirname, 'storage/uploads')));
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/events', eventRoutes);
|
||||
app.use('/api/gallery', galleryRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
app.use('/api/admin/auth', adminAuthRoutes);
|
||||
app.use('/api/admin/system', require('./src/routes/adminSystem'));
|
||||
app.use('/api/public/settings', require('./src/routes/publicSettings'));
|
||||
app.use('/api/public', require('./src/routes/publicCMS'));
|
||||
app.use('/api/images', require('./src/routes/protectedImages'));
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error(err.stack);
|
||||
res.status(500).json({ error: 'Something went wrong!' });
|
||||
});
|
||||
|
||||
// Initialize services
|
||||
async function startServer() {
|
||||
try {
|
||||
// Initialize database
|
||||
await initializeDatabase();
|
||||
|
||||
// Start file watcher
|
||||
startFileWatcher();
|
||||
|
||||
// Start expiration checker
|
||||
startExpirationChecker();
|
||||
|
||||
// Start email queue processor
|
||||
startEmailQueueProcessor();
|
||||
|
||||
app.listen(PORT, () => {
|
||||
logger.info(`Server running on port ${PORT}`);
|
||||
logger.info(`Admin interface: ${process.env.ADMIN_URL || 'http://localhost:3000'}`);
|
||||
logger.info(`Frontend: ${process.env.FRONTEND_URL || 'http://localhost:3001'}`);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startServer();
|
||||
|
||||
module.exports = app; // For testing
|
||||
+39
-40
@@ -1,13 +1,7 @@
|
||||
const knex = require('knex');
|
||||
const path = require('path');
|
||||
const knexConfig = require('../../knexfile');
|
||||
|
||||
const db = knex({
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: path.join(__dirname, '../../data/photo_sharing.db')
|
||||
},
|
||||
useNullAsDefault: true
|
||||
});
|
||||
const db = knex(knexConfig);
|
||||
|
||||
async function initializeDatabase() {
|
||||
// Events table
|
||||
@@ -35,37 +29,42 @@ async function initializeDatabase() {
|
||||
} else {
|
||||
// Check if color_theme needs to be updated to TEXT type
|
||||
// This is needed for larger theme configurations
|
||||
try {
|
||||
await db.raw(`
|
||||
CREATE TABLE IF NOT EXISTS events_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
event_name TEXT NOT NULL,
|
||||
event_date DATE NOT NULL,
|
||||
host_email TEXT NOT NULL,
|
||||
admin_email TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
welcome_message TEXT,
|
||||
color_theme TEXT,
|
||||
share_link TEXT UNIQUE NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
is_archived BOOLEAN DEFAULT 0,
|
||||
archive_path TEXT,
|
||||
archived_at DATETIME,
|
||||
allow_user_uploads BOOLEAN DEFAULT 0,
|
||||
upload_category_id INTEGER
|
||||
)
|
||||
`);
|
||||
|
||||
await db.raw(`INSERT INTO events_new SELECT * FROM events`);
|
||||
await db.raw(`DROP TABLE events`);
|
||||
await db.raw(`ALTER TABLE events_new RENAME TO events`);
|
||||
} catch (error) {
|
||||
// If the migration fails, it might already have been applied
|
||||
console.log('Color theme migration may have already been applied');
|
||||
const isPostgres = knexConfig.client === 'pg';
|
||||
|
||||
if (!isPostgres) {
|
||||
// SQLite-specific migration
|
||||
try {
|
||||
await db.raw(`
|
||||
CREATE TABLE IF NOT EXISTS events_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
event_name TEXT NOT NULL,
|
||||
event_date DATE NOT NULL,
|
||||
host_email TEXT NOT NULL,
|
||||
admin_email TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
welcome_message TEXT,
|
||||
color_theme TEXT,
|
||||
share_link TEXT UNIQUE NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
is_archived BOOLEAN DEFAULT 0,
|
||||
archive_path TEXT,
|
||||
archived_at DATETIME,
|
||||
allow_user_uploads BOOLEAN DEFAULT 0,
|
||||
upload_category_id INTEGER
|
||||
)
|
||||
`);
|
||||
|
||||
await db.raw(`INSERT INTO events_new SELECT * FROM events`);
|
||||
await db.raw(`DROP TABLE events`);
|
||||
await db.raw(`ALTER TABLE events_new RENAME TO events`);
|
||||
} catch (error) {
|
||||
// If the migration fails, it might already have been applied
|
||||
console.log('Color theme migration may have already been applied');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,4 +224,4 @@ async function logActivity(activityType, metadata = {}, eventId = null, actor =
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { db, initializeDatabase, logActivity };
|
||||
module.exports = { db, initializeDatabase, logActivity };
|
||||
@@ -395,12 +395,15 @@ function stopEmailQueueProcessor() {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on module load
|
||||
initializeTransporter().then(() => {
|
||||
startEmailQueueProcessor();
|
||||
});
|
||||
// Initialize on module load - DISABLED for production startup
|
||||
// This will be called from server.js after database is ready
|
||||
// initializeTransporter().then(() => {
|
||||
// startEmailQueueProcessor();
|
||||
// });
|
||||
|
||||
module.exports = {
|
||||
initializeTransporter,
|
||||
startEmailQueueProcessor,
|
||||
sendTemplateEmail,
|
||||
processEmailQueue,
|
||||
queueEmail,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
const { db } = require('./src/database/db');
|
||||
|
||||
async function updateTemplate() {
|
||||
try {
|
||||
const englishBody = `<h2>Gallery Successfully Created</h2>
|
||||
<p>Dear {{host_name}},</p>
|
||||
<p>Your photo gallery "{{event_name}}" has been successfully created\!</p>
|
||||
{{welcome_message_section}}
|
||||
<p><strong>Gallery Details:</strong></p>
|
||||
<ul>
|
||||
<li>Event Date: {{event_date}}</li>
|
||||
<li>Gallery Link: <a href="{{gallery_link}}" style="color: #5C8762; text-decoration: none;">{{gallery_link}}</a></li>
|
||||
<li>Password: {{gallery_password}}</li>
|
||||
<li>Valid Until: {{expiry_date}}</li>
|
||||
</ul>
|
||||
<p>Share this link and password with your guests so they can view and download photos.</p>
|
||||
<p><a href="{{gallery_link}}" style="display: inline-block; padding: 10px 20px; background-color: #5C8762; color: white; text-decoration: none; border-radius: 5px;">View Gallery</a></p>`;
|
||||
|
||||
const germanBody = `<h2>Galerie erfolgreich erstellt</h2>
|
||||
<p>Liebe(r) {{host_name}},</p>
|
||||
<p>Ihre Fotogalerie "{{event_name}}" wurde erfolgreich erstellt\!</p>
|
||||
{{welcome_message_section}}
|
||||
<p><strong>Galerie-Details:</strong></p>
|
||||
<ul>
|
||||
<li>Veranstaltungsdatum: {{event_date}}</li>
|
||||
<li>Galerie-Link: <a href="{{gallery_link}}" style="color: #5C8762; text-decoration: none;">{{gallery_link}}</a></li>
|
||||
<li>Passwort: {{gallery_password}}</li>
|
||||
<li>Gültig bis: {{expiry_date}}</li>
|
||||
</ul>
|
||||
<p>Teilen Sie diesen Link und das Passwort mit Ihren Gästen, damit sie Fotos ansehen und herunterladen können.</p>
|
||||
<p><a href="{{gallery_link}}" style="display: inline-block; padding: 10px 20px; background-color: #5C8762; color: white; text-decoration: none; border-radius: 5px;">Galerie anzeigen</a></p>`;
|
||||
|
||||
await db('email_templates')
|
||||
.where('template_key', 'gallery_created')
|
||||
.update({
|
||||
body_html_en: englishBody,
|
||||
body_html_de: germanBody
|
||||
});
|
||||
|
||||
console.log('Email template updated successfully');
|
||||
} catch (error) {
|
||||
console.error('Error updating template:', error);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
updateTemplate();
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
# wait-for-db.sh - Wait for PostgreSQL to be ready before starting the application
|
||||
|
||||
set -e
|
||||
|
||||
host="$DB_HOST"
|
||||
port="${DB_PORT:-5432}"
|
||||
user="${DB_USER:-picpeak}"
|
||||
|
||||
echo "Waiting for PostgreSQL at $host:$port..."
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
until PGPASSWORD=$DB_PASSWORD psql -h "$host" -p "$port" -U "$user" -d "${DB_NAME:-picpeak}" -c '\q' 2>/dev/null; do
|
||||
>&2 echo "PostgreSQL is unavailable - sleeping"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
>&2 echo "PostgreSQL is up - executing command"
|
||||
|
||||
# Run migrations
|
||||
echo "Running database migrations..."
|
||||
npm run migrate
|
||||
|
||||
# Execute the main command
|
||||
exec "$@"
|
||||
@@ -0,0 +1,61 @@
|
||||
# Development Docker Compose Configuration
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=3000
|
||||
- JWT_SECRET=dev-secret-change-in-production
|
||||
- ADMIN_URL=http://localhost:3005
|
||||
- FRONTEND_URL=http://localhost:3005
|
||||
- DATABASE_CLIENT=sqlite3
|
||||
- DATABASE_PATH=./data/photo_sharing.db
|
||||
# Email - uses Mailhog
|
||||
- SMTP_HOST=mailhog
|
||||
- SMTP_PORT=1025
|
||||
- SMTP_SECURE=false
|
||||
- EMAIL_FROM=noreply@photo-sharing.local
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
depends_on:
|
||||
- mailhog
|
||||
command: sh -c "npm install && npm run dev"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3005:80"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
- ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
mailhog:
|
||||
image: mailhog/mailhog:latest
|
||||
ports:
|
||||
- "1025:1025" # SMTP
|
||||
- "8025:8025" # Web UI
|
||||
@@ -1,109 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.dev
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=3000
|
||||
- JWT_SECRET=b375996f704bb1541ad9297e7710a215fce0b59ecf1c43236aeea32ec01e7b27
|
||||
- ADMIN_URL=http://localhost:3005
|
||||
- FRONTEND_URL=http://localhost:3005
|
||||
# Email - uses Mailhog
|
||||
- SMTP_HOST=mailhog
|
||||
- SMTP_PORT=1025
|
||||
- SMTP_SECURE=false
|
||||
- SMTP_USER=
|
||||
- SMTP_PASS=
|
||||
- EMAIL_FROM=noreply@photo-sharing.local
|
||||
# Storage path
|
||||
- STORAGE_PATH=/app/storage
|
||||
# Umami Analytics (optional)
|
||||
- UMAMI_URL=
|
||||
- UMAMI_WEBSITE_ID=
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
depends_on:
|
||||
- mailhog
|
||||
command: sh -c "npm install && npm run dev"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
- VITE_API_URL=http://localhost:3001
|
||||
- VITE_UMAMI_URL=
|
||||
- VITE_UMAMI_WEBSITE_ID=
|
||||
ports:
|
||||
- "3005:80"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
- ./frontend/dist:/usr/share/nginx/html
|
||||
- ./frontend/nginx.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# Development frontend with hot reload
|
||||
frontend-dev:
|
||||
image: node:18-alpine
|
||||
working_dir: /app
|
||||
ports:
|
||||
- "3002:5173"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- VITE_API_URL=http://localhost:3001
|
||||
- VITE_UMAMI_URL=
|
||||
- VITE_UMAMI_WEBSITE_ID=
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
command: sh -c "npm install --legacy-peer-deps && npm run dev -- --host"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
mailhog:
|
||||
image: mailhog/mailhog:latest
|
||||
ports:
|
||||
- "1025:1025" # SMTP
|
||||
- "8025:8025" # Web UI
|
||||
|
||||
# Optional: File watcher service
|
||||
file-watcher:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.dev
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- STORAGE_PATH=/app/storage
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
command: node src/services/fileWatcher.js
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
node_modules_backend:
|
||||
node_modules_frontend:
|
||||
+22
-2
@@ -8,20 +8,35 @@ services:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ADMIN_URL=${ADMIN_URL}
|
||||
- FRONTEND_URL=${FRONTEND_URL}
|
||||
# Database
|
||||
- DATABASE_CLIENT=pg
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
- DB_USER=${DB_USER:-picpeak}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_NAME=${DB_NAME:-picpeak}
|
||||
# Email
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_SECURE=${SMTP_SECURE}
|
||||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASS=${SMTP_PASS}
|
||||
- EMAIL_FROM=${EMAIL_FROM}
|
||||
# Analytics
|
||||
- UMAMI_URL=${UMAMI_URL}
|
||||
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
|
||||
# Storage paths
|
||||
- STORAGE_PATH=/app/storage
|
||||
- EVENTS_PATH=/app/storage/events
|
||||
- ARCHIVE_PATH=/app/storage/events/archived
|
||||
volumes:
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
@@ -70,19 +85,24 @@ services:
|
||||
image: postgres:14-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=${DB_USER:-photoapp}
|
||||
- POSTGRES_USER=${DB_USER:-picpeak}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME:-picpeak}
|
||||
# Allow connections from any host with password authentication
|
||||
- POSTGRES_HOST_AUTH_METHOD=scram-sha-256
|
||||
- POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=trust
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- picpeak
|
||||
# Allow connections without SSL requirement from Docker network
|
||||
command: postgres -c ssl=off
|
||||
|
||||
umami:
|
||||
image: ghcr.io/umami-software/umami:postgresql-latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${DB_USER:-photoapp}:${DB_PASSWORD}@db:5432/umami
|
||||
DATABASE_URL: postgresql://${DB_USER:-picpeak}:${DB_PASSWORD}@db:5432/umami
|
||||
DATABASE_TYPE: postgresql
|
||||
HASH_SALT: ${UMAMI_HASH_SALT}
|
||||
depends_on:
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# docker-compose.yml - Development configuration
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=3000
|
||||
- JWT_SECRET=b55e4b3e9f212e1d1836c2ee2ec5ac47672350acdf1391c894d3433646579ad0
|
||||
- ADMIN_URL=http://localhost:3001
|
||||
- FRONTEND_URL=http://localhost:3005
|
||||
- SMTP_HOST=mailhog
|
||||
- SMTP_PORT=1025
|
||||
- SMTP_SECURE=false
|
||||
- SMTP_USER=
|
||||
- SMTP_PASS=
|
||||
- EMAIL_FROM=noreply@localhost
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
depends_on:
|
||||
- mailhog
|
||||
command: node server.js
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3005:80"
|
||||
environment:
|
||||
- REACT_APP_API_URL=http://localhost:3001
|
||||
volumes:
|
||||
- ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
mailhog:
|
||||
image: mailhog/mailhog:latest
|
||||
ports:
|
||||
- "1025:1025"
|
||||
- "8025:8025"
|
||||
@@ -0,0 +1,14 @@
|
||||
# Production Environment Configuration
|
||||
# When running behind a reverse proxy like Traefik, use relative URLs
|
||||
|
||||
# Backend API URL
|
||||
# For production behind reverse proxy, use relative URL:
|
||||
VITE_API_URL=/api
|
||||
|
||||
# For development or if frontend/backend are on different domains:
|
||||
# VITE_API_URL=https://api.yourdomain.com
|
||||
|
||||
# Umami Analytics Configuration (optional)
|
||||
# VITE_UMAMI_URL=https://analytics.yourdomain.com
|
||||
# VITE_UMAMI_WEBSITE_ID=your-website-id-from-umami
|
||||
# VITE_UMAMI_SHARE_URL=https://analytics.yourdomain.com/share/your-share-id/photo-sharing
|
||||
@@ -79,6 +79,20 @@ server {
|
||||
proxy_cache_valid 404 1m;
|
||||
}
|
||||
|
||||
# Uploads serving proxy (logos, favicons, watermarks)
|
||||
location /uploads {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Cache uploads
|
||||
proxy_cache_valid 200 302 7d;
|
||||
proxy_cache_valid 404 1m;
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.8",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@tiptap/extension-link": "^2.25.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -27,6 +27,7 @@ import { AdminLayout, AdminAuthWrapper } from './components/admin';
|
||||
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon } from './components/common';
|
||||
import { MaintenanceWrapper } from './components/MaintenanceWrapper';
|
||||
import { GlobalThemeProvider } from './components/GlobalThemeProvider';
|
||||
import { getApiBaseUrl } from './utils/url';
|
||||
|
||||
// Create a client
|
||||
const queryClient = new QueryClient({
|
||||
@@ -48,7 +49,7 @@ function App() {
|
||||
if (umamiUrl && umamiWebsiteId) {
|
||||
try {
|
||||
// Fetch public settings to check if analytics is enabled
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}/api/public/settings`);
|
||||
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||
const settings = await response.json();
|
||||
|
||||
// Only initialize if analytics is enabled in settings
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AlertTriangle } from 'lucide-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { api } from '../config/api';
|
||||
import { buildResourceUrl } from '../utils/url';
|
||||
|
||||
interface BrandingSettings {
|
||||
branding_company_name?: string;
|
||||
@@ -50,7 +51,7 @@ export const MaintenanceMode: React.FC = () => {
|
||||
src={settings?.branding_logo_url ?
|
||||
(settings.branding_logo_url.startsWith('http')
|
||||
? settings.branding_logo_url
|
||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settings.branding_logo_url}`)
|
||||
: buildResourceUrl(settings.branding_logo_url))
|
||||
: '/picpeak-logo-transparent.png'
|
||||
}
|
||||
alt={settings?.branding_company_name || 'PicPeak'}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Card, Input } from '../common';
|
||||
import { GALLERY_THEME_PRESETS, type ThemeConfig } from '../../contexts/ThemeContext';
|
||||
import { settingsService } from '../../services/settings.service';
|
||||
import { toast } from 'react-toastify';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
|
||||
interface ThemeCustomizerProps {
|
||||
value: ThemeConfig;
|
||||
@@ -269,7 +270,7 @@ export const ThemeCustomizer: React.FC<ThemeCustomizerProps> = ({
|
||||
<div className="flex items-center gap-4">
|
||||
{localTheme.logoUrl && (
|
||||
<img
|
||||
src={localTheme.logoUrl.startsWith('http') ? localTheme.logoUrl : `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${localTheme.logoUrl}`}
|
||||
src={localTheme.logoUrl.startsWith('http') ? localTheme.logoUrl : buildResourceUrl(localTheme.logoUrl)}
|
||||
alt="Custom logo"
|
||||
className="h-16 w-auto object-contain"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { getAuthToken } from '../../config/api';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
|
||||
interface AuthenticatedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
src: string;
|
||||
@@ -60,9 +61,8 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
// Use the src as-is since it should already be the correct endpoint
|
||||
let imageUrl = src;
|
||||
|
||||
// Prepend API URL for absolute paths
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3001';
|
||||
const fullImageUrl = imageUrl.startsWith('/') ? `${apiUrl}${imageUrl}` : imageUrl;
|
||||
// Build full URL for the image
|
||||
const fullImageUrl = imageUrl.startsWith('/') ? buildResourceUrl(imageUrl) : imageUrl;
|
||||
|
||||
console.log('Fetching authenticated image:', fullImageUrl);
|
||||
const response = await fetch(fullImageUrl, {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getApiBaseUrl, buildResourceUrl } from '../../utils/url';
|
||||
|
||||
export const DynamicFavicon: React.FC = () => {
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['public-settings'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}/api/public/settings`);
|
||||
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
@@ -30,7 +31,7 @@ export const DynamicFavicon: React.FC = () => {
|
||||
link.type = 'image/png';
|
||||
link.href = settings.branding_favicon_url.startsWith('http')
|
||||
? settings.branding_favicon_url
|
||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settings.branding_favicon_url}`;
|
||||
: buildResourceUrl(settings.branding_favicon_url);
|
||||
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReCAPTCHA from 'react-google-recaptcha';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getApiBaseUrl } from '../../utils/url';
|
||||
|
||||
interface ReCaptchaProps {
|
||||
onChange: (token: string | null) => void;
|
||||
@@ -20,7 +21,7 @@ export const ReCaptcha: React.FC<ReCaptchaProps> = ({
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['public-settings'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}/api/public/settings`);
|
||||
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||
return response.json();
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
||||
import { Button } from '../common';
|
||||
import { DynamicFavicon } from '../common/DynamicFavicon';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
|
||||
interface GalleryLayoutProps {
|
||||
event: {
|
||||
@@ -122,7 +123,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
||||
<div className="flex-shrink-0">
|
||||
<img
|
||||
src={brandingSettings?.logo_url ?
|
||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.logo_url}` :
|
||||
buildResourceUrl(brandingSettings.logo_url) :
|
||||
'/picpeak-logo-transparent.png'
|
||||
}
|
||||
alt={brandingSettings?.company_name || 'PicPeak'}
|
||||
@@ -277,7 +278,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
||||
<div className="mb-6">
|
||||
<img
|
||||
src={brandingSettings?.logo_url ?
|
||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.logo_url}` :
|
||||
buildResourceUrl(brandingSettings.logo_url) :
|
||||
'/picpeak-logo-transparent.png'
|
||||
}
|
||||
alt={brandingSettings?.company_name || 'PicPeak'}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { AuthenticatedImage } from '../../common';
|
||||
import type { BaseGalleryLayoutProps } from './BaseGalleryLayout';
|
||||
import type { Photo } from '../../../types';
|
||||
import { buildResourceUrl } from '../../../utils/url';
|
||||
|
||||
interface HeroGalleryLayoutProps extends BaseGalleryLayoutProps {
|
||||
eventName?: string;
|
||||
@@ -97,7 +98,7 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
<div className="mb-6">
|
||||
<img
|
||||
src={eventLogo ?
|
||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${eventLogo}` :
|
||||
buildResourceUrl(eventLogo) :
|
||||
'/picpeak-logo-transparent.png'
|
||||
}
|
||||
alt="Event logo"
|
||||
|
||||
@@ -14,7 +14,7 @@ export const setMaintenanceModeCallback = (callback: (enabled: boolean) => void)
|
||||
|
||||
// Create axios instance
|
||||
export const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001',
|
||||
baseURL: import.meta.env.VITE_API_URL || '/api',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { setMaintenanceModeCallback } from '../config/api';
|
||||
import { getApiBaseUrl } from '../utils/url';
|
||||
|
||||
interface MaintenanceContextType {
|
||||
isMaintenanceMode: boolean;
|
||||
@@ -29,7 +30,7 @@ export const MaintenanceProvider: React.FC<MaintenanceProviderProps> = ({ childr
|
||||
queryKey: ['public-settings-maintenance'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}/api/public/settings`);
|
||||
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||
if (response.status === 503) {
|
||||
setIsMaintenanceMode(true);
|
||||
return null;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { GalleryView } from '../components/gallery';
|
||||
import { analyticsService } from '../services/analytics.service';
|
||||
import { api } from '../config/api';
|
||||
import { GALLERY_THEME_PRESETS } from '../types/theme.types';
|
||||
import { buildResourceUrl } from '../utils/url';
|
||||
|
||||
export const GalleryPage: React.FC = () => {
|
||||
const { slug, token } = useParams<{ slug: string; token?: string }>();
|
||||
@@ -164,7 +165,7 @@ export const GalleryPage: React.FC = () => {
|
||||
{settingsData?.branding_logo_url && (
|
||||
<div className="p-8 text-center">
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settingsData.branding_logo_url}`}
|
||||
src={buildResourceUrl(settingsData.branding_logo_url)}
|
||||
alt={settingsData.branding_company_name || 'Company Logo'}
|
||||
className="h-16 w-auto object-contain mx-auto"
|
||||
/>
|
||||
@@ -220,7 +221,7 @@ export const GalleryPage: React.FC = () => {
|
||||
{settingsData?.branding_logo_url && (
|
||||
<div className="p-8 text-center">
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settingsData.branding_logo_url}`}
|
||||
src={buildResourceUrl(settingsData.branding_logo_url)}
|
||||
alt={settingsData.branding_company_name || 'Company Logo'}
|
||||
className="h-16 w-auto object-contain mx-auto"
|
||||
/>
|
||||
@@ -282,7 +283,7 @@ export const GalleryPage: React.FC = () => {
|
||||
<div className="text-center mb-4 sm:mb-6">
|
||||
<img
|
||||
src={settingsData?.branding_logo_url ?
|
||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settingsData.branding_logo_url}` :
|
||||
buildResourceUrl(settingsData.branding_logo_url) :
|
||||
'/picpeak-logo-transparent.png'
|
||||
}
|
||||
alt={settingsData?.branding_company_name || 'PicPeak'}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTheme, type ThemeConfig, GALLERY_THEME_PRESETS } from '../../context
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { settingsService, type BrandingSettings } from '../../services/settings.service';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
|
||||
export const BrandingPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -279,7 +280,7 @@ export const BrandingPage: React.FC = () => {
|
||||
{brandingSettings.favicon_url && (
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={brandingSettings.favicon_url.startsWith('http') ? brandingSettings.favicon_url : `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.favicon_url}`}
|
||||
src={brandingSettings.favicon_url.startsWith('http') ? brandingSettings.favicon_url : buildResourceUrl(brandingSettings.favicon_url)}
|
||||
alt="Current favicon"
|
||||
className="w-8 h-8"
|
||||
/>
|
||||
@@ -344,7 +345,7 @@ export const BrandingPage: React.FC = () => {
|
||||
{brandingSettings.watermark_logo_url && (
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={brandingSettings.watermark_logo_url.startsWith('http') ? brandingSettings.watermark_logo_url : `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.watermark_logo_url}`}
|
||||
src={brandingSettings.watermark_logo_url.startsWith('http') ? brandingSettings.watermark_logo_url : buildResourceUrl(brandingSettings.watermark_logo_url)}
|
||||
alt="Current watermark"
|
||||
className="h-16 w-auto object-contain bg-neutral-100 p-2 rounded"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Utility functions for URL handling in production environments
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the base API URL, preferring relative URLs for production
|
||||
* @returns The API base URL
|
||||
*/
|
||||
export const getApiBaseUrl = (): string => {
|
||||
// If VITE_API_URL is explicitly set, use it
|
||||
if (import.meta.env.VITE_API_URL && import.meta.env.VITE_API_URL !== '/api') {
|
||||
return import.meta.env.VITE_API_URL;
|
||||
}
|
||||
|
||||
// In production, use relative URL
|
||||
return '/api';
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a full URL for resources (images, files, etc.)
|
||||
* In production, this will use the current origin
|
||||
* @param path - The resource path
|
||||
* @returns The full URL
|
||||
*/
|
||||
export const buildResourceUrl = (path: string): string => {
|
||||
// Remove leading slash if present
|
||||
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
||||
|
||||
// If we have an explicit API URL that's not relative, use it
|
||||
const apiUrl = import.meta.env.VITE_API_URL;
|
||||
if (apiUrl && apiUrl !== '/api' && apiUrl.startsWith('http')) {
|
||||
const baseUrl = apiUrl.replace(/\/api\/?$/, ''); // Remove /api suffix if present
|
||||
return `${baseUrl}/${cleanPath}`;
|
||||
}
|
||||
|
||||
// In production (relative API), use current origin
|
||||
return `${window.location.origin}/${cleanPath}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if we're in production mode (using relative URLs)
|
||||
* @returns True if in production mode
|
||||
*/
|
||||
export const isProductionMode = (): boolean => {
|
||||
return !import.meta.env.VITE_API_URL || import.meta.env.VITE_API_URL === '/api';
|
||||
};
|
||||
Reference in New Issue
Block a user