Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e343106af5 | |||
| 2f848eb602 | |||
| 1c7fa781ad | |||
| 66940c2f5b | |||
| a3638fe954 | |||
| 0934695a69 | |||
| 77ece5c5f1 |
+28
-20
@@ -1,26 +1,34 @@
|
|||||||
# JWT Secret for authentication
|
# Environment Configuration Template
|
||||||
# IMPORTANT: Generate a secure random secret with: openssl rand -hex 32
|
# Copy this file to .env and adjust values for your environment
|
||||||
# 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
|
|
||||||
|
|
||||||
# URLs
|
# Development: Use docker-compose.dev.yml
|
||||||
ADMIN_URL=https://admin.photos.yourdomain.com
|
# Production: Use docker-compose.prod.yml with .env.production.example
|
||||||
FRONTEND_URL=https://photos.yourdomain.com
|
|
||||||
|
|
||||||
# Database (for PostgreSQL in production)
|
# JWT Secret (CRITICAL for production)
|
||||||
DB_USER=photoapp
|
# Generate with: openssl rand -base64 32
|
||||||
DB_PASSWORD=secure-password-here
|
JWT_SECRET=dev-secret-change-in-production
|
||||||
DB_NAME=photo_sharing
|
|
||||||
|
# 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
|
# Email Configuration
|
||||||
SMTP_HOST=smtp.gmail.com
|
# Development: Uses Mailhog (included in docker-compose.dev.yml)
|
||||||
SMTP_PORT=587
|
# Production: Configure real SMTP server
|
||||||
|
SMTP_HOST=mailhog
|
||||||
|
SMTP_PORT=1025
|
||||||
SMTP_SECURE=false
|
SMTP_SECURE=false
|
||||||
SMTP_USER=your-email@gmail.com
|
SMTP_USER=
|
||||||
SMTP_PASS=your-app-password
|
SMTP_PASS=
|
||||||
EMAIL_FROM=noreply@yourdomain.com
|
EMAIL_FROM=noreply@localhost
|
||||||
|
|
||||||
# Umami Analytics
|
# Optional: Umami Analytics
|
||||||
UMAMI_URL=https://analytics.yourdomain.com
|
UMAMI_URL=
|
||||||
UMAMI_WEBSITE_ID=your-website-id
|
UMAMI_WEBSITE_ID=
|
||||||
UMAMI_HASH_SALT=random-salt-here
|
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
|
# Application URLs
|
||||||
FRONTEND_HOST=photos.yourdomain.com
|
ADMIN_URL=https://yourdomain.com
|
||||||
BACKEND_HOST=api.photos.yourdomain.com
|
FRONTEND_URL=https://yourdomain.com
|
||||||
ADMIN_URL=https://admin.photos.yourdomain.com
|
|
||||||
FRONTEND_URL=https://photos.yourdomain.com
|
|
||||||
|
|
||||||
# Database Configuration
|
# Security - CRITICAL: Generate a secure random JWT secret
|
||||||
DB_NAME=photo_sharing
|
# You can generate one with: openssl rand -base64 32
|
||||||
DB_USER=photoapp
|
JWT_SECRET=your-secure-random-jwt-secret-here
|
||||||
DB_PASSWORD=your-secure-password-here
|
|
||||||
|
|
||||||
# JWT Configuration
|
# Database Configuration (PostgreSQL)
|
||||||
JWT_SECRET=your-jwt-secret-here
|
DB_USER=picpeak
|
||||||
|
DB_PASSWORD=your-secure-database-password
|
||||||
|
DB_NAME=picpeak
|
||||||
|
|
||||||
# Email Configuration
|
# Email Configuration
|
||||||
SMTP_HOST=smtp.gmail.com
|
SMTP_HOST=smtp.gmail.com
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=false
|
SMTP_SECURE=true
|
||||||
SMTP_USER=your-email@gmail.com
|
SMTP_USER=your-email@gmail.com
|
||||||
SMTP_PASS=your-app-password
|
SMTP_PASS=your-app-password
|
||||||
EMAIL_FROM=noreply@yourdomain.com
|
EMAIL_FROM=noreply@yourdomain.com
|
||||||
|
|
||||||
# Umami Analytics
|
# Umami Analytics (Optional)
|
||||||
UMAMI_URL=https://analytics.yourdomain.com
|
UMAMI_URL=https://analytics.yourdomain.com
|
||||||
UMAMI_HOST=analytics.yourdomain.com
|
|
||||||
UMAMI_WEBSITE_ID=your-website-id
|
UMAMI_WEBSITE_ID=your-website-id
|
||||||
UMAMI_HASH_SALT=your-random-salt
|
UMAMI_HASH_SALT=your-random-hash-salt
|
||||||
UMAMI_DB_PASSWORD=umami-db-password
|
|
||||||
|
|
||||||
# Traefik Configuration
|
# First Admin User (for initial setup)
|
||||||
TRAEFIK_HOST=traefik.yourdomain.com
|
# Run: docker-compose exec backend node scripts/create-admin.js --email admin@yourdomain.com
|
||||||
ACME_EMAIL=admin@yourdomain.com
|
ADMIN_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
|
|
||||||
+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
|
## Table of Contents
|
||||||
- [Prerequisites](#prerequisites)
|
- [Quick Start (Development)](#quick-start-development)
|
||||||
- [Infrastructure Setup](#infrastructure-setup)
|
- [Production Deployment](#production-deployment)
|
||||||
- [Docker Swarm Setup](#docker-swarm-setup)
|
- [Admin User Setup](#admin-user-setup)
|
||||||
- [Traefik Setup](#traefik-setup)
|
- [Configuration Reference](#configuration-reference)
|
||||||
- [Application Deployment](#application-deployment)
|
|
||||||
- [CI/CD with Drone](#cicd-with-drone)
|
|
||||||
- [Monitoring](#monitoring)
|
|
||||||
- [Backup and Recovery](#backup-and-recovery)
|
|
||||||
- [Troubleshooting](#troubleshooting)
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
## Prerequisites
|
## Quick Start (Development)
|
||||||
|
|
||||||
### Hardware Requirements
|
### 1. Clone and Setup
|
||||||
- **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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install Docker
|
git clone https://github.com/yourusername/picpeak.git
|
||||||
curl -fsSL https://get.docker.com | sh
|
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
|
# Copy environment template
|
||||||
cp .env.production.example .env.production
|
cp .env.example .env
|
||||||
|
|
||||||
# Edit with your values
|
# Start development environment
|
||||||
nano .env.production
|
docker-compose -f docker-compose.dev.yml up -d
|
||||||
```
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Access Services
|
### 2. Access Services
|
||||||
|
|
||||||
- Grafana: `https://grafana.yourdomain.com`
|
- Frontend: http://localhost:3005
|
||||||
- Prometheus: `https://prometheus.yourdomain.com`
|
- Backend API: http://localhost:3001
|
||||||
- Alertmanager: `https://alerts.yourdomain.com`
|
- 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
|
```yaml
|
||||||
groups:
|
version: '3.8'
|
||||||
- name: photo-sharing
|
|
||||||
rules:
|
services:
|
||||||
- alert: ServiceDown
|
frontend:
|
||||||
expr: up{job="photo-sharing-backend"} == 0
|
labels:
|
||||||
for: 5m
|
- "traefik.enable=true"
|
||||||
annotations:
|
- "traefik.http.routers.picpeak.rule=Host(`yourdomain.com`)"
|
||||||
summary: "Photo sharing backend is down"
|
- "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
|
```bash
|
||||||
# Edit crontab
|
# Production
|
||||||
crontab -e
|
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
|
# Auto-generate password
|
||||||
0 2 * * * /opt/photo-sharing/deploy/scripts/backup.sh
|
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
|
```bash
|
||||||
cd deploy/scripts
|
# List admin users
|
||||||
./backup.sh
|
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
|
```bash
|
||||||
# Extract backup
|
# PostgreSQL backup
|
||||||
tar -xzf backup-20240615-020000.tar.gz
|
docker-compose -f docker-compose.prod.yml exec db \
|
||||||
|
pg_dump -U picpeak picpeak > backup-$(date +%Y%m%d).sql
|
||||||
|
|
||||||
# Restore database
|
# Backup storage
|
||||||
docker exec -i $(docker ps -q -f name=photo-sharing_db) \
|
tar -czf storage-backup-$(date +%Y%m%d).tar.gz ./storage
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Maintenance
|
### Restore Database
|
||||||
|
|
||||||
### 1. Scaling Services
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Scale backend to 5 replicas
|
# PostgreSQL restore
|
||||||
docker service scale photo-sharing_backend=5
|
docker-compose -f docker-compose.prod.yml exec -T db \
|
||||||
|
psql -U picpeak picpeak < backup-20240115.sql
|
||||||
|
|
||||||
# Scale frontend to 3 replicas
|
# Restore storage
|
||||||
docker service scale photo-sharing_frontend=3
|
tar -xzf storage-backup-20240115.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Rolling Updates
|
## Monitoring
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Health Checks
|
### Health Checks
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check all endpoints
|
# Backend health
|
||||||
curl -f https://photos.yourdomain.com/health
|
curl https://yourdomain.com/api/health
|
||||||
curl -f https://api.photos.yourdomain.com/api/health
|
|
||||||
curl -f https://traefik.yourdomain.com/ping
|
# Frontend health
|
||||||
|
curl https://yourdomain.com/health
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security Best Practices
|
### Logs
|
||||||
|
|
||||||
1. **Regular Updates**
|
```bash
|
||||||
- Keep Docker and system packages updated
|
# All services
|
||||||
- Update application dependencies regularly
|
docker-compose -f docker-compose.prod.yml logs -f
|
||||||
- Monitor security advisories
|
|
||||||
|
|
||||||
2. **Access Control**
|
# Specific service
|
||||||
- Use strong passwords for all services
|
docker-compose -f docker-compose.prod.yml logs -f backend
|
||||||
- Enable 2FA where possible
|
|
||||||
- Restrict SSH access to specific IPs
|
|
||||||
- Use Docker secrets for sensitive data
|
|
||||||
|
|
||||||
3. **Network Security**
|
# Last 100 lines
|
||||||
- Use internal networks for service communication
|
docker-compose -f docker-compose.prod.yml logs --tail=100 backend
|
||||||
- Enable firewall rules
|
```
|
||||||
- Use TLS for all external communication
|
|
||||||
- Regular security scans with Trivy
|
|
||||||
|
|
||||||
4. **Backup Security**
|
## Troubleshooting
|
||||||
- Encrypt backups at rest
|
|
||||||
- Test restore procedures regularly
|
|
||||||
- Store backups in multiple locations
|
|
||||||
- Rotate old backups
|
|
||||||
|
|
||||||
## Performance Tuning
|
### Backend Won't Start
|
||||||
|
|
||||||
1. **Database Optimization**
|
1. Check database connection:
|
||||||
```sql
|
```bash
|
||||||
-- Add indexes for common queries
|
docker-compose -f docker-compose.prod.yml logs db
|
||||||
CREATE INDEX idx_photos_event_id ON photos(event_id);
|
|
||||||
CREATE INDEX idx_access_logs_event_id ON access_logs(event_id);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Image Optimization**
|
2. Verify environment variables:
|
||||||
- Use CDN for static assets
|
```bash
|
||||||
- Enable aggressive caching
|
docker-compose -f docker-compose.prod.yml exec backend env | grep DB_
|
||||||
- Optimize image sizes before upload
|
|
||||||
|
|
||||||
3. **Service Limits**
|
|
||||||
```yaml
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '2'
|
|
||||||
memory: 1G
|
|
||||||
reservations:
|
|
||||||
cpus: '0.5'
|
|
||||||
memory: 256M
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Support
|
### Can't Login as Admin
|
||||||
|
|
||||||
For issues and questions:
|
1. Verify admin user exists:
|
||||||
- Check logs: `docker service logs <service_name>`
|
```bash
|
||||||
- Review documentation: [README.md](README.md)
|
docker-compose -f docker-compose.prod.yml exec backend \
|
||||||
- Check monitoring dashboards
|
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||||
- Contact: admin@yourdomain.com
|
-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
|
# Logging
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
|
||||||
# Database (for production, consider PostgreSQL)
|
# Database Configuration
|
||||||
|
# Development: Use SQLite
|
||||||
DATABASE_CLIENT=sqlite3
|
DATABASE_CLIENT=sqlite3
|
||||||
DATABASE_PATH=./data/photo_sharing.db
|
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 Analytics (optional)
|
||||||
UMAMI_URL=
|
UMAMI_URL=
|
||||||
UMAMI_WEBSITE_ID=
|
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",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.1",
|
"version": "1.0.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.1",
|
"version": "1.0.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.1",
|
"version": "1.0.4",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"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 knex = require('knex');
|
||||||
const path = require('path');
|
const knexConfig = require('../../knexfile');
|
||||||
|
|
||||||
const db = knex({
|
const db = knex(knexConfig);
|
||||||
client: 'sqlite3',
|
|
||||||
connection: {
|
|
||||||
filename: path.join(__dirname, '../../data/photo_sharing.db')
|
|
||||||
},
|
|
||||||
useNullAsDefault: true
|
|
||||||
});
|
|
||||||
|
|
||||||
async function initializeDatabase() {
|
async function initializeDatabase() {
|
||||||
// Events table
|
// Events table
|
||||||
@@ -35,37 +29,42 @@ async function initializeDatabase() {
|
|||||||
} else {
|
} else {
|
||||||
// Check if color_theme needs to be updated to TEXT type
|
// Check if color_theme needs to be updated to TEXT type
|
||||||
// This is needed for larger theme configurations
|
// This is needed for larger theme configurations
|
||||||
try {
|
const isPostgres = knexConfig.client === 'pg';
|
||||||
await db.raw(`
|
|
||||||
CREATE TABLE IF NOT EXISTS events_new (
|
if (!isPostgres) {
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
// SQLite-specific migration
|
||||||
slug TEXT UNIQUE NOT NULL,
|
try {
|
||||||
event_type TEXT NOT NULL,
|
await db.raw(`
|
||||||
event_name TEXT NOT NULL,
|
CREATE TABLE IF NOT EXISTS events_new (
|
||||||
event_date DATE NOT NULL,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
host_email TEXT NOT NULL,
|
slug TEXT UNIQUE NOT NULL,
|
||||||
admin_email TEXT NOT NULL,
|
event_type TEXT NOT NULL,
|
||||||
password_hash TEXT NOT NULL,
|
event_name TEXT NOT NULL,
|
||||||
welcome_message TEXT,
|
event_date DATE NOT NULL,
|
||||||
color_theme TEXT,
|
host_email TEXT NOT NULL,
|
||||||
share_link TEXT UNIQUE NOT NULL,
|
admin_email TEXT NOT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
password_hash TEXT NOT NULL,
|
||||||
expires_at DATETIME NOT NULL,
|
welcome_message TEXT,
|
||||||
is_active BOOLEAN DEFAULT 1,
|
color_theme TEXT,
|
||||||
is_archived BOOLEAN DEFAULT 0,
|
share_link TEXT UNIQUE NOT NULL,
|
||||||
archive_path TEXT,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
archived_at DATETIME,
|
expires_at DATETIME NOT NULL,
|
||||||
allow_user_uploads BOOLEAN DEFAULT 0,
|
is_active BOOLEAN DEFAULT 1,
|
||||||
upload_category_id INTEGER
|
is_archived BOOLEAN DEFAULT 0,
|
||||||
)
|
archive_path TEXT,
|
||||||
`);
|
archived_at DATETIME,
|
||||||
|
allow_user_uploads BOOLEAN DEFAULT 0,
|
||||||
await db.raw(`INSERT INTO events_new SELECT * FROM events`);
|
upload_category_id INTEGER
|
||||||
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
|
await db.raw(`INSERT INTO events_new SELECT * FROM events`);
|
||||||
console.log('Color theme migration may have already been applied');
|
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}
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
- ADMIN_URL=${ADMIN_URL}
|
- ADMIN_URL=${ADMIN_URL}
|
||||||
- FRONTEND_URL=${FRONTEND_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_HOST=${SMTP_HOST}
|
||||||
- SMTP_PORT=${SMTP_PORT}
|
- SMTP_PORT=${SMTP_PORT}
|
||||||
- SMTP_SECURE=${SMTP_SECURE}
|
- SMTP_SECURE=${SMTP_SECURE}
|
||||||
- SMTP_USER=${SMTP_USER}
|
- SMTP_USER=${SMTP_USER}
|
||||||
- SMTP_PASS=${SMTP_PASS}
|
- SMTP_PASS=${SMTP_PASS}
|
||||||
- EMAIL_FROM=${EMAIL_FROM}
|
- EMAIL_FROM=${EMAIL_FROM}
|
||||||
|
# Analytics
|
||||||
- UMAMI_URL=${UMAMI_URL}
|
- UMAMI_URL=${UMAMI_URL}
|
||||||
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
|
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
|
||||||
volumes:
|
volumes:
|
||||||
@@ -70,7 +79,7 @@ services:
|
|||||||
image: postgres:14-alpine
|
image: postgres:14-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=${DB_USER:-photoapp}
|
- POSTGRES_USER=${DB_USER:-picpeak}
|
||||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||||
- POSTGRES_DB=${DB_NAME:-picpeak}
|
- POSTGRES_DB=${DB_NAME:-picpeak}
|
||||||
volumes:
|
volumes:
|
||||||
@@ -82,7 +91,7 @@ services:
|
|||||||
image: ghcr.io/umami-software/umami:postgresql-latest
|
image: ghcr.io/umami-software/umami:postgresql-latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
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
|
DATABASE_TYPE: postgresql
|
||||||
HASH_SALT: ${UMAMI_HASH_SALT}
|
HASH_SALT: ${UMAMI_HASH_SALT}
|
||||||
depends_on:
|
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;
|
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
|
# SPA fallback
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.1",
|
"version": "1.0.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.1",
|
"version": "1.0.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tiptap/extension-link": "^2.25.0",
|
"@tiptap/extension-link": "^2.25.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.1",
|
"version": "1.0.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { AdminLayout, AdminAuthWrapper } from './components/admin';
|
|||||||
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon } from './components/common';
|
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon } from './components/common';
|
||||||
import { MaintenanceWrapper } from './components/MaintenanceWrapper';
|
import { MaintenanceWrapper } from './components/MaintenanceWrapper';
|
||||||
import { GlobalThemeProvider } from './components/GlobalThemeProvider';
|
import { GlobalThemeProvider } from './components/GlobalThemeProvider';
|
||||||
|
import { getApiBaseUrl } from './utils/url';
|
||||||
|
|
||||||
// Create a client
|
// Create a client
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@@ -48,7 +49,7 @@ function App() {
|
|||||||
if (umamiUrl && umamiWebsiteId) {
|
if (umamiUrl && umamiWebsiteId) {
|
||||||
try {
|
try {
|
||||||
// Fetch public settings to check if analytics is enabled
|
// 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();
|
const settings = await response.json();
|
||||||
|
|
||||||
// Only initialize if analytics is enabled in settings
|
// Only initialize if analytics is enabled in settings
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AlertTriangle } from 'lucide-react';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { api } from '../config/api';
|
import { api } from '../config/api';
|
||||||
|
import { buildResourceUrl } from '../utils/url';
|
||||||
|
|
||||||
interface BrandingSettings {
|
interface BrandingSettings {
|
||||||
branding_company_name?: string;
|
branding_company_name?: string;
|
||||||
@@ -50,7 +51,7 @@ export const MaintenanceMode: React.FC = () => {
|
|||||||
src={settings?.branding_logo_url ?
|
src={settings?.branding_logo_url ?
|
||||||
(settings.branding_logo_url.startsWith('http')
|
(settings.branding_logo_url.startsWith('http')
|
||||||
? settings.branding_logo_url
|
? 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'
|
: '/picpeak-logo-transparent.png'
|
||||||
}
|
}
|
||||||
alt={settings?.branding_company_name || 'PicPeak'}
|
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 { GALLERY_THEME_PRESETS, type ThemeConfig } from '../../contexts/ThemeContext';
|
||||||
import { settingsService } from '../../services/settings.service';
|
import { settingsService } from '../../services/settings.service';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { buildResourceUrl } from '../../utils/url';
|
||||||
|
|
||||||
interface ThemeCustomizerProps {
|
interface ThemeCustomizerProps {
|
||||||
value: ThemeConfig;
|
value: ThemeConfig;
|
||||||
@@ -269,7 +270,7 @@ export const ThemeCustomizer: React.FC<ThemeCustomizerProps> = ({
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{localTheme.logoUrl && (
|
{localTheme.logoUrl && (
|
||||||
<img
|
<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"
|
alt="Custom logo"
|
||||||
className="h-16 w-auto object-contain"
|
className="h-16 w-auto object-contain"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { getAuthToken } from '../../config/api';
|
import { getAuthToken } from '../../config/api';
|
||||||
|
import { buildResourceUrl } from '../../utils/url';
|
||||||
|
|
||||||
interface AuthenticatedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
interface AuthenticatedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||||
src: string;
|
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
|
// Use the src as-is since it should already be the correct endpoint
|
||||||
let imageUrl = src;
|
let imageUrl = src;
|
||||||
|
|
||||||
// Prepend API URL for absolute paths
|
// Build full URL for the image
|
||||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3001';
|
const fullImageUrl = imageUrl.startsWith('/') ? buildResourceUrl(imageUrl) : imageUrl;
|
||||||
const fullImageUrl = imageUrl.startsWith('/') ? `${apiUrl}${imageUrl}` : imageUrl;
|
|
||||||
|
|
||||||
console.log('Fetching authenticated image:', fullImageUrl);
|
console.log('Fetching authenticated image:', fullImageUrl);
|
||||||
const response = await fetch(fullImageUrl, {
|
const response = await fetch(fullImageUrl, {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { getApiBaseUrl, buildResourceUrl } from '../../utils/url';
|
||||||
|
|
||||||
export const DynamicFavicon: React.FC = () => {
|
export const DynamicFavicon: React.FC = () => {
|
||||||
const { data: settings } = useQuery({
|
const { data: settings } = useQuery({
|
||||||
queryKey: ['public-settings'],
|
queryKey: ['public-settings'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
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) {
|
if (response.ok) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
@@ -30,7 +31,7 @@ export const DynamicFavicon: React.FC = () => {
|
|||||||
link.type = 'image/png';
|
link.type = 'image/png';
|
||||||
link.href = settings.branding_favicon_url.startsWith('http')
|
link.href = settings.branding_favicon_url.startsWith('http')
|
||||||
? settings.branding_favicon_url
|
? 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);
|
document.head.appendChild(link);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
import ReCAPTCHA from 'react-google-recaptcha';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { getApiBaseUrl } from '../../utils/url';
|
||||||
|
|
||||||
interface ReCaptchaProps {
|
interface ReCaptchaProps {
|
||||||
onChange: (token: string | null) => void;
|
onChange: (token: string | null) => void;
|
||||||
@@ -20,7 +21,7 @@ export const ReCaptcha: React.FC<ReCaptchaProps> = ({
|
|||||||
const { data: settings } = useQuery({
|
const { data: settings } = useQuery({
|
||||||
queryKey: ['public-settings'],
|
queryKey: ['public-settings'],
|
||||||
queryFn: async () => {
|
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();
|
return response.json();
|
||||||
},
|
},
|
||||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
|||||||
import { Button } from '../common';
|
import { Button } from '../common';
|
||||||
import { DynamicFavicon } from '../common/DynamicFavicon';
|
import { DynamicFavicon } from '../common/DynamicFavicon';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
|
import { buildResourceUrl } from '../../utils/url';
|
||||||
|
|
||||||
interface GalleryLayoutProps {
|
interface GalleryLayoutProps {
|
||||||
event: {
|
event: {
|
||||||
@@ -122,7 +123,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
src={brandingSettings?.logo_url ?
|
src={brandingSettings?.logo_url ?
|
||||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.logo_url}` :
|
buildResourceUrl(brandingSettings.logo_url) :
|
||||||
'/picpeak-logo-transparent.png'
|
'/picpeak-logo-transparent.png'
|
||||||
}
|
}
|
||||||
alt={brandingSettings?.company_name || 'PicPeak'}
|
alt={brandingSettings?.company_name || 'PicPeak'}
|
||||||
@@ -277,7 +278,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<img
|
<img
|
||||||
src={brandingSettings?.logo_url ?
|
src={brandingSettings?.logo_url ?
|
||||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.logo_url}` :
|
buildResourceUrl(brandingSettings.logo_url) :
|
||||||
'/picpeak-logo-transparent.png'
|
'/picpeak-logo-transparent.png'
|
||||||
}
|
}
|
||||||
alt={brandingSettings?.company_name || 'PicPeak'}
|
alt={brandingSettings?.company_name || 'PicPeak'}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useTheme } from '../../../contexts/ThemeContext';
|
|||||||
import { AuthenticatedImage } from '../../common';
|
import { AuthenticatedImage } from '../../common';
|
||||||
import type { BaseGalleryLayoutProps } from './BaseGalleryLayout';
|
import type { BaseGalleryLayoutProps } from './BaseGalleryLayout';
|
||||||
import type { Photo } from '../../../types';
|
import type { Photo } from '../../../types';
|
||||||
|
import { buildResourceUrl } from '../../../utils/url';
|
||||||
|
|
||||||
interface HeroGalleryLayoutProps extends BaseGalleryLayoutProps {
|
interface HeroGalleryLayoutProps extends BaseGalleryLayoutProps {
|
||||||
eventName?: string;
|
eventName?: string;
|
||||||
@@ -97,7 +98,7 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<img
|
<img
|
||||||
src={eventLogo ?
|
src={eventLogo ?
|
||||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${eventLogo}` :
|
buildResourceUrl(eventLogo) :
|
||||||
'/picpeak-logo-transparent.png'
|
'/picpeak-logo-transparent.png'
|
||||||
}
|
}
|
||||||
alt="Event logo"
|
alt="Event logo"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const setMaintenanceModeCallback = (callback: (enabled: boolean) => void)
|
|||||||
|
|
||||||
// Create axios instance
|
// Create axios instance
|
||||||
export const api = axios.create({
|
export const api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001',
|
baseURL: import.meta.env.VITE_API_URL || '/api',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { setMaintenanceModeCallback } from '../config/api';
|
import { setMaintenanceModeCallback } from '../config/api';
|
||||||
|
import { getApiBaseUrl } from '../utils/url';
|
||||||
|
|
||||||
interface MaintenanceContextType {
|
interface MaintenanceContextType {
|
||||||
isMaintenanceMode: boolean;
|
isMaintenanceMode: boolean;
|
||||||
@@ -29,7 +30,7 @@ export const MaintenanceProvider: React.FC<MaintenanceProviderProps> = ({ childr
|
|||||||
queryKey: ['public-settings-maintenance'],
|
queryKey: ['public-settings-maintenance'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
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) {
|
if (response.status === 503) {
|
||||||
setIsMaintenanceMode(true);
|
setIsMaintenanceMode(true);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -704,6 +704,8 @@
|
|||||||
"category_created": "Kategorie erstellt: {{categoryName}}",
|
"category_created": "Kategorie erstellt: {{categoryName}}",
|
||||||
"category_updated": "Kategorie aktualisiert: {{categoryName}}",
|
"category_updated": "Kategorie aktualisiert: {{categoryName}}",
|
||||||
"category_deleted": "Kategorie gelöscht: {{categoryName}}",
|
"category_deleted": "Kategorie gelöscht: {{categoryName}}",
|
||||||
|
"general_settings_updated": "Allgemeine Einstellungen aktualisiert",
|
||||||
|
"favicon_uploaded": "Favicon hochgeladen",
|
||||||
"unknown": "Unbekannte Aktivität"
|
"unknown": "Unbekannte Aktivität"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -755,6 +755,8 @@
|
|||||||
"category_created": "Category created: {{categoryName}}",
|
"category_created": "Category created: {{categoryName}}",
|
||||||
"category_updated": "Category updated: {{categoryName}}",
|
"category_updated": "Category updated: {{categoryName}}",
|
||||||
"category_deleted": "Category deleted: {{categoryName}}",
|
"category_deleted": "Category deleted: {{categoryName}}",
|
||||||
|
"general_settings_updated": "General settings updated",
|
||||||
|
"favicon_uploaded": "Favicon uploaded",
|
||||||
"unknown": "Unknown activity"
|
"unknown": "Unknown activity"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { GalleryView } from '../components/gallery';
|
|||||||
import { analyticsService } from '../services/analytics.service';
|
import { analyticsService } from '../services/analytics.service';
|
||||||
import { api } from '../config/api';
|
import { api } from '../config/api';
|
||||||
import { GALLERY_THEME_PRESETS } from '../types/theme.types';
|
import { GALLERY_THEME_PRESETS } from '../types/theme.types';
|
||||||
|
import { buildResourceUrl } from '../utils/url';
|
||||||
|
|
||||||
export const GalleryPage: React.FC = () => {
|
export const GalleryPage: React.FC = () => {
|
||||||
const { slug, token } = useParams<{ slug: string; token?: string }>();
|
const { slug, token } = useParams<{ slug: string; token?: string }>();
|
||||||
@@ -164,7 +165,7 @@ export const GalleryPage: React.FC = () => {
|
|||||||
{settingsData?.branding_logo_url && (
|
{settingsData?.branding_logo_url && (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
<img
|
<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'}
|
alt={settingsData.branding_company_name || 'Company Logo'}
|
||||||
className="h-16 w-auto object-contain mx-auto"
|
className="h-16 w-auto object-contain mx-auto"
|
||||||
/>
|
/>
|
||||||
@@ -220,7 +221,7 @@ export const GalleryPage: React.FC = () => {
|
|||||||
{settingsData?.branding_logo_url && (
|
{settingsData?.branding_logo_url && (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
<img
|
<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'}
|
alt={settingsData.branding_company_name || 'Company Logo'}
|
||||||
className="h-16 w-auto object-contain mx-auto"
|
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">
|
<div className="text-center mb-4 sm:mb-6">
|
||||||
<img
|
<img
|
||||||
src={settingsData?.branding_logo_url ?
|
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'
|
'/picpeak-logo-transparent.png'
|
||||||
}
|
}
|
||||||
alt={settingsData?.branding_company_name || 'PicPeak'}
|
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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { settingsService, type BrandingSettings } from '../../services/settings.service';
|
import { settingsService, type BrandingSettings } from '../../services/settings.service';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { buildResourceUrl } from '../../utils/url';
|
||||||
|
|
||||||
export const BrandingPage: React.FC = () => {
|
export const BrandingPage: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -279,7 +280,7 @@ export const BrandingPage: React.FC = () => {
|
|||||||
{brandingSettings.favicon_url && (
|
{brandingSettings.favicon_url && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<img
|
<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"
|
alt="Current favicon"
|
||||||
className="w-8 h-8"
|
className="w-8 h-8"
|
||||||
/>
|
/>
|
||||||
@@ -344,7 +345,7 @@ export const BrandingPage: React.FC = () => {
|
|||||||
{brandingSettings.watermark_logo_url && (
|
{brandingSettings.watermark_logo_url && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<img
|
<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"
|
alt="Current watermark"
|
||||||
className="h-16 w-auto object-contain bg-neutral-100 p-2 rounded"
|
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