Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cdff4ebb8a | |||
| 59f958b085 | |||
| 097d7a0b65 | |||
| 1273777541 | |||
| b6a960879f | |||
| 4bd153104b | |||
| e099fcf600 |
+31
-18
@@ -1,26 +1,24 @@
|
|||||||
# Environment Configuration Template
|
# PicPeak Development Environment Configuration
|
||||||
# Copy this file to .env and adjust values for your environment
|
# Copy this file to .env for local development
|
||||||
|
|
||||||
# Development: Use docker-compose.dev.yml
|
# SECURITY WARNING: This configuration is for development only!
|
||||||
# Production: Use docker-compose.prod.yml with .env.production.example
|
# For production, use .env.production.example
|
||||||
|
|
||||||
# JWT Secret (CRITICAL for production)
|
# JWT Secret (Change in production!)
|
||||||
# Generate with: openssl rand -base64 32
|
# Generate secure secret with: openssl rand -base64 32
|
||||||
JWT_SECRET=dev-secret-change-in-production
|
JWT_SECRET=dev-secret-DO-NOT-USE-IN-PRODUCTION
|
||||||
|
|
||||||
# Application URLs
|
# Application URLs (Docker Compose development setup)
|
||||||
ADMIN_URL=http://localhost:3005
|
ADMIN_URL=http://localhost:3005
|
||||||
FRONTEND_URL=http://localhost:3005
|
FRONTEND_URL=http://localhost:3005
|
||||||
|
BACKEND_URL=http://localhost:3001
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration (SQLite for development)
|
||||||
# SQLite is used for development by default
|
|
||||||
# For production PostgreSQL config, see .env.production.example
|
|
||||||
DATABASE_CLIENT=sqlite3
|
DATABASE_CLIENT=sqlite3
|
||||||
DATABASE_PATH=./data/photo_sharing.db
|
DATABASE_PATH=./data/photo_sharing.db
|
||||||
|
|
||||||
# Email Configuration
|
# Email Configuration (Mailhog for development)
|
||||||
# Development: Uses Mailhog (included in docker-compose.dev.yml)
|
# Access Mailhog UI at: http://localhost:8025
|
||||||
# Production: Configure real SMTP server
|
|
||||||
SMTP_HOST=mailhog
|
SMTP_HOST=mailhog
|
||||||
SMTP_PORT=1025
|
SMTP_PORT=1025
|
||||||
SMTP_SECURE=false
|
SMTP_SECURE=false
|
||||||
@@ -28,7 +26,22 @@ SMTP_USER=
|
|||||||
SMTP_PASS=
|
SMTP_PASS=
|
||||||
EMAIL_FROM=noreply@localhost
|
EMAIL_FROM=noreply@localhost
|
||||||
|
|
||||||
# Optional: Umami Analytics
|
# Backend Port Configuration
|
||||||
UMAMI_URL=
|
PORT=3001
|
||||||
UMAMI_WEBSITE_ID=
|
|
||||||
UMAMI_HASH_SALT=
|
# Optional: Umami Analytics Backend Config
|
||||||
|
# NOTE: Primary configuration through Admin UI > Settings > Analytics
|
||||||
|
# These are fallback values for server-side tracking
|
||||||
|
# UMAMI_URL=https://analytics.example.com
|
||||||
|
# UMAMI_WEBSITE_ID=your-website-id
|
||||||
|
# UMAMI_HASH_SALT=your-hash-salt
|
||||||
|
|
||||||
|
# Development Features
|
||||||
|
NODE_ENV=development
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# Admin Setup Notes:
|
||||||
|
# 1. Run 'npm run migrate' in backend folder
|
||||||
|
# 2. Admin credentials will be auto-generated
|
||||||
|
# 3. Check ADMIN_CREDENTIALS.txt for login details
|
||||||
|
# 4. Change password on first login (required)
|
||||||
+83
-27
@@ -1,44 +1,100 @@
|
|||||||
# PicPeak Production Configuration
|
# PicPeak Production Configuration
|
||||||
# Copy this file to .env and update with your values
|
# Copy this file to .env and update with your production values
|
||||||
|
|
||||||
# Required: Security
|
# ============================================
|
||||||
JWT_SECRET=CHANGE_THIS_TO_RANDOM_32_CHAR_STRING
|
# CRITICAL SECURITY - MUST CHANGE ALL VALUES!
|
||||||
|
# ============================================
|
||||||
|
|
||||||
# Required: URLs (update with your domain)
|
# JWT Secret - REQUIRED (minimum 32 characters)
|
||||||
|
# Generate with: openssl rand -base64 32
|
||||||
|
JWT_SECRET=CHANGE-THIS-PRODUCTION-SECRET-USE-OPENSSL-COMMAND
|
||||||
|
|
||||||
|
# Application URLs - REQUIRED (your actual domain)
|
||||||
FRONTEND_URL=https://your-domain.com
|
FRONTEND_URL=https://your-domain.com
|
||||||
BACKEND_URL=https://your-domain.com
|
BACKEND_URL=https://your-domain.com
|
||||||
ADMIN_URL=https://your-domain.com
|
ADMIN_URL=https://your-domain.com
|
||||||
|
|
||||||
# Required: Email Settings
|
# ============================================
|
||||||
SMTP_HOST=smtp.gmail.com
|
# DATABASE CONFIGURATION - REQUIRED
|
||||||
SMTP_PORT=587
|
# ============================================
|
||||||
SMTP_USER=your-email@gmail.com
|
|
||||||
SMTP_PASS=your-app-password
|
|
||||||
SMTP_FROM=your-email@gmail.com
|
|
||||||
|
|
||||||
# Required: Initial Admin Account
|
# PostgreSQL Configuration (Recommended for production)
|
||||||
ADMIN_EMAIL=admin@your-domain.com
|
|
||||||
ADMIN_PASSWORD=change-this-password
|
|
||||||
|
|
||||||
# Database (PostgreSQL recommended for production)
|
|
||||||
DATABASE_CLIENT=pg
|
DATABASE_CLIENT=pg
|
||||||
DB_HOST=postgres
|
DB_HOST=postgres # or your database host
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_NAME=picpeak
|
DB_NAME=picpeak
|
||||||
DB_USER=picpeak
|
DB_USER=picpeak
|
||||||
DB_PASSWORD=secure-database-password
|
DB_PASSWORD=CHANGE-THIS-SECURE-DATABASE-PASSWORD
|
||||||
|
|
||||||
# Optional: Customization
|
# ============================================
|
||||||
SITE_NAME=PicPeak
|
# EMAIL CONFIGURATION - REQUIRED
|
||||||
DEFAULT_EXPIRATION_DAYS=30
|
# ============================================
|
||||||
SESSION_TIMEOUT_MINUTES=60
|
|
||||||
|
|
||||||
# Optional: Analytics (Umami)
|
# Example: Gmail with App Password
|
||||||
VITE_UMAMI_URL=
|
# SMTP_HOST=smtp.gmail.com
|
||||||
VITE_UMAMI_WEBSITE_ID=
|
# SMTP_PORT=587
|
||||||
|
# SMTP_SECURE=false
|
||||||
|
# SMTP_USER=your-email@gmail.com
|
||||||
|
# SMTP_PASS=your-16-char-app-password
|
||||||
|
# EMAIL_FROM=Your Name <your-email@gmail.com>
|
||||||
|
|
||||||
|
# Example: SendGrid
|
||||||
|
SMTP_HOST=smtp.sendgrid.net
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_SECURE=false
|
||||||
|
SMTP_USER=apikey
|
||||||
|
SMTP_PASS=YOUR-SENDGRID-API-KEY
|
||||||
|
EMAIL_FROM=PicPeak <noreply@your-domain.com>
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ADMIN SETUP - AUTO-GENERATED
|
||||||
|
# ============================================
|
||||||
|
# NOTE: Admin credentials are automatically generated during setup
|
||||||
|
# DO NOT set ADMIN_EMAIL or ADMIN_PASSWORD anymore!
|
||||||
|
# Run 'npm run migrate' and check ADMIN_CREDENTIALS.txt
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# OPTIONAL CONFIGURATION
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Umami Analytics (Optional - Fallback values)
|
||||||
|
# Primary config via Admin UI > Settings > Analytics
|
||||||
|
# UMAMI_URL=https://analytics.your-domain.com
|
||||||
|
# UMAMI_WEBSITE_ID=your-website-id
|
||||||
|
# UMAMI_HASH_SALT=your-hash-salt
|
||||||
|
|
||||||
|
# Frontend Analytics (Optional - Fallback values)
|
||||||
|
# VITE_UMAMI_URL=https://analytics.your-domain.com
|
||||||
|
# VITE_UMAMI_WEBSITE_ID=your-website-id
|
||||||
|
# VITE_UMAMI_SHARE_URL=https://analytics.your-domain.com/share/xyz/gallery
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# PERFORMANCE & SECURITY TUNING
|
||||||
|
# ============================================
|
||||||
|
|
||||||
# Advanced: Performance Tuning
|
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
PORT=3001
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Security Settings (Defaults are secure)
|
||||||
BCRYPT_ROUNDS=12
|
BCRYPT_ROUNDS=12
|
||||||
RATE_LIMIT_WINDOW_MS=900000
|
SESSION_TIMEOUT_MINUTES=60
|
||||||
RATE_LIMIT_MAX_REQUESTS=100
|
RATE_LIMIT_WINDOW_MS=900000 # 15 minutes
|
||||||
|
RATE_LIMIT_MAX_REQUESTS=100 # per window
|
||||||
|
|
||||||
|
# Connection Pool (Adjust based on load)
|
||||||
|
DB_POOL_MIN=5
|
||||||
|
DB_POOL_MAX=25
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# DOCKER COMPOSE SPECIFIC
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Traefik Configuration (if using Traefik)
|
||||||
|
DOMAIN=your-domain.com
|
||||||
|
LETSENCRYPT_EMAIL=admin@your-domain.com
|
||||||
|
|
||||||
|
# Volume Paths (Docker)
|
||||||
|
STORAGE_PATH=/app/storage
|
||||||
|
EVENTS_PATH=/app/storage/events
|
||||||
|
ARCHIVE_PATH=/app/storage/events/archived
|
||||||
@@ -63,9 +63,18 @@ jobs:
|
|||||||
COMMITS_AFTER_TARGET=$(git rev-list --reverse --no-merges $TARGET_COMMIT..main)
|
COMMITS_AFTER_TARGET=$(git rev-list --reverse --no-merges $TARGET_COMMIT..main)
|
||||||
|
|
||||||
if [ -n "$COMMITS_AFTER_TARGET" ]; then
|
if [ -n "$COMMITS_AFTER_TARGET" ]; then
|
||||||
echo "📋 Applying changes from commits after $TARGET_COMMIT:"
|
echo "📋 Applying changes from commits after $TARGET_COMMIT (excluding Claude commits):"
|
||||||
|
|
||||||
for commit in $COMMITS_AFTER_TARGET; do
|
for commit in $COMMITS_AFTER_TARGET; do
|
||||||
|
# Get the commit author name
|
||||||
|
COMMIT_AUTHOR_NAME=$(git log --format="%an" -n 1 $commit)
|
||||||
|
|
||||||
|
# Skip commits by Claude
|
||||||
|
if [ "$COMMIT_AUTHOR_NAME" = "Claude" ]; then
|
||||||
|
echo "⚠️ Skipping commit by Claude: $(git log --oneline -1 $commit)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Processing: $(git log --oneline -1 $commit)"
|
echo "Processing: $(git log --oneline -1 $commit)"
|
||||||
|
|
||||||
# Get the commit message and author info
|
# Get the commit message and author info
|
||||||
@@ -121,7 +130,7 @@ jobs:
|
|||||||
|
|
||||||
# Remove sensitive files/directories if they exist
|
# Remove sensitive files/directories if they exist
|
||||||
echo "Removing sensitive files..."
|
echo "Removing sensitive files..."
|
||||||
rm -rf .env* || true
|
rm -rf .env || true
|
||||||
rm -rf backend/.env* || true
|
rm -rf backend/.env* || true
|
||||||
rm -rf frontend/.env* || true
|
rm -rf frontend/.env* || true
|
||||||
rm -rf docker-compose.prod.yml || true
|
rm -rf docker-compose.prod.yml || true
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Production Deployment Guide
|
# Production Deployment Guide
|
||||||
|
|
||||||
This guide addresses all known production deployment issues and provides solutions.
|
This comprehensive guide addresses all production deployment scenarios and common issues.
|
||||||
|
|
||||||
## Pre-Deployment Checklist
|
## Pre-Deployment Checklist
|
||||||
|
|
||||||
@@ -8,43 +8,80 @@ This guide addresses all known production deployment issues and provides solutio
|
|||||||
Create a `.env` file with ALL required variables:
|
Create a `.env` file with ALL required variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Required
|
# CRITICAL - Must change these!
|
||||||
JWT_SECRET=<generate-with-openssl-rand-base64-32>
|
JWT_SECRET=<generate-with-openssl-rand-base64-32>
|
||||||
DB_PASSWORD=<strong-password>
|
DB_PASSWORD=<strong-password>
|
||||||
|
|
||||||
|
# Application URLs (your actual domain)
|
||||||
ADMIN_URL=https://yourdomain.com
|
ADMIN_URL=https://yourdomain.com
|
||||||
FRONTEND_URL=https://yourdomain.com
|
FRONTEND_URL=https://yourdomain.com
|
||||||
|
BACKEND_URL=https://yourdomain.com
|
||||||
|
|
||||||
# Database
|
# Database (PostgreSQL)
|
||||||
|
DATABASE_CLIENT=pg
|
||||||
|
DB_HOST=postgres # or external host
|
||||||
|
DB_PORT=5432
|
||||||
DB_USER=picpeak
|
DB_USER=picpeak
|
||||||
DB_NAME=picpeak
|
DB_NAME=picpeak
|
||||||
|
|
||||||
# Email (Optional but recommended)
|
# Email Configuration (required for notifications)
|
||||||
SMTP_HOST=smtp.gmail.com
|
SMTP_HOST=smtp.gmail.com
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=false
|
SMTP_SECURE=false
|
||||||
SMTP_USER=your-email@gmail.com
|
SMTP_USER=your-email@gmail.com
|
||||||
SMTP_PASS=your-app-password
|
SMTP_PASS=your-app-password # Use app-specific password
|
||||||
EMAIL_FROM=noreply@yourdomain.com
|
EMAIL_FROM=PicPeak <noreply@yourdomain.com>
|
||||||
|
|
||||||
# Umami Analytics (Optional)
|
# Port Configuration
|
||||||
UMAMI_URL=https://analytics.yourdomain.com
|
PORT=3001
|
||||||
UMAMI_WEBSITE_ID=your-website-id
|
|
||||||
UMAMI_HASH_SALT=<generate-random-string>
|
# Performance Tuning
|
||||||
|
DB_POOL_MIN=5
|
||||||
|
DB_POOL_MAX=25
|
||||||
|
NODE_ENV=production
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Optional: Umami Analytics (configured via Admin UI)
|
||||||
|
# UMAMI_URL=https://analytics.yourdomain.com
|
||||||
|
# UMAMI_WEBSITE_ID=your-website-id
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Generate Secrets
|
### 2. Generate Secrets
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate JWT Secret
|
# Generate JWT Secret (REQUIRED)
|
||||||
openssl rand -base64 32
|
openssl rand -base64 32
|
||||||
|
|
||||||
# Generate Database Password
|
# Generate Database Password
|
||||||
openssl rand -base64 24
|
openssl rand -base64 24
|
||||||
|
|
||||||
# Generate Umami Hash Salt
|
|
||||||
openssl rand -hex 32
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Frontend Configuration
|
||||||
|
|
||||||
|
For production deployment behind a reverse proxy:
|
||||||
|
|
||||||
|
### Frontend Environment
|
||||||
|
```bash
|
||||||
|
# frontend/.env.production
|
||||||
|
VITE_API_URL=/api # Uses relative path for reverse proxy
|
||||||
|
|
||||||
|
# Optional: Umami fallback (primary config via Admin UI)
|
||||||
|
# VITE_UMAMI_URL=https://analytics.yourdomain.com
|
||||||
|
# VITE_UMAMI_WEBSITE_ID=your-website-id
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures all API calls use the same domain/protocol as the frontend.
|
||||||
|
|
||||||
|
### Nginx Proxy Configuration
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
All static assets are served through the nginx proxy, inheriting authentication headers.
|
||||||
|
|
||||||
## Deployment Steps
|
## Deployment Steps
|
||||||
|
|
||||||
### 1. Initial Setup
|
### 1. Initial Setup
|
||||||
@@ -96,24 +133,31 @@ docker-compose -f docker-compose.prod.yml up -d
|
|||||||
docker-compose -f docker-compose.prod.yml logs -f backend
|
docker-compose -f docker-compose.prod.yml logs -f backend
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Create Admin User
|
### 4. Initial Admin Setup
|
||||||
|
|
||||||
After deployment, create the first admin user:
|
The admin user is automatically created during database migration:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Enter backend container
|
# Run migrations (this creates admin user)
|
||||||
docker-compose -f docker-compose.prod.yml exec backend sh
|
docker-compose -f docker-compose.prod.yml exec backend npm run migrate
|
||||||
|
|
||||||
# Create admin
|
# Admin credentials will be displayed in console and saved to ADMIN_CREDENTIALS.txt
|
||||||
node scripts/create-admin.js \
|
# Example output:
|
||||||
--username admin \
|
# ========================================
|
||||||
--email admin@yourdomain.com \
|
# ✅ Admin user created successfully!
|
||||||
--password <your-secure-password>
|
# ========================================
|
||||||
|
# Username: admin
|
||||||
|
# Password: SwiftEagle3847!
|
||||||
|
#
|
||||||
|
# ⚠️ IMPORTANT: Change password on first login
|
||||||
|
# ========================================
|
||||||
|
|
||||||
# Exit container
|
# Retrieve credentials if needed
|
||||||
exit
|
docker-compose -f docker-compose.prod.yml exec backend cat ADMIN_CREDENTIALS.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Important**: You MUST change the auto-generated password on first login.
|
||||||
|
|
||||||
### 5. Configure Email (if using database config)
|
### 5. Configure Email (if using database config)
|
||||||
|
|
||||||
1. Login to admin panel: https://yourdomain.com/admin
|
1. Login to admin panel: https://yourdomain.com/admin
|
||||||
@@ -189,6 +233,23 @@ docker-compose -f docker-compose.prod.yml logs backend | grep email
|
|||||||
|
|
||||||
## SSL/HTTPS Setup
|
## SSL/HTTPS Setup
|
||||||
|
|
||||||
|
### Option 1: Using Traefik (Recommended)
|
||||||
|
|
||||||
|
Add these labels to your docker-compose override:
|
||||||
|
|
||||||
|
```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"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Using Certbot
|
||||||
|
|
||||||
1. Update `nginx/sites-enabled/default` with your domain
|
1. Update `nginx/sites-enabled/default` with your domain
|
||||||
2. Run certbot:
|
2. Run certbot:
|
||||||
|
|
||||||
@@ -294,13 +355,18 @@ docker-compose -f docker-compose.prod.yml up -d
|
|||||||
|
|
||||||
- [ ] Strong JWT_SECRET (min 32 chars)
|
- [ ] Strong JWT_SECRET (min 32 chars)
|
||||||
- [ ] Strong database password
|
- [ ] Strong database password
|
||||||
|
- [ ] Admin password changed from auto-generated one
|
||||||
- [ ] SSL/HTTPS enabled
|
- [ ] SSL/HTTPS enabled
|
||||||
- [ ] Firewall configured (only 80/443 open)
|
- [ ] Firewall configured (only 80/443 open)
|
||||||
- [ ] Regular security updates
|
- [ ] Regular security updates
|
||||||
- [ ] Backup encryption
|
- [ ] Backup encryption
|
||||||
- [ ] Access logs monitored
|
- [ ] Access logs monitored
|
||||||
- [ ] Rate limiting enabled
|
- [ ] Rate limiting enabled (built-in)
|
||||||
- [ ] File upload restrictions configured
|
- [ ] File upload restrictions configured
|
||||||
|
- [ ] Password complexity requirements configured (Admin > Settings)
|
||||||
|
- [ ] Session timeout configured (default 60 min)
|
||||||
|
- [ ] Umami analytics configured (if using)
|
||||||
|
- [ ] SMTP credentials secured with app-specific password
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
|
|||||||
@@ -1,714 +0,0 @@
|
|||||||
# 🚀 Production Todo List - PicPeak Enhancements
|
|
||||||
|
|
||||||
**Priority:** HIGH - These are production fixes and enhancements
|
|
||||||
**Estimated Time:** 2-3 days
|
|
||||||
**Status:** Ready for Implementation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Action Items Overview
|
|
||||||
|
|
||||||
1. [Password Complexity Settings](#1-password-complexity-settings)
|
|
||||||
2. [Gallery Login Page - Remove Event Date](#2-gallery-login-page---remove-event-date)
|
|
||||||
3. [Analytics Umami Configuration Check](#3-analytics-umami-configuration-check)
|
|
||||||
4. [Analytics Numbers Accuracy Fix](#4-analytics-numbers-accuracy-fix)
|
|
||||||
5. [Missing Translation Key Fix](#5-missing-translation-key-fix)
|
|
||||||
6. [Complete Translation Audit](#6-complete-translation-audit)
|
|
||||||
7. [CMS Page Long German Text Formatting](#7-cms-page-long-german-text-formatting)
|
|
||||||
8. [Event Creation Date Format Fix](#8-event-creation-date-format-fix)
|
|
||||||
9. [Language Selector Country Flags Chrome Fix](#9-language-selector-country-flags-chrome-fix)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Password Complexity Settings
|
|
||||||
|
|
||||||
**Problem:** Admin security tab only has password minimum length setting, no complexity requirements.
|
|
||||||
|
|
||||||
**Current State:**
|
|
||||||
- File: `frontend/src/pages/admin/SettingsPage.tsx` (lines 635-669)
|
|
||||||
- Backend: `backend/src/utils/passwordValidation.js` has complexity logic but not exposed in settings
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
|
|
||||||
### Frontend Changes:
|
|
||||||
```typescript
|
|
||||||
// File: frontend/src/pages/admin/SettingsPage.tsx
|
|
||||||
// Add to securitySettings state (around line 61):
|
|
||||||
const [securitySettings, setSecuritySettings] = useState({
|
|
||||||
require_password: true,
|
|
||||||
password_min_length: 8,
|
|
||||||
password_complexity_level: 'medium', // ADD THIS
|
|
||||||
enable_2fa: false,
|
|
||||||
session_timeout_minutes: 60,
|
|
||||||
max_login_attempts: 5,
|
|
||||||
enable_recaptcha: false,
|
|
||||||
recaptcha_site_key: '',
|
|
||||||
recaptcha_secret_key: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add complexity setting UI after password_min_length (around line 660):
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-neutral-700 mb-1">
|
|
||||||
{t('settings.security.passwordComplexity')}
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={securitySettings.password_complexity_level}
|
|
||||||
onChange={(e) => setSecuritySettings(prev => ({ ...prev, password_complexity_level: e.target.value }))}
|
|
||||||
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
|
||||||
>
|
|
||||||
<option value="low">{t('settings.security.complexityLow')}</option>
|
|
||||||
<option value="medium">{t('settings.security.complexityMedium')}</option>
|
|
||||||
<option value="high">{t('settings.security.complexityHigh')}</option>
|
|
||||||
</select>
|
|
||||||
<p className="text-xs text-neutral-500 mt-1">
|
|
||||||
{t('settings.security.complexityHelp')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Translation Updates:
|
|
||||||
```json
|
|
||||||
// File: frontend/src/i18n/locales/en.json (add to settings.security):
|
|
||||||
"passwordComplexity": "Password Complexity Level",
|
|
||||||
"complexityLow": "Low - Length only",
|
|
||||||
"complexityMedium": "Medium - Letters and numbers",
|
|
||||||
"complexityHigh": "High - Letters, numbers, and symbols",
|
|
||||||
"complexityHelp": "Controls password requirements for new gallery passwords"
|
|
||||||
|
|
||||||
// File: frontend/src/i18n/locales/de.json (add to settings.security):
|
|
||||||
"passwordComplexity": "Passwort-Komplexitätsstufe",
|
|
||||||
"complexityLow": "Niedrig - Nur Länge",
|
|
||||||
"complexityMedium": "Mittel - Buchstaben und Zahlen",
|
|
||||||
"complexityHigh": "Hoch - Buchstaben, Zahlen und Symbole",
|
|
||||||
"complexityHelp": "Steuert Passwort-Anforderungen für neue Galerie-Passwörter"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backend Changes:
|
|
||||||
```javascript
|
|
||||||
// File: backend/src/utils/passwordValidation.js
|
|
||||||
// Update PASSWORD_CONFIG based on settings (around line 8):
|
|
||||||
const getPasswordConfigFromSettings = async () => {
|
|
||||||
const { db } = require('../database/db');
|
|
||||||
const settings = await db('admin_settings').select('key', 'value');
|
|
||||||
const settingsMap = settings.reduce((acc, setting) => {
|
|
||||||
acc[setting.key] = setting.value;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const complexityLevel = settingsMap.security_password_complexity_level || 'medium';
|
|
||||||
|
|
||||||
return {
|
|
||||||
...PASSWORD_CONFIG,
|
|
||||||
minLength: parseInt(settingsMap.security_password_min_length) || 8,
|
|
||||||
requireUppercase: complexityLevel !== 'low',
|
|
||||||
requireLowercase: complexityLevel !== 'low',
|
|
||||||
requireNumbers: complexityLevel === 'high' || complexityLevel === 'medium',
|
|
||||||
requireSpecialChars: complexityLevel === 'high',
|
|
||||||
minStrengthScore: complexityLevel === 'high' ? 3 : (complexityLevel === 'medium' ? 2 : 1)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Gallery Login Page - Remove Event Date
|
|
||||||
|
|
||||||
**Problem:** Gallery login shows event date which is often used as password, creating security risk.
|
|
||||||
|
|
||||||
**Current State:**
|
|
||||||
- File: `frontend/src/pages/GalleryPage.tsx` (lines 275-285)
|
|
||||||
- Shows both event name and date with calendar icon
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
|
|
||||||
### Frontend Changes:
|
|
||||||
```typescript
|
|
||||||
// File: frontend/src/pages/GalleryPage.tsx
|
|
||||||
// Replace the date display section (around lines 280-285):
|
|
||||||
|
|
||||||
// REMOVE THIS:
|
|
||||||
/*
|
|
||||||
<div className="flex items-center justify-center text-xs sm:text-sm" style={{ color: 'var(--color-text, #171717)', opacity: 0.7 }}>
|
|
||||||
<Calendar className="w-3 h-3 sm:w-4 sm:h-4 mr-1" />
|
|
||||||
<span className="truncate">{format(parseISO(galleryInfo!.event_date), 'PP')}</span>
|
|
||||||
</div>
|
|
||||||
*/
|
|
||||||
|
|
||||||
// REPLACE WITH:
|
|
||||||
<div className="text-center text-xs sm:text-sm" style={{ color: 'var(--color-text, #171717)', opacity: 0.7 }}>
|
|
||||||
<span className="truncate">{galleryInfo?.event_type ? t(`events.types.${galleryInfo.event_type}`) : ''}</span>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Additional Layout Improvements:
|
|
||||||
```typescript
|
|
||||||
// File: frontend/src/pages/GalleryPage.tsx
|
|
||||||
// Update the header section for better visual balance (around line 275):
|
|
||||||
<div className="text-center mb-4 sm:mb-6">
|
|
||||||
<img
|
|
||||||
src={settingsData?.branding_logo_url ?
|
|
||||||
buildResourceUrl(settingsData.branding_logo_url) :
|
|
||||||
'/picpeak-logo-transparent.png'
|
|
||||||
}
|
|
||||||
alt={settingsData?.branding_company_name || 'PicPeak'}
|
|
||||||
className="h-12 sm:h-16 lg:h-20 w-auto object-contain mx-auto mb-3 sm:mb-4"
|
|
||||||
/>
|
|
||||||
<h1 className="text-xl sm:text-2xl lg:text-3xl font-bold mb-2 px-2" style={{ color: 'var(--color-text, #171717)' }}>
|
|
||||||
{galleryInfo?.event_name}
|
|
||||||
</h1>
|
|
||||||
{/* Event type instead of date */}
|
|
||||||
{galleryInfo?.event_type && (
|
|
||||||
<div className="text-center text-sm" style={{ color: 'var(--color-text, #171717)', opacity: 0.7 }}>
|
|
||||||
<span className="px-3 py-1 bg-white/20 rounded-full backdrop-blur-sm">
|
|
||||||
{t(`events.types.${galleryInfo.event_type}`)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Analytics Umami Configuration Check
|
|
||||||
|
|
||||||
**Problem:** Analytics page shows "Umami Analytics Not Configured" even when configured in settings.
|
|
||||||
|
|
||||||
**Current State:**
|
|
||||||
- File: `frontend/src/pages/admin/AnalyticsPage.tsx` (lines 67-87)
|
|
||||||
- Check logic may not be working correctly
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
|
|
||||||
### Frontend Fix:
|
|
||||||
```typescript
|
|
||||||
// File: frontend/src/pages/admin/AnalyticsPage.tsx
|
|
||||||
// Fix the Umami configuration check (around lines 67-87):
|
|
||||||
|
|
||||||
// REPLACE the useEffect:
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchUmamiConfig = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/public/settings`);
|
|
||||||
const settings = await response.json();
|
|
||||||
|
|
||||||
// Check if Umami is properly configured
|
|
||||||
const isConfigured = settings.analytics_umami_enabled &&
|
|
||||||
settings.analytics_umami_url &&
|
|
||||||
settings.analytics_umami_website_id;
|
|
||||||
|
|
||||||
if (isConfigured) {
|
|
||||||
setUmamiConfig({
|
|
||||||
url: settings.analytics_umami_url,
|
|
||||||
shareUrl: settings.analytics_umami_share_url,
|
|
||||||
websiteId: settings.analytics_umami_website_id,
|
|
||||||
enabled: true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fall back to environment variables
|
|
||||||
const envConfigured = import.meta.env.VITE_UMAMI_URL &&
|
|
||||||
import.meta.env.VITE_UMAMI_WEBSITE_ID;
|
|
||||||
|
|
||||||
setUmamiConfig({
|
|
||||||
url: import.meta.env.VITE_UMAMI_URL,
|
|
||||||
shareUrl: import.meta.env.VITE_UMAMI_SHARE_URL,
|
|
||||||
websiteId: import.meta.env.VITE_UMAMI_WEBSITE_ID,
|
|
||||||
enabled: envConfigured
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch Umami config:', error);
|
|
||||||
setUmamiConfig({ enabled: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUmamiConfig();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Update the configuration notice condition (around line 441):
|
|
||||||
{!umamiConfig.enabled && (
|
|
||||||
<Card padding="md" className="mt-6 bg-amber-50 border-amber-200">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<Activity className="w-5 h-5 text-amber-600 flex-shrink-0" />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-amber-900">{t('analytics.notConfigured')}</p>
|
|
||||||
<p className="text-sm text-amber-700 mt-1">
|
|
||||||
{t('analytics.configureInstructions')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Analytics Numbers Accuracy Fix
|
|
||||||
|
|
||||||
**Problem:** Dashboard shows correct numbers but analytics page shows different numbers.
|
|
||||||
|
|
||||||
**Current State:**
|
|
||||||
- Dashboard: `frontend/src/services/admin.service.ts` `getDashboardStats()`
|
|
||||||
- Analytics: `frontend/src/services/admin.service.ts` `getAnalytics()`
|
|
||||||
- Backend: Different endpoints with potentially different calculation logic
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
|
|
||||||
### Backend Investigation and Fix:
|
|
||||||
```javascript
|
|
||||||
// File: backend/src/routes/adminDashboard.js
|
|
||||||
// Ensure consistent calculation logic in both /stats and /analytics endpoints
|
|
||||||
|
|
||||||
// Update the analytics endpoint (around line 216) to use the same calculation as stats:
|
|
||||||
router.get('/analytics', adminAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const days = sanitizeDays(req.query.days || 7);
|
|
||||||
|
|
||||||
// Use same calculation logic as /stats endpoint
|
|
||||||
const thirtyDaysAgo = new Date();
|
|
||||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - days);
|
|
||||||
|
|
||||||
// Get total downloads - SAME logic as /stats
|
|
||||||
const totalDownloads = await db('access_logs')
|
|
||||||
.whereIn('action', ['download', 'download_all'])
|
|
||||||
.where('timestamp', '>=', thirtyDaysAgo.toISOString())
|
|
||||||
.count('id as count')
|
|
||||||
.first();
|
|
||||||
|
|
||||||
// Get total views - SAME logic as /stats
|
|
||||||
const totalViews = await db('access_logs')
|
|
||||||
.where('action', 'view')
|
|
||||||
.where('timestamp', '>=', thirtyDaysAgo.toISOString())
|
|
||||||
.count('id as count')
|
|
||||||
.first();
|
|
||||||
|
|
||||||
// ... rest of the analytics logic
|
|
||||||
|
|
||||||
// Add totals to response for verification
|
|
||||||
res.json({
|
|
||||||
chartData: dates,
|
|
||||||
topGalleries,
|
|
||||||
devices,
|
|
||||||
totals: {
|
|
||||||
totalViews: totalViews.count,
|
|
||||||
totalDownloads: totalDownloads.count,
|
|
||||||
period: `${days} days`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Analytics error:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to fetch analytics data' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend Verification:
|
|
||||||
```typescript
|
|
||||||
// File: frontend/src/pages/admin/AnalyticsPage.tsx
|
|
||||||
// Add debug information in development (around line 107):
|
|
||||||
|
|
||||||
const analytics: ComponentAnalyticsData | undefined = React.useMemo(() => {
|
|
||||||
if (!apiData) return undefined;
|
|
||||||
|
|
||||||
// Calculate totals from chart data
|
|
||||||
const totalViews = apiData.chartData.reduce((sum, day) => sum + day.views, 0);
|
|
||||||
const totalDownloads = apiData.chartData.reduce((sum, day) => sum + day.downloads, 0);
|
|
||||||
|
|
||||||
// Debug: Compare with API totals in development
|
|
||||||
if (process.env.NODE_ENV === 'development' && apiData.totals) {
|
|
||||||
console.log('Analytics Debug:', {
|
|
||||||
calculatedViews: totalViews,
|
|
||||||
apiTotalViews: apiData.totals.totalViews,
|
|
||||||
calculatedDownloads: totalDownloads,
|
|
||||||
apiTotalDownloads: apiData.totals.totalDownloads,
|
|
||||||
period: apiData.totals.period
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... rest of the calculation
|
|
||||||
}, [apiData]);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Missing Translation Key Fix
|
|
||||||
|
|
||||||
**Problem:** Missing translation key `admin.activities.analytics_settings_updated` in recent activities.
|
|
||||||
|
|
||||||
**Current State:**
|
|
||||||
- Key not found in `frontend/src/i18n/locales/en.json` or `de.json`
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
|
|
||||||
### Translation Updates:
|
|
||||||
```json
|
|
||||||
// File: frontend/src/i18n/locales/en.json
|
|
||||||
// Add to admin.activities section (around line 785):
|
|
||||||
"analytics_settings_updated": "Analytics settings updated"
|
|
||||||
|
|
||||||
// File: frontend/src/i18n/locales/de.json
|
|
||||||
// Add to admin.activities section (around line 710):
|
|
||||||
"analytics_settings_updated": "Analytik-Einstellungen aktualisiert"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backend Activity Logging:
|
|
||||||
```javascript
|
|
||||||
// File: backend/src/routes/adminSettings.js (or wherever analytics settings are updated)
|
|
||||||
// Ensure activity is logged with correct key:
|
|
||||||
|
|
||||||
await logActivity(req.admin.id, 'analytics_settings_updated', {
|
|
||||||
settingsUpdated: Object.keys(updateData).filter(key => key.startsWith('analytics_')),
|
|
||||||
timestamp: new Date()
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Complete Translation Audit
|
|
||||||
|
|
||||||
**Problem:** Need to check all recent activity types for missing translations.
|
|
||||||
|
|
||||||
**Current State:**
|
|
||||||
- Activity types defined in backend, translations in frontend
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
|
|
||||||
### Audit Script:
|
|
||||||
```bash
|
|
||||||
# Create a script to find missing translation keys
|
|
||||||
# File: scripts/audit-translations.js
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// Read translation files
|
|
||||||
const enTranslations = JSON.parse(fs.readFileSync('frontend/src/i18n/locales/en.json', 'utf8'));
|
|
||||||
const deTranslations = JSON.parse(fs.readFileSync('frontend/src/i18n/locales/de.json', 'utf8'));
|
|
||||||
|
|
||||||
// Common activity types that should exist
|
|
||||||
const requiredActivityKeys = [
|
|
||||||
'event_created', 'event_updated', 'event_deleted', 'event_archived',
|
|
||||||
'photos_uploaded', 'photo_deleted', 'photos_bulk_deleted',
|
|
||||||
'archive_downloaded', 'archive_deleted', 'archive_restored',
|
|
||||||
'email_config_updated', 'email_template_updated',
|
|
||||||
'branding_updated', 'theme_updated', 'analytics_settings_updated',
|
|
||||||
'general_settings_updated', 'security_settings_updated',
|
|
||||||
'category_created', 'category_updated', 'category_deleted',
|
|
||||||
'cms_page_updated', 'favicon_uploaded',
|
|
||||||
'bulk_download', 'gallery_password_entry', 'expiration_warning_viewed'
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log('Missing English translations:');
|
|
||||||
requiredActivityKeys.forEach(key => {
|
|
||||||
if (!enTranslations.admin?.activities?.[key]) {
|
|
||||||
console.log(`- admin.activities.${key}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nMissing German translations:');
|
|
||||||
requiredActivityKeys.forEach(key => {
|
|
||||||
if (!deTranslations.admin?.activities?.[key]) {
|
|
||||||
console.log(`- admin.activities.${key}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Missing Translations to Add:
|
|
||||||
```json
|
|
||||||
// File: frontend/src/i18n/locales/en.json
|
|
||||||
// Add any missing keys to admin.activities:
|
|
||||||
"analytics_settings_updated": "Analytics settings updated",
|
|
||||||
"cms_page_updated": "CMS page updated: {{page}}",
|
|
||||||
"security_settings_updated": "Security settings updated",
|
|
||||||
"password_reset": "Password reset for: {{eventName}}",
|
|
||||||
"admin_logout": "Admin {{actorName}} logged out",
|
|
||||||
"system_activity": "System activity: {{type}}"
|
|
||||||
|
|
||||||
// File: frontend/src/i18n/locales/de.json
|
|
||||||
// German equivalents:
|
|
||||||
"analytics_settings_updated": "Analytik-Einstellungen aktualisiert",
|
|
||||||
"cms_page_updated": "CMS-Seite aktualisiert: {{page}}",
|
|
||||||
"security_settings_updated": "Sicherheitseinstellungen aktualisiert",
|
|
||||||
"password_reset": "Passwort zurückgesetzt für: {{eventName}}",
|
|
||||||
"admin_logout": "Admin {{actorName}} abgemeldet",
|
|
||||||
"system_activity": "Systemaktivität: {{type}}"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. CMS Page Long German Text Formatting
|
|
||||||
|
|
||||||
**Problem:** Long German text like "Datenschutzerklärung" pushes image to left and looks ugly.
|
|
||||||
|
|
||||||
**Current State:**
|
|
||||||
- File: `frontend/src/pages/admin/CMSPageEnhanced.tsx` (lines 130-170)
|
|
||||||
- File: `frontend/src/pages/admin/CMSPage.tsx` (lines 90-110)
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
|
|
||||||
### CSS Fix:
|
|
||||||
```typescript
|
|
||||||
// File: frontend/src/pages/admin/CMSPageEnhanced.tsx
|
|
||||||
// Update the page selection buttons (around line 130):
|
|
||||||
|
|
||||||
<button
|
|
||||||
key={page.slug}
|
|
||||||
onClick={() => {
|
|
||||||
if (hasUnsavedChanges) {
|
|
||||||
if (confirm('You have unsaved changes. Do you want to save them?')) {
|
|
||||||
handleSave();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setSelectedPage(page.slug);
|
|
||||||
}}
|
|
||||||
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${
|
|
||||||
selectedPage === page.slug
|
|
||||||
? 'bg-primary-100 text-primary-700 border border-primary-300'
|
|
||||||
: 'bg-white border border-neutral-200 hover:bg-neutral-50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FileText className="w-5 h-5 flex-shrink-0" />
|
|
||||||
<div className="flex-1 min-w-0"> {/* Add min-w-0 for text overflow */}
|
|
||||||
<p className="font-medium text-sm truncate" title={t(`legal.${page.slug}`)}>
|
|
||||||
{t(`legal.${page.slug}`)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-neutral-500 truncate">/{page.slug}</p>
|
|
||||||
</div>
|
|
||||||
{selectedPage === page.slug && hasUnsavedChanges && (
|
|
||||||
<div className="w-2 h-2 bg-yellow-500 rounded-full flex-shrink-0" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Alternative - Responsive Layout:
|
|
||||||
```typescript
|
|
||||||
// File: frontend/src/pages/admin/CMSPageEnhanced.tsx
|
|
||||||
// Alternative: Use responsive text sizing
|
|
||||||
|
|
||||||
<p className="font-medium text-sm sm:text-base truncate" title={t(`legal.${page.slug}`)}>
|
|
||||||
{/* For very long German words, show abbreviated version */}
|
|
||||||
{t(`legal.${page.slug}`).length > 15
|
|
||||||
? `${t(`legal.${page.slug}`).substring(0, 12)}...`
|
|
||||||
: t(`legal.${page.slug}`)
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Event Creation Date Format Fix
|
|
||||||
|
|
||||||
**Problem:** Event creation page uses browser English format instead of saved admin settings date format.
|
|
||||||
|
|
||||||
**Current State:**
|
|
||||||
- Files: `frontend/src/pages/admin/CreateEventPageEnhanced.tsx`, `CreateEventPage.tsx`
|
|
||||||
- Uses browser locale instead of admin date format settings
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
|
|
||||||
### Hook Enhancement:
|
|
||||||
```typescript
|
|
||||||
// File: frontend/src/hooks/useLocalizedDate.ts
|
|
||||||
// Add admin settings integration:
|
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { format as dateFnsFormat, formatDistanceToNow as dateFnsFormatDistanceToNow } from 'date-fns';
|
|
||||||
import { de, enUS, enGB } from 'date-fns/locale';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { settingsService } from '../services/settings.service';
|
|
||||||
|
|
||||||
export const useLocalizedDate = () => {
|
|
||||||
const { i18n } = useTranslation();
|
|
||||||
|
|
||||||
// Fetch admin date format settings
|
|
||||||
const { data: settings } = useQuery({
|
|
||||||
queryKey: ['admin-date-settings'],
|
|
||||||
queryFn: () => settingsService.getAllSettings(),
|
|
||||||
staleTime: 10 * 60 * 1000, // Cache for 10 minutes
|
|
||||||
});
|
|
||||||
|
|
||||||
const getLocale = () => {
|
|
||||||
// Use admin settings if available, otherwise fall back to i18n language
|
|
||||||
const savedFormat = settings?.general_date_format;
|
|
||||||
if (savedFormat?.locale) {
|
|
||||||
switch (savedFormat.locale) {
|
|
||||||
case 'en-US': return enUS;
|
|
||||||
case 'en-GB': return enGB;
|
|
||||||
case 'de': return de;
|
|
||||||
default: return i18n.language === 'de' ? de : enUS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i18n.language === 'de' ? de : enUS;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDateFormat = () => {
|
|
||||||
const savedFormat = settings?.general_date_format?.format;
|
|
||||||
if (savedFormat) {
|
|
||||||
// Convert admin format to date-fns format
|
|
||||||
switch (savedFormat) {
|
|
||||||
case 'DD/MM/YYYY': return 'dd/MM/yyyy';
|
|
||||||
case 'MM/DD/YYYY': return 'MM/dd/yyyy';
|
|
||||||
case 'YYYY-MM-DD': return 'yyyy-MM-dd';
|
|
||||||
case 'DD.MM.YYYY': return 'dd.MM.yyyy';
|
|
||||||
default: return 'dd/MM/yyyy';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i18n.language === 'de' ? 'dd.MM.yyyy' : 'MM/dd/yyyy';
|
|
||||||
};
|
|
||||||
|
|
||||||
const format = (date: Date | string, formatStr?: string) => {
|
|
||||||
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
|
||||||
const finalFormat = formatStr || getDateFormat();
|
|
||||||
return dateFnsFormat(dateObj, finalFormat, { locale: getLocale() });
|
|
||||||
};
|
|
||||||
|
|
||||||
// ... rest of the hook
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Event Creation Page Fix:
|
|
||||||
```typescript
|
|
||||||
// File: frontend/src/pages/admin/CreateEventPageEnhanced.tsx
|
|
||||||
// Update the expiration date display (around line 495):
|
|
||||||
|
|
||||||
{formData.event_date && (
|
|
||||||
<p className="mt-2 text-sm text-neutral-500">
|
|
||||||
{t('events.expiresOn')}: {format(addDays(new Date(formData.event_date), formData.expires_in_days), 'PPP')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Language Selector Country Flags Chrome Fix
|
|
||||||
|
|
||||||
**Problem:** Country flags not showing in Chrome browser on Windows in admin language selector.
|
|
||||||
|
|
||||||
**Current State:**
|
|
||||||
- File: `frontend/src/components/common/LanguageSelector.tsx` (lines 5-8)
|
|
||||||
- Uses emoji flags: `🇬🇧`, `🇩🇪`
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
|
|
||||||
### SVG Icon Replacement:
|
|
||||||
```typescript
|
|
||||||
// File: frontend/src/components/common/LanguageSelector.tsx
|
|
||||||
// Replace emoji flags with SVG icons or image flags:
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Globe } from 'lucide-react';
|
|
||||||
|
|
||||||
// SVG flag components for better browser compatibility
|
|
||||||
const FlagGB: React.FC<{ className?: string }> = ({ className = "w-4 h-4" }) => (
|
|
||||||
<svg className={className} viewBox="0 0 640 480" fill="none">
|
|
||||||
<path fill="#012169" d="M0 0h640v480H0z"/>
|
|
||||||
<path fill="#FFF" d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0h75z"/>
|
|
||||||
<path fill="#C8102E" d="m424 281 216 159v40L369 281h55zm-184 20 6 35L54 480H0l246-179zM640 0v3L391 191l2-44L590 0h50zM0 0l239 176h-60L0 42V0z"/>
|
|
||||||
<path fill="#FFF" d="M241 0v480h160V0H241zM0 160v160h640V160H0z"/>
|
|
||||||
<path fill="#C8102E" d="M0 193v96h640v-96H0zM273 0v480h96V0h-96z"/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
const FlagDE: React.FC<{ className?: string }> = ({ className = "w-4 h-4" }) => (
|
|
||||||
<svg className={className} viewBox="0 0 640 480" fill="none">
|
|
||||||
<path fill="#ffce00" d="M0 320h640v160H0z"/>
|
|
||||||
<path d="M0 0h640v160H0z"/>
|
|
||||||
<path fill="#d00" d="M0 160h640v160H0z"/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
const languages = [
|
|
||||||
{ code: 'en', name: 'English', flag: FlagGB },
|
|
||||||
{ code: 'de', name: 'Deutsch', flag: FlagDE },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const LanguageSelector: React.FC = () => {
|
|
||||||
const { i18n } = useTranslation();
|
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
|
||||||
|
|
||||||
const currentLanguage = languages.find(lang => lang.code === i18n.language) || languages[0];
|
|
||||||
|
|
||||||
const handleLanguageChange = (languageCode: string) => {
|
|
||||||
i18n.changeLanguage(languageCode);
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-neutral-700 bg-white border border-neutral-300 rounded-lg hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
||||||
>
|
|
||||||
<Globe className="w-4 h-4" />
|
|
||||||
<currentLanguage.flag className="w-4 h-4" />
|
|
||||||
<span>{currentLanguage.name}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{isOpen && (
|
|
||||||
<div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-neutral-200 py-1 z-50">
|
|
||||||
{languages.map((language) => (
|
|
||||||
<button
|
|
||||||
key={language.code}
|
|
||||||
onClick={() => handleLanguageChange(language.code)}
|
|
||||||
className={`w-full text-left px-4 py-2 text-sm hover:bg-neutral-50 flex items-center gap-3 ${
|
|
||||||
language.code === i18n.language
|
|
||||||
? 'text-primary-600 bg-primary-50'
|
|
||||||
: 'text-neutral-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<language.flag className="w-4 h-4" />
|
|
||||||
<span>{language.name}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Alternative - Image Flags:
|
|
||||||
```typescript
|
|
||||||
// Alternative solution using flag images:
|
|
||||||
const languages = [
|
|
||||||
{ code: 'en', name: 'English', flag: '/flags/gb.svg' },
|
|
||||||
{ code: 'de', name: 'Deutsch', flag: '/flags/de.svg' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Add images to public/flags/ directory
|
|
||||||
// Use: <img src={language.flag} alt={language.name} className="w-4 h-4" />
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Testing Instructions
|
|
||||||
|
|
||||||
### After implementing each fix:
|
|
||||||
|
|
||||||
1. **Password Complexity**: Test different complexity levels in admin settings
|
|
||||||
2. **Gallery Login**: Verify event date is hidden on gallery login pages
|
|
||||||
3. **Analytics Check**: Verify "Not Configured" message appears/disappears correctly
|
|
||||||
4. **Analytics Numbers**: Compare dashboard vs analytics page numbers
|
|
||||||
5. **Translations**: Check recent activities display correct translations
|
|
||||||
6. **CMS Formatting**: Test with long German page names
|
|
||||||
7. **Date Format**: Test event creation with different admin date settings
|
|
||||||
8. **Language Flags**: Test language selector in Chrome on Windows
|
|
||||||
|
|
||||||
### Regression Testing:
|
|
||||||
- [ ] Gallery login still works correctly
|
|
||||||
- [ ] Analytics page displays correctly when Umami is configured
|
|
||||||
- [ ] Admin settings save and load correctly
|
|
||||||
- [ ] Event creation works with all date formats
|
|
||||||
- [ ] Language switching works in all browsers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Notes
|
|
||||||
|
|
||||||
- All changes maintain backward compatibility
|
|
||||||
- No database schema changes required
|
|
||||||
- Frontend changes are non-breaking
|
|
||||||
- Can be deployed incrementally
|
|
||||||
- All text is properly internationalized
|
|
||||||
|
|
||||||
**⚠️ Important**: Test each fix in isolation before combining, especially the analytics changes as they affect production data display.
|
|
||||||
+33
-16
@@ -3,39 +3,56 @@
|
|||||||
|
|
||||||
# Application
|
# Application
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3001
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
JWT_SECRET=your-very-secure-jwt-secret-at-least-32-characters-long
|
# Generate with: openssl rand -base64 32
|
||||||
|
JWT_SECRET=your-very-secure-jwt-secret-at-least-32-characters-long-example123456
|
||||||
|
|
||||||
# URLs
|
# URLs (adjust for your domain)
|
||||||
ADMIN_URL=https://yourdomain.com
|
ADMIN_URL=https://photos.example.com
|
||||||
FRONTEND_URL=https://yourdomain.com
|
FRONTEND_URL=https://photos.example.com
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
DATABASE_CLIENT=pg
|
DATABASE_CLIENT=pg
|
||||||
DB_HOST=db
|
DB_HOST=localhost
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_USER=picpeak
|
DB_USER=picpeak
|
||||||
DB_PASSWORD=your-secure-database-password
|
DB_PASSWORD=your-secure-database-password-change-this
|
||||||
DB_NAME=picpeak
|
DB_NAME=picpeak
|
||||||
|
|
||||||
# Email Configuration
|
# Email Configuration (Examples for common providers)
|
||||||
SMTP_HOST=smtp.example.com
|
# Gmail example:
|
||||||
|
# SMTP_HOST=smtp.gmail.com
|
||||||
|
# SMTP_PORT=587
|
||||||
|
# SMTP_SECURE=false
|
||||||
|
# SMTP_USER=your-email@gmail.com
|
||||||
|
# SMTP_PASS=your-app-specific-password
|
||||||
|
|
||||||
|
# SendGrid example:
|
||||||
|
SMTP_HOST=smtp.sendgrid.net
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=false
|
SMTP_SECURE=false
|
||||||
SMTP_USER=your-smtp-username
|
SMTP_USER=apikey
|
||||||
SMTP_PASS=your-smtp-password
|
SMTP_PASS=your-sendgrid-api-key
|
||||||
EMAIL_FROM=noreply@yourdomain.com
|
EMAIL_FROM=noreply@example.com
|
||||||
|
|
||||||
# Storage Paths (Docker)
|
# Storage Paths
|
||||||
|
# Docker deployment:
|
||||||
STORAGE_PATH=/app/storage
|
STORAGE_PATH=/app/storage
|
||||||
EVENTS_PATH=/app/storage/events
|
EVENTS_PATH=/app/storage/events
|
||||||
ARCHIVE_PATH=/app/storage/events/archived
|
ARCHIVE_PATH=/app/storage/events/archived
|
||||||
|
|
||||||
# Analytics (Optional)
|
# Local development:
|
||||||
UMAMI_URL=https://analytics.yourdomain.com
|
# STORAGE_PATH=./storage
|
||||||
UMAMI_WEBSITE_ID=your-website-id
|
# EVENTS_PATH=./storage/events
|
||||||
|
# ARCHIVE_PATH=./storage/events/archived
|
||||||
|
|
||||||
|
# Analytics Backend Configuration (OPTIONAL)
|
||||||
|
# Used for server-side tracking only
|
||||||
|
# Primary configuration should be done through Admin UI > Settings > Analytics
|
||||||
|
# UMAMI_URL=https://analytics.example.com
|
||||||
|
# UMAMI_WEBSITE_ID=b4d3c2a1-5678-90ab-cdef-1234567890ab
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.71",
|
"version": "1.0.74",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.71",
|
"version": "1.0.74",
|
||||||
"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.71",
|
"version": "1.0.74",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+13
-8
@@ -1,11 +1,16 @@
|
|||||||
# Backend API URL
|
# Backend API URL
|
||||||
VITE_API_URL=http://localhost:3000
|
# For local development:
|
||||||
|
VITE_API_URL=http://localhost:3001
|
||||||
|
|
||||||
# Umami Analytics Configuration
|
# For production behind reverse proxy (Traefik, nginx, etc):
|
||||||
# Get these values from your Umami installation
|
# VITE_API_URL=/api
|
||||||
VITE_UMAMI_URL=https://analytics.yourdomain.com
|
|
||||||
VITE_UMAMI_WEBSITE_ID=your-website-id-from-umami
|
|
||||||
|
|
||||||
# Optional: Umami share URL for embedding full dashboard
|
# Umami Analytics Configuration (OPTIONAL - Fallback only)
|
||||||
# This is the public share URL from Umami's share feature
|
# NOTE: Primary Umami configuration should be done through Admin UI > Settings > Analytics
|
||||||
VITE_UMAMI_SHARE_URL=https://analytics.yourdomain.com/share/your-share-id/photo-sharing
|
# These environment variables serve as fallbacks when backend settings are not available
|
||||||
|
# Useful for: development environments, initial setup, or when backend is unavailable
|
||||||
|
#
|
||||||
|
# Example values:
|
||||||
|
# VITE_UMAMI_URL=https://analytics.example.com
|
||||||
|
# VITE_UMAMI_WEBSITE_ID=abc123def-4567-89ab-cdef-0123456789ab
|
||||||
|
# VITE_UMAMI_SHARE_URL=https://analytics.example.com/share/xyz789/wedding-photos
|
||||||
@@ -8,7 +8,11 @@ VITE_API_URL=/api
|
|||||||
# For development or if frontend/backend are on different domains:
|
# For development or if frontend/backend are on different domains:
|
||||||
# VITE_API_URL=https://api.yourdomain.com
|
# VITE_API_URL=https://api.yourdomain.com
|
||||||
|
|
||||||
# Umami Analytics Configuration (optional)
|
# Umami Analytics Configuration (OPTIONAL - Fallback only)
|
||||||
# VITE_UMAMI_URL=https://analytics.yourdomain.com
|
# NOTE: Primary Umami configuration should be done through Admin UI > Settings > Analytics
|
||||||
# VITE_UMAMI_WEBSITE_ID=your-website-id-from-umami
|
# These environment variables serve as fallbacks when backend settings are not available
|
||||||
# VITE_UMAMI_SHARE_URL=https://analytics.yourdomain.com/share/your-share-id/photo-sharing
|
#
|
||||||
|
# Real-world example values:
|
||||||
|
# VITE_UMAMI_URL=https://analytics.picpeak.com
|
||||||
|
# VITE_UMAMI_WEBSITE_ID=b4d3c2a1-5678-90ab-cdef-1234567890ab
|
||||||
|
# VITE_UMAMI_SHARE_URL=https://analytics.picpeak.com/share/Ab3Cd5Fg/picpeak-gallery
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.72",
|
"version": "1.0.74",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.72",
|
"version": "1.0.74",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tiptap/extension-character-count": "^2.26.1",
|
"@tiptap/extension-character-count": "^2.26.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.72",
|
"version": "1.0.74",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { Button, Card, Loading } from '../../components/common';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { adminService } from '../../services/admin.service';
|
import { adminService } from '../../services/admin.service';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { api } from '../../config/api';
|
||||||
|
|
||||||
// Map API response to component format
|
// Map API response to component format
|
||||||
interface ComponentAnalyticsData {
|
interface ComponentAnalyticsData {
|
||||||
@@ -71,23 +72,25 @@ export const AnalyticsPage: React.FC = () => {
|
|||||||
queryFn: () => adminService.getDashboardStats(),
|
queryFn: () => adminService.getDashboardStats(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch public settings to get Umami config
|
// Fetch Umami config from admin settings since we're in admin panel
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUmamiConfig = async () => {
|
const fetchUmamiConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/public/settings`);
|
// Use admin API endpoint with auth token since we're in admin area
|
||||||
|
const response = await api.get('/admin/settings');
|
||||||
|
const settings = response.data;
|
||||||
|
|
||||||
if (!response.ok) {
|
// Transform the settings array to object
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
const settingsMap = settings.reduce((acc: any, setting: any) => {
|
||||||
}
|
acc[setting.key] = setting.value;
|
||||||
|
return acc;
|
||||||
const settings = await response.json();
|
}, {});
|
||||||
|
|
||||||
// Check if Umami is enabled in admin settings
|
// Check if Umami is enabled in admin settings
|
||||||
if (settings.umami_enabled && settings.umami_url && settings.umami_website_id) {
|
if (settingsMap.analytics_umami_enabled && settingsMap.analytics_umami_url && settingsMap.analytics_umami_website_id) {
|
||||||
setUmamiConfig({
|
setUmamiConfig({
|
||||||
url: settings.umami_url,
|
url: settingsMap.analytics_umami_url,
|
||||||
shareUrl: settings.umami_share_url,
|
shareUrl: settingsMap.analytics_umami_share_url,
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user