Compare commits

...

9 Commits

Author SHA1 Message Date
Gitea Actions Bot d1033cb83a chore: bump version to 1.0.6
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-13 20:04:46 +00:00
paul b7458b5a37 fix: add missing pg dependency for PostgreSQL support
Test and Lint / backend-test (push) Successful in 1m10s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m7s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s
- Add pg package required for production PostgreSQL connections
- Fixes "Cannot find module 'pg'" error in production deployment

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 22:00:41 +02:00
Gitea Actions Bot face8f1496 chore: bump version to 1.0.5
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-13 19:54:25 +00:00
paul ba6ee55bf7 chore: clean up obsolete files and documentation
Test and Lint / backend-test (push) Successful in 1m5s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 39s
Version and Release / trigger-drone (push) Successful in 3s
- Remove completed auth migration documentation (13 files)
- Delete unused test and one-time scripts (23 files)
- Remove backup files and old logs
- Clean up duplicate/empty database files
- Remove old migration backup file
- Delete root level setup scripts

Total: ~185KB of obsolete files removed
All active functionality preserved

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 21:50:18 +02:00
Gitea Actions Bot e343106af5 chore: bump version to 1.0.4
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-13 19:28:44 +00:00
paul 2f848eb602 fix: make create-admin script executable
Test and Lint / backend-test (push) Successful in 1m7s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m7s
Version and Release / version-bump (push) Successful in 37s
Version and Release / trigger-drone (push) Successful in 4s
2025-07-13 21:24:40 +02:00
paul 1c7fa781ad fix: configure PostgreSQL for production and clean up deployment
- Fix database configuration to use PostgreSQL in production
- Add knexfile.js to support both SQLite (dev) and PostgreSQL (prod)
- Create admin user creation script (scripts/create-admin.js)
- Clean up docker-compose files:
  - Remove redundant docker-compose.yml and docker-compose.local.yml
  - Create docker-compose.dev.yml for development
  - Update docker-compose.prod.yml with proper DB configuration
- Clean up environment files:
  - Update .env.example for development
  - Update .env.production.example with proper settings
  - Remove redundant .env.local
- Update backend .env.example with database configuration options
- Create comprehensive DEPLOYMENT.md with admin setup instructions
- Fix production database name consistency (picpeak instead of photoapp)
2025-07-13 21:24:40 +02:00
Gitea Actions Bot 66940c2f5b chore: bump version to 1.0.3
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-13 19:18:49 +00:00
paul a3638fe954 fix: remove hardcoded localhost URLs for production deployment
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m17s
Version and Release / version-bump (push) Successful in 43s
Version and Release / trigger-drone (push) Successful in 3s
- Add URL utility functions for building resource URLs
- Update all components to use relative URLs in production
- Add production deployment documentation
- Update nginx config to proxy all required endpoints
- Add .env.production.example with proper configuration
2025-07-13 21:14:26 +02:00
77 changed files with 923 additions and 5287 deletions
+28 -20
View File
@@ -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
View File
@@ -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
View File
@@ -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
+98
View File
@@ -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
+10 -1
View File
@@ -24,10 +24,19 @@ ARCHIVE_PATH=./storage/events/archived
# Logging
LOG_LEVEL=info
# Database (for production, consider PostgreSQL)
# Database Configuration
# Development: Use SQLite
DATABASE_CLIENT=sqlite3
DATABASE_PATH=./data/photo_sharing.db
# Production: Use PostgreSQL
# DATABASE_CLIENT=pg
# DB_HOST=localhost
# DB_PORT=5432
# DB_USER=picpeak
# DB_PASSWORD=your-secure-password
# DB_NAME=picpeak
# Umami Analytics (optional)
UMAMI_URL=
UMAMI_WEBSITE_ID=
-137
View File
@@ -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
-64
View File
@@ -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)
-216
View File
@@ -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
-221
View File
@@ -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)"
```
-187
View File
@@ -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
-119
View File
@@ -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"
```
-232
View File
@@ -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
-114
View File
@@ -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!
-215
View File
@@ -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.
-167
View File
@@ -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.
-158
View File
@@ -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
-94
View File
@@ -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
-64
View File
@@ -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
View File
Binary file not shown.
View File
View File
View File
+46
View File
@@ -0,0 +1,46 @@
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 || 'postgres',
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'
},
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();
+140 -2
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-backend",
"version": "1.0.2",
"version": "1.0.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-backend",
"version": "1.0.2",
"version": "1.0.6",
"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",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "picpeak-backend",
"version": "1.0.2",
"version": "1.0.6",
"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",
-80
View File
@@ -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();
-94
View File
@@ -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();
-96
View File
@@ -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');
}
});
+77
View File
@@ -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();
-56
View File
@@ -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();
-81
View File
@@ -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.');
}
});
});
-56
View File
@@ -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.');
}
});
});
-132
View File
@@ -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"
-37
View File
@@ -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();
-136
View File
@@ -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);
});
-269
View File
@@ -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();
-47
View File
@@ -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();
-55
View File
@@ -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();
-146
View File
@@ -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!"
-112
View File
@@ -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);
}
-146
View File
@@ -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();
-98
View File
@@ -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);
});
-108
View File
@@ -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);
}
-59
View File
@@ -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();
-106
View File
@@ -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();
-80
View File
@@ -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');
-32
View File
@@ -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')
});
}
});
-167
View File
@@ -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
View File
@@ -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 };
-48
View File
@@ -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();
+61
View File
@@ -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
-109
View File
@@ -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:
+11 -2
View File
@@ -14,12 +14,21 @@ services:
- 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}
volumes:
@@ -70,7 +79,7 @@ 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}
volumes:
@@ -82,7 +91,7 @@ services:
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:
-49
View File
@@ -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"
+14
View File
@@ -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
+14
View File
@@ -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;
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-frontend",
"version": "1.0.2",
"version": "1.0.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-frontend",
"version": "1.0.2",
"version": "1.0.6",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"@tiptap/extension-link": "^2.25.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "picpeak-frontend",
"private": true,
"version": "1.0.2",
"version": "1.0.6",
"type": "module",
"scripts": {
"dev": "vite",
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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);
}
+2 -1
View File
@@ -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"
+1 -1
View File
@@ -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',
},
+2 -1
View File
@@ -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;
+4 -3
View File
@@ -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'}
+3 -2
View File
@@ -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"
/>
+46
View File
@@ -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';
};