Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e343106af5 | |||
| 2f848eb602 | |||
| 1c7fa781ad | |||
| 66940c2f5b | |||
| a3638fe954 |
+28
-20
@@ -1,26 +1,34 @@
|
||||
# JWT Secret for authentication
|
||||
# IMPORTANT: Generate a secure random secret with: openssl rand -hex 32
|
||||
# NEVER use the default value or commit the actual secret to version control
|
||||
JWT_SECRET=CHANGE_ME_TO_A_64_CHARACTER_SECURE_RANDOM_STRING_GENERATED_BY_OPENSSL
|
||||
# Environment Configuration Template
|
||||
# Copy this file to .env and adjust values for your environment
|
||||
|
||||
# URLs
|
||||
ADMIN_URL=https://admin.photos.yourdomain.com
|
||||
FRONTEND_URL=https://photos.yourdomain.com
|
||||
# Development: Use docker-compose.dev.yml
|
||||
# Production: Use docker-compose.prod.yml with .env.production.example
|
||||
|
||||
# Database (for PostgreSQL in production)
|
||||
DB_USER=photoapp
|
||||
DB_PASSWORD=secure-password-here
|
||||
DB_NAME=photo_sharing
|
||||
# JWT Secret (CRITICAL for production)
|
||||
# Generate with: openssl rand -base64 32
|
||||
JWT_SECRET=dev-secret-change-in-production
|
||||
|
||||
# Application URLs
|
||||
ADMIN_URL=http://localhost:3005
|
||||
FRONTEND_URL=http://localhost:3005
|
||||
|
||||
# Database Configuration
|
||||
# SQLite is used for development by default
|
||||
# For production PostgreSQL config, see .env.production.example
|
||||
DATABASE_CLIENT=sqlite3
|
||||
DATABASE_PATH=./data/photo_sharing.db
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
# Development: Uses Mailhog (included in docker-compose.dev.yml)
|
||||
# Production: Configure real SMTP server
|
||||
SMTP_HOST=mailhog
|
||||
SMTP_PORT=1025
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
EMAIL_FROM=noreply@localhost
|
||||
|
||||
# Umami Analytics
|
||||
UMAMI_URL=https://analytics.yourdomain.com
|
||||
UMAMI_WEBSITE_ID=your-website-id
|
||||
UMAMI_HASH_SALT=random-salt-here
|
||||
# Optional: Umami Analytics
|
||||
UMAMI_URL=
|
||||
UMAMI_WEBSITE_ID=
|
||||
UMAMI_HASH_SALT=
|
||||
+18
-46
@@ -1,60 +1,32 @@
|
||||
# Production Environment Configuration Template
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# Application URLs
|
||||
FRONTEND_HOST=photos.yourdomain.com
|
||||
BACKEND_HOST=api.photos.yourdomain.com
|
||||
ADMIN_URL=https://admin.photos.yourdomain.com
|
||||
FRONTEND_URL=https://photos.yourdomain.com
|
||||
ADMIN_URL=https://yourdomain.com
|
||||
FRONTEND_URL=https://yourdomain.com
|
||||
|
||||
# Database Configuration
|
||||
DB_NAME=photo_sharing
|
||||
DB_USER=photoapp
|
||||
DB_PASSWORD=your-secure-password-here
|
||||
# Security - CRITICAL: Generate a secure random JWT secret
|
||||
# You can generate one with: openssl rand -base64 32
|
||||
JWT_SECRET=your-secure-random-jwt-secret-here
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-jwt-secret-here
|
||||
# Database Configuration (PostgreSQL)
|
||||
DB_USER=picpeak
|
||||
DB_PASSWORD=your-secure-database-password
|
||||
DB_NAME=picpeak
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_SECURE=true
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
|
||||
# Umami Analytics
|
||||
# Umami Analytics (Optional)
|
||||
UMAMI_URL=https://analytics.yourdomain.com
|
||||
UMAMI_HOST=analytics.yourdomain.com
|
||||
UMAMI_WEBSITE_ID=your-website-id
|
||||
UMAMI_HASH_SALT=your-random-salt
|
||||
UMAMI_DB_PASSWORD=umami-db-password
|
||||
UMAMI_HASH_SALT=your-random-hash-salt
|
||||
|
||||
# Traefik Configuration
|
||||
TRAEFIK_HOST=traefik.yourdomain.com
|
||||
ACME_EMAIL=admin@yourdomain.com
|
||||
TRAEFIK_DASHBOARD_AUTH=admin:$2y$10$... # Use htpasswd to generate
|
||||
|
||||
# Docker Registry (optional)
|
||||
REGISTRY_URL=registry.yourdomain.com
|
||||
VERSION=latest
|
||||
|
||||
# Monitoring
|
||||
DOMAIN=yourdomain.com
|
||||
GRAFANA_USER=admin
|
||||
GRAFANA_PASSWORD=your-grafana-password
|
||||
|
||||
# OAuth Configuration (optional)
|
||||
OAUTH_AUTH_URL=https://auth.yourdomain.com/oauth2/auth
|
||||
OAUTH_TOKEN_URL=https://auth.yourdomain.com/oauth2/token
|
||||
OAUTH_USER_URL=https://auth.yourdomain.com/oauth2/userinfo
|
||||
OAUTH_CLIENT_ID=photo-sharing
|
||||
OAUTH_CLIENT_SECRET=your-oauth-secret
|
||||
OAUTH_SECRET=your-random-secret
|
||||
COOKIE_DOMAIN=.yourdomain.com
|
||||
OAUTH_WHITELIST=admin@yourdomain.com
|
||||
|
||||
# Backup Configuration (optional)
|
||||
S3_BACKUP_BUCKET=your-backup-bucket
|
||||
|
||||
# Drone CI Configuration
|
||||
DRONE_RPC_SECRET=your-drone-secret
|
||||
DRONE_GITHUB_CLIENT_ID=your-github-client-id
|
||||
DRONE_GITHUB_CLIENT_SECRET=your-github-client-secret
|
||||
# First Admin User (for initial setup)
|
||||
# Run: docker-compose exec backend node scripts/create-admin.js --email admin@yourdomain.com
|
||||
ADMIN_EMAIL=admin@yourdomain.com
|
||||
+287
-424
@@ -1,483 +1,346 @@
|
||||
# Photo Sharing Platform - Production Deployment Guide
|
||||
# PicPeak Deployment Guide
|
||||
|
||||
This guide covers deploying the photo sharing platform using Docker Swarm, Traefik, and Drone CI/CD.
|
||||
This guide covers deploying PicPeak for development and production environments.
|
||||
|
||||
## Table of Contents
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Infrastructure Setup](#infrastructure-setup)
|
||||
- [Docker Swarm Setup](#docker-swarm-setup)
|
||||
- [Traefik Setup](#traefik-setup)
|
||||
- [Application Deployment](#application-deployment)
|
||||
- [CI/CD with Drone](#cicd-with-drone)
|
||||
- [Monitoring](#monitoring)
|
||||
- [Backup and Recovery](#backup-and-recovery)
|
||||
- [Quick Start (Development)](#quick-start-development)
|
||||
- [Production Deployment](#production-deployment)
|
||||
- [Admin User Setup](#admin-user-setup)
|
||||
- [Configuration Reference](#configuration-reference)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Prerequisites
|
||||
## Quick Start (Development)
|
||||
|
||||
### Hardware Requirements
|
||||
- **Manager Node**: 2 CPU cores, 4GB RAM, 50GB storage
|
||||
- **Worker Nodes**: 2 CPU cores, 2GB RAM, 20GB storage
|
||||
- **Storage**: SSD recommended for database and photo storage
|
||||
|
||||
### Software Requirements
|
||||
- Ubuntu 20.04+ or similar Linux distribution
|
||||
- Docker Engine 20.10+
|
||||
- Docker Compose 2.0+
|
||||
- Git
|
||||
- SSL certificates (automated with Let's Encrypt)
|
||||
|
||||
### Network Requirements
|
||||
- Ports 80, 443 open for web traffic
|
||||
- Port 2377 for Swarm management
|
||||
- Ports 7946, 4789 for Swarm networking
|
||||
- Static IP or reliable dynamic DNS
|
||||
|
||||
## Infrastructure Setup
|
||||
|
||||
### 1. Install Docker
|
||||
### 1. Clone and Setup
|
||||
|
||||
```bash
|
||||
# Install Docker
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
git clone https://github.com/yourusername/picpeak.git
|
||||
cd picpeak
|
||||
|
||||
# Add user to docker group
|
||||
sudo usermod -aG docker $USER
|
||||
|
||||
# Enable Docker service
|
||||
sudo systemctl enable docker
|
||||
sudo systemctl start docker
|
||||
```
|
||||
|
||||
### 2. Configure Firewall
|
||||
|
||||
```bash
|
||||
# Allow Docker Swarm ports
|
||||
sudo ufw allow 2377/tcp
|
||||
sudo ufw allow 7946/tcp
|
||||
sudo ufw allow 7946/udp
|
||||
sudo ufw allow 4789/udp
|
||||
|
||||
# Allow web traffic
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 443/tcp
|
||||
```
|
||||
|
||||
## Docker Swarm Setup
|
||||
|
||||
### 1. Initialize Swarm
|
||||
|
||||
On the manager node:
|
||||
|
||||
```bash
|
||||
cd deploy/scripts
|
||||
sudo ./init-swarm.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
- Initialize Docker Swarm
|
||||
- Create overlay networks
|
||||
- Label nodes for service placement
|
||||
- Create required directories
|
||||
|
||||
### 2. Join Worker Nodes
|
||||
|
||||
On each worker node, run the join command displayed by the init script:
|
||||
|
||||
```bash
|
||||
docker swarm join --token SWMTKN-1-xxx... manager-ip:2377
|
||||
```
|
||||
|
||||
### 3. Verify Swarm
|
||||
|
||||
```bash
|
||||
docker node ls
|
||||
```
|
||||
|
||||
## Application Configuration
|
||||
|
||||
### 1. Environment Setup
|
||||
|
||||
```bash
|
||||
# Copy environment template
|
||||
cp .env.production.example .env.production
|
||||
cp .env.example .env
|
||||
|
||||
# Edit with your values
|
||||
nano .env.production
|
||||
```
|
||||
|
||||
Required configurations:
|
||||
- Domain names for frontend, backend, and services
|
||||
- SMTP credentials for email
|
||||
- Database passwords
|
||||
- JWT secrets
|
||||
|
||||
### 2. Create Docker Secrets
|
||||
|
||||
```bash
|
||||
cd deploy/scripts
|
||||
./create-secrets.sh
|
||||
```
|
||||
|
||||
This will create all required secrets in Docker Swarm. Save the generated passwords!
|
||||
|
||||
## Traefik Setup
|
||||
|
||||
### 1. Deploy Traefik
|
||||
|
||||
```bash
|
||||
cd deploy/traefik
|
||||
|
||||
# Create traefik network
|
||||
docker network create --driver overlay traefik-public
|
||||
|
||||
# Deploy Traefik stack
|
||||
docker stack deploy -c docker-compose.traefik.yml traefik
|
||||
```
|
||||
|
||||
### 2. Verify Traefik
|
||||
|
||||
```bash
|
||||
# Check service status
|
||||
docker service ls | grep traefik
|
||||
|
||||
# View logs
|
||||
docker service logs traefik_traefik
|
||||
```
|
||||
|
||||
Access Traefik dashboard at: `https://traefik.yourdomain.com/dashboard/`
|
||||
|
||||
## Application Deployment
|
||||
|
||||
### 1. Build Images (if using local registry)
|
||||
|
||||
```bash
|
||||
# Build frontend
|
||||
cd frontend
|
||||
docker build -t photo-sharing-frontend:latest .
|
||||
|
||||
# Build backend
|
||||
cd ../backend
|
||||
docker build -t photo-sharing-backend:latest .
|
||||
```
|
||||
|
||||
### 2. Deploy Application Stack
|
||||
|
||||
```bash
|
||||
cd deploy/scripts
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--env FILE`: Specify environment file
|
||||
- `--registry URL`: Docker registry URL
|
||||
- `--version VERSION`: Image version to deploy
|
||||
|
||||
### 3. Verify Deployment
|
||||
|
||||
```bash
|
||||
# Check all services
|
||||
docker service ls
|
||||
|
||||
# Check specific service
|
||||
docker service ps photo-sharing_backend
|
||||
|
||||
# View logs
|
||||
docker service logs photo-sharing_backend -f
|
||||
```
|
||||
|
||||
### 4. Run Database Migrations
|
||||
|
||||
The deploy script automatically runs migrations, but you can run manually:
|
||||
|
||||
```bash
|
||||
docker exec $(docker ps -q -f name=photo-sharing_backend) npm run migrate
|
||||
```
|
||||
|
||||
## CI/CD with Drone
|
||||
|
||||
### 1. Drone Server Setup
|
||||
|
||||
Deploy Drone server on your CI infrastructure:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
--volume=/var/lib/drone:/data \
|
||||
--env=DRONE_GITHUB_CLIENT_ID=your-id \
|
||||
--env=DRONE_GITHUB_CLIENT_SECRET=your-secret \
|
||||
--env=DRONE_RPC_SECRET=your-rpc-secret \
|
||||
--env=DRONE_SERVER_HOST=drone.yourdomain.com \
|
||||
--env=DRONE_SERVER_PROTO=https \
|
||||
--publish=80:80 \
|
||||
--publish=443:443 \
|
||||
--restart=always \
|
||||
--detach=true \
|
||||
--name=drone \
|
||||
drone/drone:2
|
||||
```
|
||||
|
||||
### 2. Drone Runner Setup
|
||||
|
||||
On build servers:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e DRONE_RPC_PROTO=https \
|
||||
-e DRONE_RPC_HOST=drone.yourdomain.com \
|
||||
-e DRONE_RPC_SECRET=your-rpc-secret \
|
||||
-e DRONE_RUNNER_CAPACITY=2 \
|
||||
-e DRONE_RUNNER_NAME=runner-1 \
|
||||
-p 3000:3000 \
|
||||
--restart always \
|
||||
--name runner \
|
||||
drone/drone-runner-docker:1
|
||||
```
|
||||
|
||||
### 3. Repository Setup
|
||||
|
||||
1. Enable repository in Drone UI
|
||||
2. Add secrets in Drone:
|
||||
- `docker_username`
|
||||
- `docker_password`
|
||||
- `docker_registry`
|
||||
- `staging_swarm_host`
|
||||
- `staging_swarm_user`
|
||||
- `staging_swarm_key`
|
||||
- `prod_swarm_host`
|
||||
- `prod_swarm_user`
|
||||
- `prod_swarm_key`
|
||||
- `slack_webhook`
|
||||
|
||||
### 4. Deployment Workflow
|
||||
|
||||
- Push to `develop` → Deploy to staging
|
||||
- Create tag → Deploy to production
|
||||
- Automatic rollback on failure
|
||||
|
||||
## Monitoring
|
||||
|
||||
### 1. Deploy Monitoring Stack
|
||||
|
||||
```bash
|
||||
cd deploy/monitoring
|
||||
|
||||
# Deploy monitoring services
|
||||
docker stack deploy -c docker-compose.monitoring.yml monitoring
|
||||
# Start development environment
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
### 2. Access Services
|
||||
|
||||
- Grafana: `https://grafana.yourdomain.com`
|
||||
- Prometheus: `https://prometheus.yourdomain.com`
|
||||
- Alertmanager: `https://alerts.yourdomain.com`
|
||||
- Frontend: http://localhost:3005
|
||||
- Backend API: http://localhost:3001
|
||||
- MailHog (email testing): http://localhost:8025
|
||||
|
||||
### 3. Configure Alerts
|
||||
### 3. Create Admin User
|
||||
|
||||
Create alert rules in `deploy/monitoring/alerts/`:
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml exec backend node scripts/create-admin.js \
|
||||
--email admin@localhost \
|
||||
--username admin \
|
||||
--password admin123
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- Domain with DNS configured
|
||||
- SSL/TLS handled by reverse proxy (Traefik, Nginx, etc.)
|
||||
|
||||
### 1. Environment Setup
|
||||
|
||||
```bash
|
||||
# Copy production template
|
||||
cp .env.production.example .env
|
||||
|
||||
# Generate secure secrets
|
||||
echo "JWT_SECRET=$(openssl rand -base64 32)" >> .env
|
||||
echo "DB_PASSWORD=$(openssl rand -base64 24)" >> .env
|
||||
```
|
||||
|
||||
Edit `.env` with your configuration:
|
||||
|
||||
```env
|
||||
# Your domain
|
||||
ADMIN_URL=https://yourdomain.com
|
||||
FRONTEND_URL=https://yourdomain.com
|
||||
|
||||
# Database (PostgreSQL)
|
||||
DB_USER=picpeak
|
||||
DB_NAME=picpeak
|
||||
# DB_PASSWORD already generated above
|
||||
|
||||
# Email
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=true
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
### 2. Frontend Configuration
|
||||
|
||||
```bash
|
||||
# Configure frontend for production
|
||||
echo "VITE_API_URL=/api" > frontend/.env.production
|
||||
```
|
||||
|
||||
### 3. Deploy with Docker Compose
|
||||
|
||||
```bash
|
||||
# Build and start services
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# Check status
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# View logs
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
```
|
||||
|
||||
### 4. Deploy with Traefik
|
||||
|
||||
If using Traefik, create `docker-compose.override.yml`:
|
||||
|
||||
```yaml
|
||||
groups:
|
||||
- name: photo-sharing
|
||||
rules:
|
||||
- alert: ServiceDown
|
||||
expr: up{job="photo-sharing-backend"} == 0
|
||||
for: 5m
|
||||
annotations:
|
||||
summary: "Photo sharing backend is down"
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.picpeak.rule=Host(`yourdomain.com`)"
|
||||
- "traefik.http.routers.picpeak.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak.loadbalancer.server.port=80"
|
||||
networks:
|
||||
- traefik
|
||||
- picpeak
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
```
|
||||
|
||||
## Backup and Recovery
|
||||
## Admin User Setup
|
||||
|
||||
### 1. Automated Backups
|
||||
### Create First Admin
|
||||
|
||||
Set up cron job for automated backups:
|
||||
After deployment, create your admin user:
|
||||
|
||||
```bash
|
||||
# Edit crontab
|
||||
crontab -e
|
||||
# Production
|
||||
docker-compose -f docker-compose.prod.yml exec backend node scripts/create-admin.js \
|
||||
--email admin@yourdomain.com \
|
||||
--username admin \
|
||||
--password yourSecurePassword
|
||||
|
||||
# Add daily backup at 2 AM
|
||||
0 2 * * * /opt/photo-sharing/deploy/scripts/backup.sh
|
||||
# Auto-generate password
|
||||
docker-compose -f docker-compose.prod.yml exec backend node scripts/create-admin.js \
|
||||
--email admin@yourdomain.com
|
||||
```
|
||||
|
||||
### 2. Manual Backup
|
||||
The script will display:
|
||||
- ✅ Admin user created successfully!
|
||||
- Email: admin@yourdomain.com
|
||||
- Username: admin
|
||||
- Login URL: https://yourdomain.com/admin/login
|
||||
- Password: (save this if auto-generated!)
|
||||
|
||||
### Managing Admin Users
|
||||
|
||||
```bash
|
||||
cd deploy/scripts
|
||||
./backup.sh
|
||||
# List admin users
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||
-c "SELECT id, username, email, is_active, last_login FROM admin_users;"
|
||||
|
||||
# Deactivate user
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||
-c "UPDATE admin_users SET is_active = false WHERE email = 'user@example.com';"
|
||||
```
|
||||
|
||||
### 3. Restore from Backup
|
||||
## Configuration Reference
|
||||
|
||||
### Database Configuration
|
||||
|
||||
PicPeak automatically detects the environment and uses:
|
||||
- **Development**: SQLite (`./data/photo_sharing.db`)
|
||||
- **Production**: PostgreSQL (configured via environment variables)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
#### Required for Production
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `JWT_SECRET` | JWT signing key | `openssl rand -base64 32` |
|
||||
| `DB_PASSWORD` | PostgreSQL password | `openssl rand -base64 24` |
|
||||
| `ADMIN_URL` | Admin panel URL | `https://yourdomain.com` |
|
||||
| `FRONTEND_URL` | Frontend URL | `https://yourdomain.com` |
|
||||
| `EMAIL_FROM` | Sender email | `noreply@yourdomain.com` |
|
||||
|
||||
#### Email Configuration
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `SMTP_HOST` | SMTP server | `smtp.gmail.com` |
|
||||
| `SMTP_PORT` | SMTP port | `587` |
|
||||
| `SMTP_SECURE` | Use TLS | `true` |
|
||||
| `SMTP_USER` | SMTP username | `your-email@gmail.com` |
|
||||
| `SMTP_PASS` | SMTP password | App-specific password |
|
||||
|
||||
### Storage Paths
|
||||
|
||||
- Photos: `./storage/events/active/`
|
||||
- Archives: `./storage/events/archived/`
|
||||
- Thumbnails: `./storage/thumbnails/`
|
||||
- Uploads: `./storage/uploads/`
|
||||
|
||||
## Backup and Restore
|
||||
|
||||
### Backup Database
|
||||
|
||||
```bash
|
||||
# Extract backup
|
||||
tar -xzf backup-20240615-020000.tar.gz
|
||||
# PostgreSQL backup
|
||||
docker-compose -f docker-compose.prod.yml exec db \
|
||||
pg_dump -U picpeak picpeak > backup-$(date +%Y%m%d).sql
|
||||
|
||||
# Restore database
|
||||
docker exec -i $(docker ps -q -f name=photo-sharing_db) \
|
||||
psql -U postgres photo_sharing < backup-20240615-020000/database.sql
|
||||
|
||||
# Restore photos
|
||||
tar -xzf backup-20240615-020000/photos.tar.gz -C /opt/photo-sharing/
|
||||
|
||||
# Restore volumes
|
||||
docker run --rm \
|
||||
-v photo-sharing_app-data:/data \
|
||||
-v $(pwd)/backup-20240615-020000:/backup \
|
||||
alpine tar -xzf /backup/volume-photo-sharing_app-data.tar.gz -C /data
|
||||
# Backup storage
|
||||
tar -czf storage-backup-$(date +%Y%m%d).tar.gz ./storage
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### 1. Scaling Services
|
||||
### Restore Database
|
||||
|
||||
```bash
|
||||
# Scale backend to 5 replicas
|
||||
docker service scale photo-sharing_backend=5
|
||||
# PostgreSQL restore
|
||||
docker-compose -f docker-compose.prod.yml exec -T db \
|
||||
psql -U picpeak picpeak < backup-20240115.sql
|
||||
|
||||
# Scale frontend to 3 replicas
|
||||
docker service scale photo-sharing_frontend=3
|
||||
# Restore storage
|
||||
tar -xzf storage-backup-20240115.tar.gz
|
||||
```
|
||||
|
||||
### 2. Rolling Updates
|
||||
|
||||
```bash
|
||||
# Update backend image
|
||||
docker service update \
|
||||
--image registry.yourdomain.com/photo-sharing-backend:v2.0 \
|
||||
photo-sharing_backend
|
||||
```
|
||||
|
||||
### 3. Drain Node for Maintenance
|
||||
|
||||
```bash
|
||||
# Drain node
|
||||
docker node update --availability drain worker-1
|
||||
|
||||
# Perform maintenance...
|
||||
|
||||
# Activate node
|
||||
docker node update --availability active worker-1
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Service Won't Start
|
||||
```bash
|
||||
# Check service status
|
||||
docker service ps photo-sharing_backend --no-trunc
|
||||
|
||||
# View detailed logs
|
||||
docker service logs photo-sharing_backend --details
|
||||
```
|
||||
|
||||
#### 2. Database Connection Issues
|
||||
```bash
|
||||
# Check database logs
|
||||
docker service logs photo-sharing_db
|
||||
|
||||
# Test connection
|
||||
docker exec $(docker ps -q -f name=photo-sharing_db) \
|
||||
pg_isready -U postgres
|
||||
```
|
||||
|
||||
#### 3. Traefik Certificate Issues
|
||||
```bash
|
||||
# Check Traefik logs
|
||||
docker service logs traefik_traefik | grep acme
|
||||
|
||||
# Remove and regenerate certificates
|
||||
rm -rf /opt/traefik/letsencrypt/acme.json
|
||||
docker service update --force traefik_traefik
|
||||
```
|
||||
|
||||
#### 4. Storage Issues
|
||||
```bash
|
||||
# Check disk usage
|
||||
df -h
|
||||
|
||||
# Clean up Docker
|
||||
docker system prune -a
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging:
|
||||
|
||||
```bash
|
||||
# Update service with debug logging
|
||||
docker service update \
|
||||
--env-add LOG_LEVEL=debug \
|
||||
photo-sharing_backend
|
||||
```
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Check all endpoints
|
||||
curl -f https://photos.yourdomain.com/health
|
||||
curl -f https://api.photos.yourdomain.com/api/health
|
||||
curl -f https://traefik.yourdomain.com/ping
|
||||
# Backend health
|
||||
curl https://yourdomain.com/api/health
|
||||
|
||||
# Frontend health
|
||||
curl https://yourdomain.com/health
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
### Logs
|
||||
|
||||
1. **Regular Updates**
|
||||
- Keep Docker and system packages updated
|
||||
- Update application dependencies regularly
|
||||
- Monitor security advisories
|
||||
```bash
|
||||
# All services
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
|
||||
2. **Access Control**
|
||||
- Use strong passwords for all services
|
||||
- Enable 2FA where possible
|
||||
- Restrict SSH access to specific IPs
|
||||
- Use Docker secrets for sensitive data
|
||||
# Specific service
|
||||
docker-compose -f docker-compose.prod.yml logs -f backend
|
||||
|
||||
3. **Network Security**
|
||||
- Use internal networks for service communication
|
||||
- Enable firewall rules
|
||||
- Use TLS for all external communication
|
||||
- Regular security scans with Trivy
|
||||
# Last 100 lines
|
||||
docker-compose -f docker-compose.prod.yml logs --tail=100 backend
|
||||
```
|
||||
|
||||
4. **Backup Security**
|
||||
- Encrypt backups at rest
|
||||
- Test restore procedures regularly
|
||||
- Store backups in multiple locations
|
||||
- Rotate old backups
|
||||
## Troubleshooting
|
||||
|
||||
## Performance Tuning
|
||||
### Backend Won't Start
|
||||
|
||||
1. **Database Optimization**
|
||||
```sql
|
||||
-- Add indexes for common queries
|
||||
CREATE INDEX idx_photos_event_id ON photos(event_id);
|
||||
CREATE INDEX idx_access_logs_event_id ON access_logs(event_id);
|
||||
1. Check database connection:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml logs db
|
||||
```
|
||||
|
||||
2. **Image Optimization**
|
||||
- Use CDN for static assets
|
||||
- Enable aggressive caching
|
||||
- Optimize image sizes before upload
|
||||
|
||||
3. **Service Limits**
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 1G
|
||||
reservations:
|
||||
cpus: '0.5'
|
||||
memory: 256M
|
||||
2. Verify environment variables:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend env | grep DB_
|
||||
```
|
||||
|
||||
## Support
|
||||
### Can't Login as Admin
|
||||
|
||||
For issues and questions:
|
||||
- Check logs: `docker service logs <service_name>`
|
||||
- Review documentation: [README.md](README.md)
|
||||
- Check monitoring dashboards
|
||||
- Contact: admin@yourdomain.com
|
||||
1. Verify admin user exists:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||
-c "SELECT * FROM admin_users;"
|
||||
```
|
||||
|
||||
2. Reset admin password:
|
||||
```bash
|
||||
# Create new admin with different email
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
node scripts/create-admin.js --email newadmin@yourdomain.com
|
||||
```
|
||||
|
||||
### Photos Not Loading
|
||||
|
||||
1. Check file permissions:
|
||||
```bash
|
||||
ls -la ./storage/events/active/
|
||||
```
|
||||
|
||||
2. Verify nginx proxy configuration:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec frontend \
|
||||
cat /etc/nginx/conf.d/default.conf
|
||||
```
|
||||
|
||||
### Email Not Sending
|
||||
|
||||
1. Check email configuration:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend env | grep SMTP_
|
||||
```
|
||||
|
||||
2. View email queue:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||
-c "SELECT * FROM email_queue WHERE status = 'failed';"
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Update Application
|
||||
|
||||
```bash
|
||||
# Pull latest changes
|
||||
git pull
|
||||
|
||||
# Rebuild images
|
||||
docker-compose -f docker-compose.prod.yml build
|
||||
|
||||
# Restart services
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Clean Up
|
||||
|
||||
```bash
|
||||
# Remove unused images
|
||||
docker image prune -a
|
||||
|
||||
# Clean up logs
|
||||
docker-compose -f docker-compose.prod.yml logs --tail=0 -f
|
||||
|
||||
# Remove old archives
|
||||
find ./storage/events/archived -name "*.zip" -mtime +90 -delete
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Generated secure `JWT_SECRET`
|
||||
- [ ] Generated secure `DB_PASSWORD`
|
||||
- [ ] HTTPS enabled via reverse proxy
|
||||
- [ ] Changed default admin credentials
|
||||
- [ ] Configured real SMTP server
|
||||
- [ ] Set file permissions: `chmod 600 .env`
|
||||
- [ ] Firewall configured
|
||||
- [ ] Regular backups scheduled
|
||||
- [ ] Monitoring enabled
|
||||
@@ -0,0 +1,98 @@
|
||||
# Production Deployment Guide
|
||||
|
||||
This guide explains how to deploy PicPeak in production behind a reverse proxy like Traefik.
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### Frontend Configuration
|
||||
|
||||
For production deployment behind a reverse proxy (Traefik, Nginx, etc.), the frontend should use relative URLs to automatically inherit the protocol (HTTPS) and domain.
|
||||
|
||||
1. Copy the production environment template:
|
||||
```bash
|
||||
cp frontend/.env.production.example frontend/.env.production
|
||||
```
|
||||
|
||||
2. Set the API URL to use relative path:
|
||||
```env
|
||||
# frontend/.env.production
|
||||
VITE_API_URL=/api
|
||||
```
|
||||
|
||||
This ensures all API calls will use the same domain and protocol as the frontend.
|
||||
|
||||
### Backend Configuration
|
||||
|
||||
Ensure your backend `.env` file has the correct URLs:
|
||||
```env
|
||||
# backend/.env
|
||||
FRONTEND_URL=https://yourdomain.com
|
||||
ADMIN_URL=https://yourdomain.com
|
||||
```
|
||||
|
||||
## Docker Compose Production
|
||||
|
||||
When using Docker Compose in production:
|
||||
|
||||
1. Build with production environment:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml build --build-arg NODE_ENV=production
|
||||
```
|
||||
|
||||
2. The frontend nginx configuration already includes proper proxy settings for:
|
||||
- `/api` → Backend API
|
||||
- `/photos` → Protected photo access
|
||||
- `/thumbnails` → Thumbnail images
|
||||
- `/uploads` → Public uploads (logos, favicons)
|
||||
|
||||
## Traefik Configuration
|
||||
|
||||
Example Traefik labels for docker-compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
frontend:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.picpeak.rule=Host(`yourdomain.com`)"
|
||||
- "traefik.http.routers.picpeak.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak.loadbalancer.server.port=80"
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **No Hardcoded URLs**: The application uses environment variables with relative URL fallbacks, making it production-ready.
|
||||
|
||||
2. **HTTPS Only**: When `VITE_API_URL=/api`, all requests will use the same protocol as the page (HTTPS in production).
|
||||
|
||||
3. **CORS Configuration**: The backend CORS is configured to accept requests from the URLs specified in `FRONTEND_URL` and `ADMIN_URL`.
|
||||
|
||||
4. **Static Assets**: All static assets (photos, thumbnails, uploads) are served through the nginx proxy, inheriting authentication headers.
|
||||
|
||||
## Verification
|
||||
|
||||
After deployment, verify:
|
||||
|
||||
1. Check browser console for any localhost URLs (there should be none)
|
||||
2. Verify all API calls use HTTPS
|
||||
3. Check that images load correctly with authentication
|
||||
4. Test favicon and logo display
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you see console errors about localhost:
|
||||
|
||||
1. Ensure `VITE_API_URL=/api` in frontend environment
|
||||
2. Clear browser cache
|
||||
3. Rebuild frontend with production environment:
|
||||
```bash
|
||||
cd frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
If images don't load:
|
||||
|
||||
1. Check that nginx proxy locations are configured
|
||||
2. Verify authentication tokens are being sent
|
||||
3. Check backend logs for authentication errors
|
||||
+10
-1
@@ -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=
|
||||
|
||||
@@ -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'];
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^5.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"description": "Backend for PicPeak event photo sharing platform",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script to create an admin user
|
||||
* Usage: node scripts/create-admin.js --email admin@example.com --username admin --password yourpassword
|
||||
*
|
||||
* If no password is provided, a random one will be generated and displayed
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { db } = require('../src/database/db');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const getArg = (name) => {
|
||||
const index = args.findIndex(arg => arg === `--${name}`);
|
||||
return index !== -1 && args[index + 1] ? args[index + 1] : null;
|
||||
};
|
||||
|
||||
const email = getArg('email');
|
||||
const username = getArg('username') || email?.split('@')[0] || 'admin';
|
||||
let password = getArg('password');
|
||||
|
||||
// Validate email
|
||||
if (!email) {
|
||||
console.error('Error: Email is required. Use --email admin@example.com');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate password if not provided
|
||||
if (!password) {
|
||||
password = crypto.randomBytes(12).toString('base64').slice(0, 16);
|
||||
console.log(`Generated password: ${password}`);
|
||||
console.log('Please save this password securely!');
|
||||
}
|
||||
|
||||
async function createAdmin() {
|
||||
try {
|
||||
// Check if user already exists
|
||||
const existingUser = await db('admin_users')
|
||||
.where('email', email)
|
||||
.orWhere('username', username)
|
||||
.first();
|
||||
|
||||
if (existingUser) {
|
||||
console.error(`Error: User with email "${email}" or username "${username}" already exists`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
|
||||
// Create admin user
|
||||
await db('admin_users').insert({
|
||||
username,
|
||||
email,
|
||||
password_hash: passwordHash,
|
||||
is_active: true,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
console.log(`✅ Admin user created successfully!`);
|
||||
console.log(` Email: ${email}`);
|
||||
console.log(` Username: ${username}`);
|
||||
console.log(` Login URL: ${process.env.ADMIN_URL || 'http://localhost:3000'}/admin/login`);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error creating admin user:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
createAdmin();
|
||||
+39
-40
@@ -1,13 +1,7 @@
|
||||
const knex = require('knex');
|
||||
const path = require('path');
|
||||
const knexConfig = require('../../knexfile');
|
||||
|
||||
const db = knex({
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: path.join(__dirname, '../../data/photo_sharing.db')
|
||||
},
|
||||
useNullAsDefault: true
|
||||
});
|
||||
const db = knex(knexConfig);
|
||||
|
||||
async function initializeDatabase() {
|
||||
// Events table
|
||||
@@ -35,37 +29,42 @@ async function initializeDatabase() {
|
||||
} else {
|
||||
// Check if color_theme needs to be updated to TEXT type
|
||||
// This is needed for larger theme configurations
|
||||
try {
|
||||
await db.raw(`
|
||||
CREATE TABLE IF NOT EXISTS events_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
event_name TEXT NOT NULL,
|
||||
event_date DATE NOT NULL,
|
||||
host_email TEXT NOT NULL,
|
||||
admin_email TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
welcome_message TEXT,
|
||||
color_theme TEXT,
|
||||
share_link TEXT UNIQUE NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
is_archived BOOLEAN DEFAULT 0,
|
||||
archive_path TEXT,
|
||||
archived_at DATETIME,
|
||||
allow_user_uploads BOOLEAN DEFAULT 0,
|
||||
upload_category_id INTEGER
|
||||
)
|
||||
`);
|
||||
|
||||
await db.raw(`INSERT INTO events_new SELECT * FROM events`);
|
||||
await db.raw(`DROP TABLE events`);
|
||||
await db.raw(`ALTER TABLE events_new RENAME TO events`);
|
||||
} catch (error) {
|
||||
// If the migration fails, it might already have been applied
|
||||
console.log('Color theme migration may have already been applied');
|
||||
const isPostgres = knexConfig.client === 'pg';
|
||||
|
||||
if (!isPostgres) {
|
||||
// SQLite-specific migration
|
||||
try {
|
||||
await db.raw(`
|
||||
CREATE TABLE IF NOT EXISTS events_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
event_name TEXT NOT NULL,
|
||||
event_date DATE NOT NULL,
|
||||
host_email TEXT NOT NULL,
|
||||
admin_email TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
welcome_message TEXT,
|
||||
color_theme TEXT,
|
||||
share_link TEXT UNIQUE NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
is_archived BOOLEAN DEFAULT 0,
|
||||
archive_path TEXT,
|
||||
archived_at DATETIME,
|
||||
allow_user_uploads BOOLEAN DEFAULT 0,
|
||||
upload_category_id INTEGER
|
||||
)
|
||||
`);
|
||||
|
||||
await db.raw(`INSERT INTO events_new SELECT * FROM events`);
|
||||
await db.raw(`DROP TABLE events`);
|
||||
await db.raw(`ALTER TABLE events_new RENAME TO events`);
|
||||
} catch (error) {
|
||||
// If the migration fails, it might already have been applied
|
||||
console.log('Color theme migration may have already been applied');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,4 +224,4 @@ async function logActivity(activityType, metadata = {}, eventId = null, actor =
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { db, initializeDatabase, logActivity };
|
||||
module.exports = { db, initializeDatabase, logActivity };
|
||||
@@ -0,0 +1,61 @@
|
||||
# Development Docker Compose Configuration
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=3000
|
||||
- JWT_SECRET=dev-secret-change-in-production
|
||||
- ADMIN_URL=http://localhost:3005
|
||||
- FRONTEND_URL=http://localhost:3005
|
||||
- DATABASE_CLIENT=sqlite3
|
||||
- DATABASE_PATH=./data/photo_sharing.db
|
||||
# Email - uses Mailhog
|
||||
- SMTP_HOST=mailhog
|
||||
- SMTP_PORT=1025
|
||||
- SMTP_SECURE=false
|
||||
- EMAIL_FROM=noreply@photo-sharing.local
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
depends_on:
|
||||
- mailhog
|
||||
command: sh -c "npm install && npm run dev"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3005:80"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
- ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
mailhog:
|
||||
image: mailhog/mailhog:latest
|
||||
ports:
|
||||
- "1025:1025" # SMTP
|
||||
- "8025:8025" # Web UI
|
||||
@@ -1,109 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.dev
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=3000
|
||||
- JWT_SECRET=b375996f704bb1541ad9297e7710a215fce0b59ecf1c43236aeea32ec01e7b27
|
||||
- ADMIN_URL=http://localhost:3005
|
||||
- FRONTEND_URL=http://localhost:3005
|
||||
# Email - uses Mailhog
|
||||
- SMTP_HOST=mailhog
|
||||
- SMTP_PORT=1025
|
||||
- SMTP_SECURE=false
|
||||
- SMTP_USER=
|
||||
- SMTP_PASS=
|
||||
- EMAIL_FROM=noreply@photo-sharing.local
|
||||
# Storage path
|
||||
- STORAGE_PATH=/app/storage
|
||||
# Umami Analytics (optional)
|
||||
- UMAMI_URL=
|
||||
- UMAMI_WEBSITE_ID=
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
depends_on:
|
||||
- mailhog
|
||||
command: sh -c "npm install && npm run dev"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
- VITE_API_URL=http://localhost:3001
|
||||
- VITE_UMAMI_URL=
|
||||
- VITE_UMAMI_WEBSITE_ID=
|
||||
ports:
|
||||
- "3005:80"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
- ./frontend/dist:/usr/share/nginx/html
|
||||
- ./frontend/nginx.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# Development frontend with hot reload
|
||||
frontend-dev:
|
||||
image: node:18-alpine
|
||||
working_dir: /app
|
||||
ports:
|
||||
- "3002:5173"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- VITE_API_URL=http://localhost:3001
|
||||
- VITE_UMAMI_URL=
|
||||
- VITE_UMAMI_WEBSITE_ID=
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
command: sh -c "npm install --legacy-peer-deps && npm run dev -- --host"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
mailhog:
|
||||
image: mailhog/mailhog:latest
|
||||
ports:
|
||||
- "1025:1025" # SMTP
|
||||
- "8025:8025" # Web UI
|
||||
|
||||
# Optional: File watcher service
|
||||
file-watcher:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.dev
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- STORAGE_PATH=/app/storage
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
command: node src/services/fileWatcher.js
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
node_modules_backend:
|
||||
node_modules_frontend:
|
||||
+11
-2
@@ -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:
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# docker-compose.yml - Development configuration
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=3000
|
||||
- JWT_SECRET=b55e4b3e9f212e1d1836c2ee2ec5ac47672350acdf1391c894d3433646579ad0
|
||||
- ADMIN_URL=http://localhost:3001
|
||||
- FRONTEND_URL=http://localhost:3005
|
||||
- SMTP_HOST=mailhog
|
||||
- SMTP_PORT=1025
|
||||
- SMTP_SECURE=false
|
||||
- SMTP_USER=
|
||||
- SMTP_PASS=
|
||||
- EMAIL_FROM=noreply@localhost
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
depends_on:
|
||||
- mailhog
|
||||
command: node server.js
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3005:80"
|
||||
environment:
|
||||
- REACT_APP_API_URL=http://localhost:3001
|
||||
volumes:
|
||||
- ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
mailhog:
|
||||
image: mailhog/mailhog:latest
|
||||
ports:
|
||||
- "1025:1025"
|
||||
- "8025:8025"
|
||||
@@ -0,0 +1,14 @@
|
||||
# Production Environment Configuration
|
||||
# When running behind a reverse proxy like Traefik, use relative URLs
|
||||
|
||||
# Backend API URL
|
||||
# For production behind reverse proxy, use relative URL:
|
||||
VITE_API_URL=/api
|
||||
|
||||
# For development or if frontend/backend are on different domains:
|
||||
# VITE_API_URL=https://api.yourdomain.com
|
||||
|
||||
# Umami Analytics Configuration (optional)
|
||||
# VITE_UMAMI_URL=https://analytics.yourdomain.com
|
||||
# VITE_UMAMI_WEBSITE_ID=your-website-id-from-umami
|
||||
# VITE_UMAMI_SHARE_URL=https://analytics.yourdomain.com/share/your-share-id/photo-sharing
|
||||
@@ -79,6 +79,20 @@ server {
|
||||
proxy_cache_valid 404 1m;
|
||||
}
|
||||
|
||||
# Uploads serving proxy (logos, favicons, watermarks)
|
||||
location /uploads {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Cache uploads
|
||||
proxy_cache_valid 200 302 7d;
|
||||
proxy_cache_valid 404 1m;
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@tiptap/extension-link": "^2.25.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -27,6 +27,7 @@ import { AdminLayout, AdminAuthWrapper } from './components/admin';
|
||||
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon } from './components/common';
|
||||
import { MaintenanceWrapper } from './components/MaintenanceWrapper';
|
||||
import { GlobalThemeProvider } from './components/GlobalThemeProvider';
|
||||
import { getApiBaseUrl } from './utils/url';
|
||||
|
||||
// Create a client
|
||||
const queryClient = new QueryClient({
|
||||
@@ -48,7 +49,7 @@ function App() {
|
||||
if (umamiUrl && umamiWebsiteId) {
|
||||
try {
|
||||
// Fetch public settings to check if analytics is enabled
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}/api/public/settings`);
|
||||
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||
const settings = await response.json();
|
||||
|
||||
// Only initialize if analytics is enabled in settings
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AlertTriangle } from 'lucide-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { api } from '../config/api';
|
||||
import { buildResourceUrl } from '../utils/url';
|
||||
|
||||
interface BrandingSettings {
|
||||
branding_company_name?: string;
|
||||
@@ -50,7 +51,7 @@ export const MaintenanceMode: React.FC = () => {
|
||||
src={settings?.branding_logo_url ?
|
||||
(settings.branding_logo_url.startsWith('http')
|
||||
? settings.branding_logo_url
|
||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settings.branding_logo_url}`)
|
||||
: buildResourceUrl(settings.branding_logo_url))
|
||||
: '/picpeak-logo-transparent.png'
|
||||
}
|
||||
alt={settings?.branding_company_name || 'PicPeak'}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Card, Input } from '../common';
|
||||
import { GALLERY_THEME_PRESETS, type ThemeConfig } from '../../contexts/ThemeContext';
|
||||
import { settingsService } from '../../services/settings.service';
|
||||
import { toast } from 'react-toastify';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
|
||||
interface ThemeCustomizerProps {
|
||||
value: ThemeConfig;
|
||||
@@ -269,7 +270,7 @@ export const ThemeCustomizer: React.FC<ThemeCustomizerProps> = ({
|
||||
<div className="flex items-center gap-4">
|
||||
{localTheme.logoUrl && (
|
||||
<img
|
||||
src={localTheme.logoUrl.startsWith('http') ? localTheme.logoUrl : `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${localTheme.logoUrl}`}
|
||||
src={localTheme.logoUrl.startsWith('http') ? localTheme.logoUrl : buildResourceUrl(localTheme.logoUrl)}
|
||||
alt="Custom logo"
|
||||
className="h-16 w-auto object-contain"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { getAuthToken } from '../../config/api';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
|
||||
interface AuthenticatedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
src: string;
|
||||
@@ -60,9 +61,8 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
// Use the src as-is since it should already be the correct endpoint
|
||||
let imageUrl = src;
|
||||
|
||||
// Prepend API URL for absolute paths
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3001';
|
||||
const fullImageUrl = imageUrl.startsWith('/') ? `${apiUrl}${imageUrl}` : imageUrl;
|
||||
// Build full URL for the image
|
||||
const fullImageUrl = imageUrl.startsWith('/') ? buildResourceUrl(imageUrl) : imageUrl;
|
||||
|
||||
console.log('Fetching authenticated image:', fullImageUrl);
|
||||
const response = await fetch(fullImageUrl, {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getApiBaseUrl, buildResourceUrl } from '../../utils/url';
|
||||
|
||||
export const DynamicFavicon: React.FC = () => {
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['public-settings'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}/api/public/settings`);
|
||||
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
@@ -30,7 +31,7 @@ export const DynamicFavicon: React.FC = () => {
|
||||
link.type = 'image/png';
|
||||
link.href = settings.branding_favicon_url.startsWith('http')
|
||||
? settings.branding_favicon_url
|
||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settings.branding_favicon_url}`;
|
||||
: buildResourceUrl(settings.branding_favicon_url);
|
||||
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReCAPTCHA from 'react-google-recaptcha';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getApiBaseUrl } from '../../utils/url';
|
||||
|
||||
interface ReCaptchaProps {
|
||||
onChange: (token: string | null) => void;
|
||||
@@ -20,7 +21,7 @@ export const ReCaptcha: React.FC<ReCaptchaProps> = ({
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['public-settings'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}/api/public/settings`);
|
||||
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||
return response.json();
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
||||
import { Button } from '../common';
|
||||
import { DynamicFavicon } from '../common/DynamicFavicon';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
|
||||
interface GalleryLayoutProps {
|
||||
event: {
|
||||
@@ -122,7 +123,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
||||
<div className="flex-shrink-0">
|
||||
<img
|
||||
src={brandingSettings?.logo_url ?
|
||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.logo_url}` :
|
||||
buildResourceUrl(brandingSettings.logo_url) :
|
||||
'/picpeak-logo-transparent.png'
|
||||
}
|
||||
alt={brandingSettings?.company_name || 'PicPeak'}
|
||||
@@ -277,7 +278,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
||||
<div className="mb-6">
|
||||
<img
|
||||
src={brandingSettings?.logo_url ?
|
||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.logo_url}` :
|
||||
buildResourceUrl(brandingSettings.logo_url) :
|
||||
'/picpeak-logo-transparent.png'
|
||||
}
|
||||
alt={brandingSettings?.company_name || 'PicPeak'}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { AuthenticatedImage } from '../../common';
|
||||
import type { BaseGalleryLayoutProps } from './BaseGalleryLayout';
|
||||
import type { Photo } from '../../../types';
|
||||
import { buildResourceUrl } from '../../../utils/url';
|
||||
|
||||
interface HeroGalleryLayoutProps extends BaseGalleryLayoutProps {
|
||||
eventName?: string;
|
||||
@@ -97,7 +98,7 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
<div className="mb-6">
|
||||
<img
|
||||
src={eventLogo ?
|
||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${eventLogo}` :
|
||||
buildResourceUrl(eventLogo) :
|
||||
'/picpeak-logo-transparent.png'
|
||||
}
|
||||
alt="Event logo"
|
||||
|
||||
@@ -14,7 +14,7 @@ export const setMaintenanceModeCallback = (callback: (enabled: boolean) => void)
|
||||
|
||||
// Create axios instance
|
||||
export const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001',
|
||||
baseURL: import.meta.env.VITE_API_URL || '/api',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { setMaintenanceModeCallback } from '../config/api';
|
||||
import { getApiBaseUrl } from '../utils/url';
|
||||
|
||||
interface MaintenanceContextType {
|
||||
isMaintenanceMode: boolean;
|
||||
@@ -29,7 +30,7 @@ export const MaintenanceProvider: React.FC<MaintenanceProviderProps> = ({ childr
|
||||
queryKey: ['public-settings-maintenance'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}/api/public/settings`);
|
||||
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||
if (response.status === 503) {
|
||||
setIsMaintenanceMode(true);
|
||||
return null;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { GalleryView } from '../components/gallery';
|
||||
import { analyticsService } from '../services/analytics.service';
|
||||
import { api } from '../config/api';
|
||||
import { GALLERY_THEME_PRESETS } from '../types/theme.types';
|
||||
import { buildResourceUrl } from '../utils/url';
|
||||
|
||||
export const GalleryPage: React.FC = () => {
|
||||
const { slug, token } = useParams<{ slug: string; token?: string }>();
|
||||
@@ -164,7 +165,7 @@ export const GalleryPage: React.FC = () => {
|
||||
{settingsData?.branding_logo_url && (
|
||||
<div className="p-8 text-center">
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settingsData.branding_logo_url}`}
|
||||
src={buildResourceUrl(settingsData.branding_logo_url)}
|
||||
alt={settingsData.branding_company_name || 'Company Logo'}
|
||||
className="h-16 w-auto object-contain mx-auto"
|
||||
/>
|
||||
@@ -220,7 +221,7 @@ export const GalleryPage: React.FC = () => {
|
||||
{settingsData?.branding_logo_url && (
|
||||
<div className="p-8 text-center">
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settingsData.branding_logo_url}`}
|
||||
src={buildResourceUrl(settingsData.branding_logo_url)}
|
||||
alt={settingsData.branding_company_name || 'Company Logo'}
|
||||
className="h-16 w-auto object-contain mx-auto"
|
||||
/>
|
||||
@@ -282,7 +283,7 @@ export const GalleryPage: React.FC = () => {
|
||||
<div className="text-center mb-4 sm:mb-6">
|
||||
<img
|
||||
src={settingsData?.branding_logo_url ?
|
||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${settingsData.branding_logo_url}` :
|
||||
buildResourceUrl(settingsData.branding_logo_url) :
|
||||
'/picpeak-logo-transparent.png'
|
||||
}
|
||||
alt={settingsData?.branding_company_name || 'PicPeak'}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTheme, type ThemeConfig, GALLERY_THEME_PRESETS } from '../../context
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { settingsService, type BrandingSettings } from '../../services/settings.service';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
|
||||
export const BrandingPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -279,7 +280,7 @@ export const BrandingPage: React.FC = () => {
|
||||
{brandingSettings.favicon_url && (
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={brandingSettings.favicon_url.startsWith('http') ? brandingSettings.favicon_url : `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.favicon_url}`}
|
||||
src={brandingSettings.favicon_url.startsWith('http') ? brandingSettings.favicon_url : buildResourceUrl(brandingSettings.favicon_url)}
|
||||
alt="Current favicon"
|
||||
className="w-8 h-8"
|
||||
/>
|
||||
@@ -344,7 +345,7 @@ export const BrandingPage: React.FC = () => {
|
||||
{brandingSettings.watermark_logo_url && (
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={brandingSettings.watermark_logo_url.startsWith('http') ? brandingSettings.watermark_logo_url : `${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.watermark_logo_url}`}
|
||||
src={brandingSettings.watermark_logo_url.startsWith('http') ? brandingSettings.watermark_logo_url : buildResourceUrl(brandingSettings.watermark_logo_url)}
|
||||
alt="Current watermark"
|
||||
className="h-16 w-auto object-contain bg-neutral-100 p-2 rounded"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Utility functions for URL handling in production environments
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the base API URL, preferring relative URLs for production
|
||||
* @returns The API base URL
|
||||
*/
|
||||
export const getApiBaseUrl = (): string => {
|
||||
// If VITE_API_URL is explicitly set, use it
|
||||
if (import.meta.env.VITE_API_URL && import.meta.env.VITE_API_URL !== '/api') {
|
||||
return import.meta.env.VITE_API_URL;
|
||||
}
|
||||
|
||||
// In production, use relative URL
|
||||
return '/api';
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a full URL for resources (images, files, etc.)
|
||||
* In production, this will use the current origin
|
||||
* @param path - The resource path
|
||||
* @returns The full URL
|
||||
*/
|
||||
export const buildResourceUrl = (path: string): string => {
|
||||
// Remove leading slash if present
|
||||
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
||||
|
||||
// If we have an explicit API URL that's not relative, use it
|
||||
const apiUrl = import.meta.env.VITE_API_URL;
|
||||
if (apiUrl && apiUrl !== '/api' && apiUrl.startsWith('http')) {
|
||||
const baseUrl = apiUrl.replace(/\/api\/?$/, ''); // Remove /api suffix if present
|
||||
return `${baseUrl}/${cleanPath}`;
|
||||
}
|
||||
|
||||
// In production (relative API), use current origin
|
||||
return `${window.location.origin}/${cleanPath}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if we're in production mode (using relative URLs)
|
||||
* @returns True if in production mode
|
||||
*/
|
||||
export const isProductionMode = (): boolean => {
|
||||
return !import.meta.env.VITE_API_URL || import.meta.env.VITE_API_URL === '/api';
|
||||
};
|
||||
Reference in New Issue
Block a user