Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 840b8870ec | |||
| ad495a92c4 | |||
| b428543452 | |||
| 6492cb9ec8 | |||
| 0e0a0b91d1 | |||
| f8fb1c3f4b | |||
| b108f6fe1c | |||
| 61299a33c4 | |||
| 96542d7e35 | |||
| ee855a3502 | |||
| 02c407d431 | |||
| 62617f627f | |||
| 1cbeb75094 | |||
| baa08e9ec9 | |||
| 8d85454ef6 | |||
| 596bba2c1b | |||
| 055de06315 | |||
| ccf59d1d4d | |||
| 4e052966d3 | |||
| ba2c021c45 | |||
| 519518ed6c | |||
| 8a0a4436b0 | |||
| 9854ca2f59 | |||
| 0c989ce086 | |||
| 35e360dcf7 | |||
| a209796b16 | |||
| 7c79052681 | |||
| d560453982 | |||
| ecb3263267 | |||
| 4d929a71ce | |||
| bf705674d5 | |||
| fee369a503 | |||
| ad75818566 | |||
| 65f2c8610d | |||
| 517128fd99 | |||
| 55c8384a25 | |||
| 0064122eff | |||
| 0c783c66d0 | |||
| 47e2351dab | |||
| e1aca6b00c | |||
| 2cb6577f26 | |||
| c51d756503 | |||
| c48b9780df | |||
| 618e2695fd | |||
| a289f97a31 | |||
| 7387a5e9f9 | |||
| 4615a5d795 | |||
| f926cd3adf | |||
| 96b05b5e0c | |||
| 99e47785e4 | |||
| 94f10e1645 | |||
| fe4a476e41 | |||
| e9f92e66d0 | |||
| 58f4217756 | |||
| 76a466c077 | |||
| 9dc3777985 | |||
| e006d73831 | |||
| 63e88c8324 | |||
| 105167fb57 | |||
| 22cc40617f | |||
| 7f28917795 | |||
| 77af2a8415 | |||
| 4c42b4c601 | |||
| 856cdc214c | |||
| 15b244292f | |||
| 558a966f85 | |||
| 1238db58c2 | |||
| 0502ed34c9 | |||
| 1f417c7e30 | |||
| a401fbdc54 | |||
| 247e154afe | |||
| cbfd84ddea | |||
| dc1419c051 | |||
| 2624ea6130 | |||
| 08eeac66eb | |||
| 11769219e4 | |||
| 7750170832 | |||
| 833591681a | |||
| b77e60c37c | |||
| 30f6780484 | |||
| aa39e132aa | |||
| f6a79c815e | |||
| 3c6837bd90 | |||
| 1773ed5f95 | |||
| 62dcaf8555 | |||
| 4ed35f1b16 | |||
| 5eff7dd4a6 | |||
| abbcdb1113 | |||
| 17fc40e65d | |||
| a54a2c0fda | |||
| febacb79ad | |||
| c7875102c5 | |||
| 3dc013d7b1 | |||
| b7c8953cb4 | |||
| d6adde4e09 | |||
| 0bf4764a07 | |||
| 08da01f021 | |||
| b4b09c1650 | |||
| b2ae5f18ad | |||
| 4a7a3bba07 | |||
| b3f240b2a5 | |||
| ba0bf11a1d | |||
| d5790ad635 | |||
| e6757bd51b | |||
| 8588133a4e | |||
| 4b18077573 | |||
| c584369d5d | |||
| 95939d57e6 | |||
| be58146dc7 | |||
| 45ce98806d | |||
| 23b7a848ab | |||
| 0fe6d738b2 | |||
| 3f73d44c5a |
@@ -1,286 +0,0 @@
|
||||
# Security Scan Report - Wedding Photo Sharing Application
|
||||
**Date**: July 13, 2025
|
||||
**Scanner**: Claude Security Audit with --security --validate flags
|
||||
**Overall Risk Level**: MEDIUM-HIGH
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The wedding photo sharing application demonstrates strong security fundamentals with comprehensive input validation, proper authentication mechanisms, and good file security practices. However, several critical issues require immediate attention, particularly around hardcoded secrets, token storage, and Content Security Policy configuration.
|
||||
|
||||
### Security Score: 6.5/10
|
||||
|
||||
**Strengths**: Excellent input validation, parameterized queries, file security, rate limiting
|
||||
**Critical Issues**: Hardcoded JWT secrets, localStorage token storage, weak CSP, console logging in production
|
||||
|
||||
---
|
||||
|
||||
## 🔴 CRITICAL FINDINGS (Immediate Action Required)
|
||||
|
||||
### 1. Hardcoded JWT Secret in Development
|
||||
- **Location**: Backend `.env` file
|
||||
- **Risk**: Token forgery, authentication bypass
|
||||
- **Impact**: Complete authentication compromise
|
||||
- **Remediation**:
|
||||
```bash
|
||||
# Generate secure secret
|
||||
openssl rand -base64 32
|
||||
# Never commit to repository
|
||||
echo ".env" >> .gitignore
|
||||
```
|
||||
|
||||
### 2. Gallery Tokens in localStorage
|
||||
- **Location**: Frontend `api.ts` and auth contexts
|
||||
- **Risk**: XSS token theft
|
||||
- **Impact**: Gallery access compromise
|
||||
- **Remediation**: Move to httpOnly cookies:
|
||||
```typescript
|
||||
Cookies.set(`gallery_token_${slug}`, token, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'strict'
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Weak Content Security Policy
|
||||
- **Location**: Frontend `nginx.conf`
|
||||
- **Risk**: XSS, code injection
|
||||
- **Current**: `unsafe-inline` and `unsafe-eval` allowed
|
||||
- **Remediation**: Implement strict CSP (see detailed recommendations below)
|
||||
|
||||
---
|
||||
|
||||
## 🟠 HIGH SEVERITY FINDINGS
|
||||
|
||||
### 1. Console Logging in Production
|
||||
- **Locations**: 61 instances across frontend
|
||||
- **Risk**: Information disclosure
|
||||
- **Impact**: Leaking sensitive data, debugging info
|
||||
- **Remediation**: Implement environment-aware logging
|
||||
|
||||
### 2. Token Revocation Vulnerability
|
||||
- **Location**: Backend `tokenRevocation.js`
|
||||
- **Risk**: Token manipulation
|
||||
- **Impact**: Bypass revocation checks
|
||||
- **Remediation**: Verify token signature before decoding
|
||||
|
||||
### 3. Source Maps in Production
|
||||
- **Location**: Frontend build configuration
|
||||
- **Risk**: Source code exposure
|
||||
- **Impact**: Reveals application structure
|
||||
- **Remediation**: Disable in production builds
|
||||
|
||||
### 4. Missing Security Headers
|
||||
- **Location**: nginx configuration
|
||||
- **Missing**: HSTS, Permissions-Policy
|
||||
- **Impact**: Various client-side attacks
|
||||
- **Remediation**: Add comprehensive security headers
|
||||
|
||||
---
|
||||
|
||||
## 🟡 MEDIUM SEVERITY FINDINGS
|
||||
|
||||
### 1. Rate Limiting Bypass Potential
|
||||
- **Location**: Backend rate limiter
|
||||
- **Risk**: DoS attacks
|
||||
- **Current**: JWT validation in rate limiter
|
||||
- **Remediation**: Use IP-based limiting only
|
||||
|
||||
### 2. Incomplete SQL Injection Protection
|
||||
- **Location**: Complex dashboard queries
|
||||
- **Risk**: Potential injection in edge cases
|
||||
- **Current**: Mostly parameterized
|
||||
- **Remediation**: Use query builder exclusively
|
||||
|
||||
### 3. Session Management
|
||||
- **Issue**: No gallery token invalidation on password change
|
||||
- **Risk**: Persistent access after compromise
|
||||
- **Remediation**: Implement token revocation
|
||||
|
||||
### 4. Path Traversal in Gallery Slugs
|
||||
- **Location**: Frontend gallery routes
|
||||
- **Risk**: Directory traversal attempts
|
||||
- **Remediation**: Validate and sanitize slugs
|
||||
|
||||
---
|
||||
|
||||
## 🟢 LOW SEVERITY FINDINGS
|
||||
|
||||
### 1. Verbose Error Messages
|
||||
- **Location**: Multiple API endpoints
|
||||
- **Risk**: Information disclosure
|
||||
- **Remediation**: Generic client errors, detailed server logs
|
||||
|
||||
### 2. Weak Gallery Passwords
|
||||
- **Current**: zxcvbn score 2/4 allowed
|
||||
- **Risk**: Brute force attacks
|
||||
- **Remediation**: Increase to score 3/4
|
||||
|
||||
### 3. Missing File Size Validation
|
||||
- **Location**: Frontend upload components
|
||||
- **Risk**: DoS via large uploads
|
||||
- **Remediation**: Add client-side size checks
|
||||
|
||||
---
|
||||
|
||||
## ✅ SECURITY STRENGTHS
|
||||
|
||||
### Authentication & Authorization
|
||||
- JWT with proper expiration (24h/7d)
|
||||
- Token type validation
|
||||
- IP tracking and validation
|
||||
- Password change detection
|
||||
- Token revocation system
|
||||
- Bcrypt with 12 rounds
|
||||
- zxcvbn password strength checking
|
||||
|
||||
### Input Validation & SQL Security
|
||||
- express-validator on all endpoints
|
||||
- Parameterized queries via Knex
|
||||
- SQL injection protection utilities
|
||||
- Path traversal prevention
|
||||
- Comprehensive input sanitization
|
||||
|
||||
### File Security
|
||||
- Magic number verification
|
||||
- MIME type validation
|
||||
- Safe filename generation
|
||||
- Directory traversal protection
|
||||
- File extension whitelist
|
||||
|
||||
### Rate Limiting & DoS Protection
|
||||
- General: 100 req/15min
|
||||
- Auth endpoints: 5 req/15min
|
||||
- Account lockout after failed attempts
|
||||
- Suspicious activity detection
|
||||
|
||||
### Frontend Security
|
||||
- React's built-in XSS protection
|
||||
- DOMPurify for HTML content
|
||||
- No eval() or innerHTML usage
|
||||
- Proper error boundaries
|
||||
- ReCAPTCHA integration
|
||||
|
||||
---
|
||||
|
||||
## 📊 DEPENDENCY ANALYSIS
|
||||
|
||||
### Current Status
|
||||
- **Backend**: 0 vulnerabilities (691 packages)
|
||||
- **Frontend**: 0 vulnerabilities (434 packages)
|
||||
|
||||
### Recommended Updates
|
||||
1. **bcrypt** 5.1.1 → 6.0.0 (performance, compatibility)
|
||||
2. **helmet** 7.2.0 → 8.1.0 (new security features)
|
||||
3. **@tiptap** 2.x → 3.x (security improvements)
|
||||
|
||||
### Supply Chain Assessment
|
||||
- All major dependencies from trusted sources
|
||||
- No typosquatting detected
|
||||
- Regular maintenance observed
|
||||
- MIT/ISC/Apache licenses only
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ REMEDIATION PLAN
|
||||
|
||||
### Phase 1: Critical (Within 24 hours)
|
||||
1. Replace hardcoded JWT secret with secure random value
|
||||
2. Move gallery tokens from localStorage to httpOnly cookies
|
||||
3. Implement strict CSP without unsafe-eval
|
||||
4. Remove or wrap console.log statements
|
||||
|
||||
### Phase 2: High Priority (Within 1 week)
|
||||
1. Disable source maps in production
|
||||
2. Add missing security headers (HSTS, Permissions-Policy)
|
||||
3. Fix token revocation vulnerability
|
||||
4. Update critical dependencies (bcrypt, helmet)
|
||||
|
||||
### Phase 3: Medium Priority (Within 1 month)
|
||||
1. Implement comprehensive logging strategy
|
||||
2. Add gallery slug validation
|
||||
3. Enhance rate limiting logic
|
||||
4. Implement session invalidation on password change
|
||||
|
||||
### Phase 4: Ongoing
|
||||
1. Weekly dependency scanning
|
||||
2. Implement security testing in CI/CD
|
||||
3. Regular penetration testing
|
||||
4. Security awareness training
|
||||
|
||||
---
|
||||
|
||||
## 🔒 RECOMMENDED CSP CONFIGURATION
|
||||
|
||||
```nginx
|
||||
add_header Content-Security-Policy "
|
||||
default-src 'self';
|
||||
script-src 'self' 'nonce-{RANDOM}' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/;
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: blob: https:;
|
||||
font-src 'self';
|
||||
connect-src 'self' https://analytics.domain.com;
|
||||
frame-src https://www.google.com/recaptcha/;
|
||||
object-src 'none';
|
||||
base-uri 'self';
|
||||
form-action 'self';
|
||||
frame-ancestors 'none';
|
||||
upgrade-insecure-requests;
|
||||
" always;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 SECURITY IMPROVEMENTS ROADMAP
|
||||
|
||||
### Immediate Implementation
|
||||
```bash
|
||||
# 1. Generate secure secrets
|
||||
openssl rand -base64 32 > jwt-secret.txt
|
||||
|
||||
# 2. Update dependencies
|
||||
cd backend && npm install bcrypt@^6.0.0 helmet@^8.1.0
|
||||
cd ../frontend && npm update
|
||||
|
||||
# 3. Add security scanning
|
||||
npm install -D npm-audit-resolver
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
```yaml
|
||||
# Add to CI pipeline
|
||||
- name: Security Scan
|
||||
run: |
|
||||
npm audit --audit-level=moderate
|
||||
npm run test:security
|
||||
```
|
||||
|
||||
### Monitoring & Alerting
|
||||
1. Implement fail2ban for repeated auth failures
|
||||
2. Set up log analysis for suspicious patterns
|
||||
3. Configure alerts for security events
|
||||
4. Regular vulnerability scanning
|
||||
|
||||
---
|
||||
|
||||
## 📋 COMPLIANCE CHECKLIST
|
||||
|
||||
- [ ] OWASP Top 10 addressed
|
||||
- [ ] GDPR compliance (data minimization, right to erasure)
|
||||
- [ ] Security headers implemented
|
||||
- [ ] Dependency scanning automated
|
||||
- [ ] Incident response plan documented
|
||||
- [ ] Security documentation maintained
|
||||
- [ ] Regular security reviews scheduled
|
||||
|
||||
---
|
||||
|
||||
## 🎯 CONCLUSION
|
||||
|
||||
The wedding photo sharing application has a solid security foundation with excellent input validation and authentication mechanisms. However, operational security practices need immediate attention. The critical issues around secret management and token storage must be addressed before production deployment.
|
||||
|
||||
Implementing the recommended fixes will raise the security score from 6.5/10 to approximately 8.5/10, providing a robust and secure platform for wedding photo sharing.
|
||||
|
||||
---
|
||||
|
||||
*Generated by Claude Security Scanner v1.0*
|
||||
*Next scan recommended: After Phase 1 remediation completion*
|
||||
-46
@@ -108,52 +108,6 @@ steps:
|
||||
- VERSION=${DRONE_TAG}
|
||||
- VITE_API_URL=${VITE_API_URL:-/api}
|
||||
|
||||
# -------- NEW: Create Gitea Release --------
|
||||
- name: gitea-release
|
||||
image: plugins/gitea-release
|
||||
when:
|
||||
event: tag
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: GITEA_TOKEN
|
||||
base_url: https://gitea.local.nothaft.cloud
|
||||
files: []
|
||||
title: PicPeak ${DRONE_TAG}
|
||||
note: |
|
||||
# PicPeak ${DRONE_TAG}
|
||||
|
||||
## 🐳 Docker Images
|
||||
|
||||
This release includes Docker images published to local registry:
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
docker pull registry.local.nothaft.cloud/picpeak-backend:${DRONE_TAG}
|
||||
docker pull registry.local.nothaft.cloud/picpeak-backend:latest
|
||||
|
||||
# Frontend
|
||||
docker pull registry.local.nothaft.cloud/picpeak-frontend:${DRONE_TAG}
|
||||
docker pull registry.local.nothaft.cloud/picpeak-frontend:latest
|
||||
```
|
||||
|
||||
## 📦 What's New
|
||||
|
||||
See the [README](https://gitea.nothaft.cloud/paul/picpeak#readme) for features and documentation.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
# Clone and deploy
|
||||
git clone https://gitea.nothaft.cloud/paul/picpeak.git
|
||||
cd picpeak
|
||||
|
||||
# Use the tagged version
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For detailed deployment instructions, see the Deployment Guide in the repository.
|
||||
|
||||
trigger:
|
||||
event:
|
||||
|
||||
+40
-37
@@ -1,47 +1,50 @@
|
||||
# PicPeak Development Environment Configuration
|
||||
# Copy this file to .env for local development
|
||||
# PicPeak Environment Configuration
|
||||
# Copy this file to .env and update with your values
|
||||
|
||||
# SECURITY WARNING: This configuration is for development only!
|
||||
# For production, use .env.production.example
|
||||
# Environment
|
||||
NODE_ENV=production
|
||||
|
||||
# JWT Secret (Change in production!)
|
||||
# Generate secure secret with: openssl rand -base64 32
|
||||
JWT_SECRET=dev-secret-DO-NOT-USE-IN-PRODUCTION
|
||||
# JWT Secret (generate with: openssl rand -base64 64)
|
||||
JWT_SECRET=your_very_long_random_jwt_secret_here
|
||||
|
||||
# Application URLs (Docker Compose development setup)
|
||||
ADMIN_URL=http://localhost:3005
|
||||
FRONTEND_URL=http://localhost:3005
|
||||
BACKEND_URL=http://localhost:3001
|
||||
# Database Configuration (PostgreSQL)
|
||||
DATABASE_CLIENT=pg
|
||||
DB_USER=picpeak
|
||||
DB_PASSWORD=your_secure_postgres_password_here
|
||||
DB_NAME=picpeak_prod
|
||||
|
||||
# Database Configuration (SQLite for development)
|
||||
DATABASE_CLIENT=sqlite3
|
||||
DATABASE_PATH=./data/photo_sharing.db
|
||||
# Redis Configuration
|
||||
REDIS_PASSWORD=your_secure_redis_password_here
|
||||
|
||||
# Email Configuration (Mailhog for development)
|
||||
# Access Mailhog UI at: http://localhost:8025
|
||||
SMTP_HOST=mailhog
|
||||
SMTP_PORT=1025
|
||||
# Admin Account (initial setup)
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=admin@yourdomain.com
|
||||
|
||||
# Email Configuration
|
||||
# For Gmail: use app-specific password
|
||||
# For SendGrid: SMTP_USER=apikey, SMTP_PASS=your-api-key
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
EMAIL_FROM=noreply@localhost
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-specific-password
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
|
||||
# Backend Port Configuration
|
||||
PORT=3001
|
||||
# Application URLs
|
||||
FRONTEND_URL=https://yourdomain.com
|
||||
ADMIN_URL=https://yourdomain.com:3001
|
||||
VITE_API_URL=https://yourdomain.com:3001/api
|
||||
|
||||
# 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
|
||||
# Port Configuration (optional)
|
||||
# BACKEND_PORT=3001
|
||||
# FRONTEND_PORT=3000
|
||||
# DB_PORT=5432
|
||||
# REDIS_PORT=6379
|
||||
|
||||
# Development Features
|
||||
NODE_ENV=development
|
||||
LOG_LEVEL=debug
|
||||
# Timezone
|
||||
TZ=UTC
|
||||
|
||||
# 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)
|
||||
# Analytics (Optional - Umami)
|
||||
VITE_UMAMI_URL=
|
||||
VITE_UMAMI_WEBSITE_ID=
|
||||
VITE_UMAMI_SHARE_URL=
|
||||
@@ -1,100 +0,0 @@
|
||||
# PicPeak Production Configuration
|
||||
# Copy this file to .env and update with your production values
|
||||
|
||||
# ============================================
|
||||
# CRITICAL SECURITY - MUST CHANGE ALL VALUES!
|
||||
# ============================================
|
||||
|
||||
# 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
|
||||
BACKEND_URL=https://your-domain.com
|
||||
ADMIN_URL=https://your-domain.com
|
||||
|
||||
# ============================================
|
||||
# DATABASE CONFIGURATION - REQUIRED
|
||||
# ============================================
|
||||
|
||||
# PostgreSQL Configuration (Recommended for production)
|
||||
DATABASE_CLIENT=pg
|
||||
DB_HOST=postgres # or your database host
|
||||
DB_PORT=5432
|
||||
DB_NAME=picpeak
|
||||
DB_USER=picpeak
|
||||
DB_PASSWORD=CHANGE-THIS-SECURE-DATABASE-PASSWORD
|
||||
|
||||
# ============================================
|
||||
# EMAIL CONFIGURATION - REQUIRED
|
||||
# ============================================
|
||||
|
||||
# Example: Gmail with App Password
|
||||
# SMTP_HOST=smtp.gmail.com
|
||||
# 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
|
||||
# ============================================
|
||||
|
||||
NODE_ENV=production
|
||||
PORT=3001
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Security Settings (Defaults are secure)
|
||||
BCRYPT_ROUNDS=12
|
||||
SESSION_TIMEOUT_MINUTES=60
|
||||
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
|
||||
@@ -1,12 +0,0 @@
|
||||
# Files to exclude from GitHub mirror
|
||||
.env* export-ignore
|
||||
docker-compose.prod.yml export-ignore
|
||||
.claudedocs/ export-ignore
|
||||
backend/data/ export-ignore
|
||||
backend/storage/ export-ignore
|
||||
backend/.env* export-ignore
|
||||
frontend/.env* export-ignore
|
||||
secrets/ export-ignore
|
||||
*.key export-ignore
|
||||
*.pem export-ignore
|
||||
.gitea/ export-ignore
|
||||
@@ -10,145 +10,32 @@ jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository with full history
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Full history needed for finding the commit
|
||||
fetch-depth: 0 # Full history for proper mirroring
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.name "the-luap"
|
||||
git config --global user.email "paul-nothaft@hotmail.de"
|
||||
|
||||
- name: Debug - Show current branch and status
|
||||
run: |
|
||||
echo "Current branch:"
|
||||
git branch -a
|
||||
echo "Git status:"
|
||||
git status
|
||||
echo "Remote info:"
|
||||
git remote -v
|
||||
echo "Checking target commit exists:"
|
||||
git show --oneline 7aca927937 || echo "Target commit not found!"
|
||||
|
||||
- name: Create completely new history from specific commit
|
||||
run: |
|
||||
TARGET_COMMIT="7aca927937"
|
||||
|
||||
# Verify the target commit exists
|
||||
if ! git cat-file -e $TARGET_COMMIT^{commit}; then
|
||||
echo "ERROR: Target commit $TARGET_COMMIT does not exist!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Target commit found: $(git log --oneline -1 $TARGET_COMMIT)"
|
||||
|
||||
# Clean up any existing github-mirror branch
|
||||
git branch -D github-mirror || true
|
||||
|
||||
# Create a completely new orphan branch (no history)
|
||||
git checkout --orphan github-mirror
|
||||
|
||||
# Clear the staging area completely
|
||||
git rm -rf . || true
|
||||
|
||||
# Get the file tree from the target commit and create initial commit
|
||||
echo "Creating new history starting from $TARGET_COMMIT..."
|
||||
git read-tree $TARGET_COMMIT
|
||||
git commit -m "Initial commit - imported from $(git log --oneline -1 $TARGET_COMMIT)"
|
||||
|
||||
echo "✅ Created new initial commit: $(git log --oneline -1)"
|
||||
|
||||
# Now get all commits after the target commit and apply their changes
|
||||
COMMITS_AFTER_TARGET=$(git rev-list --reverse --no-merges $TARGET_COMMIT..main)
|
||||
|
||||
if [ -n "$COMMITS_AFTER_TARGET" ]; then
|
||||
echo "📋 Applying changes from commits after $TARGET_COMMIT (excluding Claude commits):"
|
||||
|
||||
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)"
|
||||
|
||||
# Get the commit message and author info
|
||||
COMMIT_MSG=$(git log --format="%B" -n 1 $commit)
|
||||
COMMIT_AUTHOR=$(git log --format="%an <%ae>" -n 1 $commit)
|
||||
COMMIT_DATE=$(git log --format="%ad" -n 1 $commit)
|
||||
|
||||
# Apply the changes from this commit
|
||||
if git diff-tree --no-commit-id --name-only -r $commit | xargs -I {} git show $commit:{} > /dev/null 2>&1; then
|
||||
# Apply file changes
|
||||
git checkout $commit -- . || true
|
||||
|
||||
# Stage all changes
|
||||
git add -A
|
||||
|
||||
# Only commit if there are changes
|
||||
if ! git diff --cached --quiet; then
|
||||
# Create new commit with original metadata but new SHA
|
||||
GIT_AUTHOR_NAME=$(echo "$COMMIT_AUTHOR" | cut -d'<' -f1 | xargs)
|
||||
GIT_AUTHOR_EMAIL=$(echo "$COMMIT_AUTHOR" | cut -d'<' -f2 | cut -d'>' -f1)
|
||||
GIT_AUTHOR_DATE="$COMMIT_DATE"
|
||||
|
||||
export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
|
||||
git commit -m "$COMMIT_MSG"
|
||||
echo "✅ Applied changes as new commit: $(git log --oneline -1)"
|
||||
else
|
||||
echo "⚠️ No changes to commit for $commit"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Skipping problematic commit $commit"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ Finished creating new history"
|
||||
else
|
||||
echo "✅ No commits after target commit - history starts fresh"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== New History Summary ==="
|
||||
echo "Total commits in new history: $(git rev-list --count github-mirror)"
|
||||
echo "History starts with: $(git log --oneline --reverse | head -1)"
|
||||
echo "Latest commit: $(git log --oneline -1)"
|
||||
|
||||
- name: Remove sensitive files and directories
|
||||
run: |
|
||||
# Switch to the github-mirror branch
|
||||
git checkout github-mirror
|
||||
|
||||
echo "Current files before cleanup:"
|
||||
ls -la | head -10 || true
|
||||
echo "..."
|
||||
|
||||
# Remove sensitive files/directories if they exist
|
||||
echo "Removing sensitive files..."
|
||||
rm -rf .env || true
|
||||
rm -rf backend/.env* || true
|
||||
rm -rf frontend/.env* || true
|
||||
rm -rf docker-compose.prod.yml || true
|
||||
rm -rf .claudedocs/ || true
|
||||
rm -rf backend/data/ || true
|
||||
rm -rf backend/storage/ || true
|
||||
rm -rf .gitea/ || true
|
||||
rm -rf scripts/install-gitea-runner.sh || true
|
||||
rm -rf scripts/ || true
|
||||
rm -rf .drone* || true
|
||||
rm -rf .github-mirror-exclude || true
|
||||
rm -rf .gitattributes-github || true
|
||||
rm -rf photo-sharing-prd.md || true
|
||||
rm -rf CLAUDE.md || true
|
||||
rm -rf PRODUCTION_DEPLOYMENT_GUIDE.md || true
|
||||
rm -rf logs/ || true
|
||||
rm -rf frontend/.claudedocs/ || true
|
||||
rm -rf test-maintenance.sh || true
|
||||
rm -rf storage/ || true
|
||||
|
||||
|
||||
|
||||
echo "Sensitive files removal completed"
|
||||
|
||||
@@ -164,18 +51,6 @@ jobs:
|
||||
echo "Final file structure (top level):"
|
||||
ls -la | head -10 || true
|
||||
|
||||
- name: Verify completely new history
|
||||
run: |
|
||||
git checkout github-mirror
|
||||
echo "=== Final History Verification ==="
|
||||
echo "Total commits in new github-mirror branch: $(git rev-list --count github-mirror)"
|
||||
echo ""
|
||||
echo "Complete commit history (should start from target commit content):"
|
||||
git log --oneline --reverse
|
||||
echo ""
|
||||
echo "⚠️ Note: This is a completely NEW history with new commit SHAs"
|
||||
echo "🔍 Original target commit content preserved but with new commit ID"
|
||||
|
||||
- name: Check GitHub token
|
||||
env:
|
||||
GITHUBTOKEN: ${{ secrets.GITHUBTOKEN }}
|
||||
@@ -187,13 +62,10 @@ jobs:
|
||||
echo "GitHub token is available (length: ${#GITHUBTOKEN})"
|
||||
fi
|
||||
|
||||
- name: Force push completely new history to GitHub
|
||||
- name: Push to GitHub
|
||||
env:
|
||||
GITHUBTOKEN: ${{ secrets.GITHUBTOKEN }}
|
||||
run: |
|
||||
# Switch to github-mirror branch
|
||||
git checkout github-mirror
|
||||
|
||||
# Remove existing github remote if it exists
|
||||
git remote remove github || true
|
||||
|
||||
@@ -204,17 +76,13 @@ jobs:
|
||||
echo "GitHub remote added:"
|
||||
git remote -v
|
||||
|
||||
# Force push the completely new history to GitHub main
|
||||
echo "🔥 FORCE PUSHING completely new history to GitHub..."
|
||||
echo "⚠️ This will COMPLETELY REPLACE all history on GitHub!"
|
||||
git push github github-mirror:main --force
|
||||
echo "✅ Force push completed - GitHub now has completely new history!"
|
||||
# Push to GitHub main branch
|
||||
echo "Pushing to GitHub..."
|
||||
git push github main --force
|
||||
echo "✅ Push to GitHub completed!"
|
||||
|
||||
- name: Workflow completed
|
||||
run: |
|
||||
echo "✅ Mirror to GitHub workflow completed successfully!"
|
||||
echo "🔥 COMPLETE HISTORY REPLACEMENT: GitHub now has entirely new history"
|
||||
echo "📊 History starts from commit content: 7aca927937"
|
||||
echo "🔍 Check https://github.com/the-luap/picpeak to verify the new history"
|
||||
echo "📈 Total commits pushed: $(git rev-list --count github-mirror)"
|
||||
echo "🆕 All commit SHAs are NEW - no connection to previous history"
|
||||
echo "📊 Repository mirrored to: https://github.com/the-luap/picpeak"
|
||||
echo "🔒 Sensitive files have been removed from the mirror"
|
||||
@@ -1,24 +0,0 @@
|
||||
# Exclude patterns for GitHub mirror
|
||||
.env
|
||||
.env.*
|
||||
.env*
|
||||
docker-compose.prod.yml
|
||||
docker-compose.traefik.yml
|
||||
.claudedocs/
|
||||
backend/data/
|
||||
backend/storage/
|
||||
backend/.env*
|
||||
frontend/.env*
|
||||
secrets/
|
||||
*.key
|
||||
*.pem
|
||||
.gitea/
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
*.log
|
||||
.DS_Store
|
||||
deploy/
|
||||
certbot/
|
||||
nginx/
|
||||
photo-sharing-prd.md
|
||||
+13
-1
@@ -11,6 +11,9 @@ yarn-error.log*
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Docker override file
|
||||
docker-compose.override.yml
|
||||
|
||||
# Security - Never commit credentials
|
||||
ADMIN_CREDENTIALS.txt
|
||||
ADMIN_PASSWORD_RESET.txt
|
||||
@@ -59,4 +62,13 @@ test-archiver/
|
||||
!data/.gitkeep
|
||||
!logs/.gitkeep
|
||||
|
||||
PRODUCTION_DEPLOYMENT_GUIDE.md
|
||||
# development files
|
||||
backend/.swarm/
|
||||
.claudedocs/
|
||||
backend/data/
|
||||
backend/docs/
|
||||
backend/logs/
|
||||
logs/
|
||||
storage/
|
||||
data/
|
||||
certbot/
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
# Backup Version Tracking Implementation
|
||||
|
||||
## Overview
|
||||
Version tracking has been added to the backup system to ensure safe restoration by tracking application versions, Node.js versions, and database schema versions at the time of backup.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Database Schema Changes (Migration 034)
|
||||
|
||||
Added version tracking columns to backup tables:
|
||||
|
||||
#### `database_backup_runs` table:
|
||||
- `app_version` - Application version from package.json
|
||||
- `node_version` - Node.js runtime version
|
||||
- `db_schema_version` - Latest migration name
|
||||
- `environment_info` - JSON with additional environment details
|
||||
|
||||
#### `backup_runs` table:
|
||||
- `app_version` - Application version
|
||||
- `node_version` - Node.js version
|
||||
- `db_schema_version` - Database schema version
|
||||
- `manifest_info` - Summary of manifest information
|
||||
|
||||
#### New `restore_history` table:
|
||||
Tracks all restore attempts with comprehensive version information:
|
||||
- Backup versions vs current versions
|
||||
- Compatibility check results
|
||||
- Warnings and errors
|
||||
- Restore outcome
|
||||
|
||||
### 2. Version Information Captured
|
||||
|
||||
During each backup, the system now records:
|
||||
- **Application Version**: From `package.json` (e.g., "1.0.77")
|
||||
- **Node.js Version**: Runtime version (e.g., "v18.17.0")
|
||||
- **Database Schema**: Latest migration file (e.g., "034_add_version_to_backups.js")
|
||||
- **Environment Info**: Platform, architecture, environment mode
|
||||
|
||||
### 3. Backup Services Updated
|
||||
|
||||
#### Database Backup Service (`databaseBackup.js`):
|
||||
- Records version info when creating backups
|
||||
- Includes versions in statistics JSON
|
||||
- New method: `checkVersionCompatibility()` for restore safety
|
||||
- New method: `getCurrentSchemaVersion()` to track migrations
|
||||
|
||||
#### File Backup Service (`backupService.js`):
|
||||
- Records version info in backup_runs table
|
||||
- Integrates with manifest system
|
||||
- Stores manifest summary with version details
|
||||
|
||||
### 4. Existing Manifest System
|
||||
|
||||
The `backupManifest.js` already provides comprehensive version tracking:
|
||||
- Application version and Node.js version
|
||||
- System information (OS, platform, architecture)
|
||||
- Database schema version
|
||||
- Detailed file and database metadata
|
||||
|
||||
### 5. Version Compatibility Checking
|
||||
|
||||
When restoring, the system can now:
|
||||
- Compare backup version vs current version
|
||||
- Detect major/minor version differences
|
||||
- Identify schema mismatches
|
||||
- Provide warnings and recommendations
|
||||
|
||||
### 6. Configuration Settings
|
||||
|
||||
New backup settings for version control:
|
||||
- `backup_require_version_match` - Enforce exact version matching
|
||||
- `backup_allow_minor_version_mismatch` - Allow same major version
|
||||
- `backup_warn_on_version_mismatch` - Show warnings on mismatch
|
||||
- `backup_check_schema_compatibility` - Validate schema versions
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating Backups
|
||||
Backups automatically capture version information - no changes needed to existing backup workflows.
|
||||
|
||||
### Checking Version Before Restore
|
||||
|
||||
1. **For Database Backups**:
|
||||
```javascript
|
||||
const compatibility = await databaseBackupService.checkVersionCompatibility({
|
||||
app_version: '1.0.75',
|
||||
node_version: 'v16.14.0',
|
||||
db_schema_version: '032_add_feedback.js'
|
||||
});
|
||||
|
||||
if (!compatibility.compatible) {
|
||||
console.error('Version mismatch:', compatibility.errors);
|
||||
}
|
||||
```
|
||||
|
||||
2. **For File Backups**:
|
||||
Check the manifest file which contains all version information:
|
||||
```bash
|
||||
cat /backup/path/manifest-backup-20250122-123456.json | jq '.application'
|
||||
```
|
||||
|
||||
### Restore History
|
||||
All restore attempts are logged in the `restore_history` table with:
|
||||
- Version compatibility results
|
||||
- Warnings encountered
|
||||
- Success/failure status
|
||||
- Who performed the restore
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always Check Compatibility**: Before restoring, verify version compatibility
|
||||
2. **Document Version Changes**: Keep changelog updated with breaking changes
|
||||
3. **Test Restores**: Regularly test restore procedures in staging
|
||||
4. **Monitor Warnings**: Even if compatible, review warnings before proceeding
|
||||
5. **Keep Backups Organized**: Label backups with version info in filename
|
||||
|
||||
## Migration Instructions
|
||||
|
||||
1. Run the new migration:
|
||||
```bash
|
||||
cd backend
|
||||
npm run migrate
|
||||
```
|
||||
|
||||
2. Existing backups will show "unknown" for version fields
|
||||
3. New backups will automatically include version information
|
||||
4. The system remains backward compatible with old backups
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Version Mismatch Errors
|
||||
- Check current app version: `cat backend/package.json | grep version`
|
||||
- Check Node version: `node --version`
|
||||
- Check latest migration: `SELECT name FROM knex_migrations ORDER BY id DESC LIMIT 1`
|
||||
|
||||
### Restore Failures
|
||||
- Review `restore_history` table for detailed error messages
|
||||
- Check version compatibility warnings
|
||||
- Consider using same version environment for critical restores
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Automated Version Matching**: Docker containers with specific versions
|
||||
2. **Migration Rollback**: Support for downgrading schema safely
|
||||
3. **Version Matrix**: Compatibility matrix for different version combinations
|
||||
4. **Restore Wizard**: UI for guided restore with compatibility checks
|
||||
|
||||
---
|
||||
|
||||
**Implementation Date**: January 2025
|
||||
**Current Version**: 1.0.77
|
||||
**Status**: Production Ready
|
||||
@@ -33,11 +33,14 @@ npm test -- path/to/test.test.js
|
||||
npm test -- --testNamePattern="test name"
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml up -d # Production deployment
|
||||
pm2 start ecosystem.config.js # Alternative: PM2 deployment
|
||||
```
|
||||
### Production Deployment
|
||||
See [DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md) for comprehensive deployment instructions including:
|
||||
- Docker Compose deployment
|
||||
- PM2 deployment
|
||||
- Manual installation
|
||||
- Non-nginx deployment options
|
||||
- SSL/HTTPS setup
|
||||
- Troubleshooting guide
|
||||
|
||||
**⚠️ CRITICAL PRODUCTION NOTICE:**
|
||||
- Production runs on a SEPARATE SERVER - never assume local changes affect production
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ We are committed to providing a welcoming and inspiring community for all photog
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of unacceptable behavior may be reported to the project team at conduct@example.com. All complaints will be reviewed and investigated promptly and fairly.
|
||||
Instances of unacceptable behavior may be reported by [opening an issue](https://github.com/the-luap/picpeak/issues/new?labels=conduct) on GitHub. All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
## Attribution
|
||||
|
||||
|
||||
+3
-3
@@ -153,8 +153,8 @@ picpeak/
|
||||
|
||||
## 📮 Contact
|
||||
|
||||
- Create an issue for bugs or features
|
||||
- Join discussions for questions
|
||||
- Email: picpeak@example.com for security issues
|
||||
- Create an [issue](https://github.com/the-luap/picpeak/issues) for bugs or features
|
||||
- Join [discussions](https://github.com/the-luap/picpeak/discussions) for questions
|
||||
- Security issues: Open a [security issue](https://github.com/the-luap/picpeak/issues/new?labels=security) on GitHub
|
||||
|
||||
Thank you for contributing! 🎉
|
||||
-220
@@ -1,220 +0,0 @@
|
||||
# 🚀 PicPeak Deployment Guide
|
||||
|
||||
This guide will help you deploy PicPeak in production. The entire process takes about 10-15 minutes.
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
- A server with Docker and Docker Compose installed
|
||||
- A domain name (for SSL certificates)
|
||||
- SMTP credentials for sending emails
|
||||
- Basic command line knowledge
|
||||
|
||||
## 🏃 Quick Deploy (Recommended)
|
||||
|
||||
### 1. Clone and Configure
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/the-luap/picpeak.git
|
||||
cd picpeak
|
||||
|
||||
# Copy environment template
|
||||
cp .env.production.example .env
|
||||
|
||||
# Generate a secure JWT secret
|
||||
echo "JWT_SECRET=$(openssl rand -base64 32)" >> .env
|
||||
|
||||
# Edit configuration
|
||||
nano .env
|
||||
```
|
||||
|
||||
### 2. Required Environment Variables
|
||||
|
||||
Edit your `.env` file with these essential settings:
|
||||
|
||||
```env
|
||||
# Application URLs
|
||||
FRONTEND_URL=https://your-domain.com
|
||||
BACKEND_URL=https://your-domain.com
|
||||
|
||||
# Email Configuration (Required for notifications)
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
SMTP_FROM=your-email@gmail.com
|
||||
|
||||
# Admin Configuration
|
||||
ADMIN_EMAIL=admin@your-domain.com
|
||||
ADMIN_PASSWORD=your-secure-password
|
||||
|
||||
# Database (PostgreSQL for production)
|
||||
DATABASE_CLIENT=pg
|
||||
DB_HOST=postgres
|
||||
DB_NAME=picpeak
|
||||
DB_USER=picpeak
|
||||
DB_PASSWORD=secure-db-password
|
||||
```
|
||||
|
||||
### 3. Deploy with Docker Compose
|
||||
|
||||
```bash
|
||||
# Start all services
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# Check logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Access your site at https://your-domain.com
|
||||
```
|
||||
|
||||
## 🔧 Configuration Options
|
||||
|
||||
### Storage Settings
|
||||
|
||||
```env
|
||||
# Storage paths (default: ./storage)
|
||||
STORAGE_PATH=./storage
|
||||
ARCHIVE_PATH=./storage/archives
|
||||
|
||||
# Gallery expiration (days)
|
||||
DEFAULT_EXPIRATION_DAYS=30
|
||||
WARNING_DAYS_BEFORE_EXPIRY=7
|
||||
```
|
||||
|
||||
### Security Settings
|
||||
|
||||
```env
|
||||
# Session timeout (minutes)
|
||||
SESSION_TIMEOUT=60
|
||||
|
||||
# Rate limiting
|
||||
RATE_LIMIT_WINDOW_MS=900000 # 15 minutes
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
```
|
||||
|
||||
### Analytics (Optional)
|
||||
|
||||
```env
|
||||
# Umami Analytics
|
||||
VITE_UMAMI_URL=https://analytics.your-domain.com
|
||||
VITE_UMAMI_WEBSITE_ID=your-website-id
|
||||
```
|
||||
|
||||
## 🔒 SSL/TLS Setup
|
||||
|
||||
The production Docker Compose includes automatic SSL via Let's Encrypt:
|
||||
|
||||
1. **Ensure your domain points to your server**
|
||||
2. **Update nginx configuration**:
|
||||
```bash
|
||||
nano nginx/nginx.conf
|
||||
# Replace your-domain.com with your actual domain
|
||||
```
|
||||
3. **Start services** - Certbot will automatically obtain certificates
|
||||
|
||||
## 📁 Directory Structure
|
||||
|
||||
After deployment, your directory structure will be:
|
||||
|
||||
```
|
||||
picpeak/
|
||||
├── backend/ # API server
|
||||
├── frontend/ # React app
|
||||
├── storage/ # Photo storage
|
||||
│ ├── events/ # Active galleries
|
||||
│ │ ├── active/ # Current photos
|
||||
│ │ └── archived/ # Expired galleries
|
||||
│ ├── thumbnails/ # Generated thumbnails
|
||||
│ └── uploads/ # User uploads
|
||||
├── data/ # Database files
|
||||
└── logs/ # Application logs
|
||||
```
|
||||
|
||||
## 🔄 Maintenance
|
||||
|
||||
### Backup
|
||||
|
||||
```bash
|
||||
# Backup database and photos
|
||||
./scripts/backup.sh
|
||||
|
||||
# Backups are stored in ./backups/
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
```bash
|
||||
# Pull latest changes
|
||||
git pull
|
||||
|
||||
# Rebuild and restart
|
||||
docker-compose -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
# View all logs
|
||||
docker-compose logs
|
||||
|
||||
# View specific service
|
||||
docker-compose logs backend
|
||||
docker-compose logs frontend
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Photos not appearing:**
|
||||
- Check storage permissions: `chmod -R 755 storage/`
|
||||
- Verify file watcher is running: `docker-compose logs backend | grep watcher`
|
||||
|
||||
**Email not sending:**
|
||||
- Test SMTP settings: Admin Panel → Settings → Email → Send Test
|
||||
- Check email queue: Admin Panel → System → Email Queue
|
||||
|
||||
**Can't access admin panel:**
|
||||
- Default login: Use email/password from `.env`
|
||||
- Reset password: `docker exec picpeak-backend npm run reset-admin`
|
||||
|
||||
### Health Check
|
||||
|
||||
```bash
|
||||
# Check service status
|
||||
docker-compose ps
|
||||
|
||||
# Test backend API
|
||||
curl https://your-domain.com/api/health
|
||||
|
||||
# Check disk space
|
||||
df -h storage/
|
||||
```
|
||||
|
||||
## 🐳 Alternative Deployment Methods
|
||||
|
||||
### Using Docker Swarm
|
||||
|
||||
For high availability deployments, see [Docker Swarm Setup](deploy/README.md).
|
||||
|
||||
### Manual Installation
|
||||
|
||||
If you prefer not to use Docker:
|
||||
|
||||
1. Install Node.js 18+
|
||||
2. Install PostgreSQL
|
||||
3. Clone repository
|
||||
4. Install dependencies: `npm install` in both `/backend` and `/frontend`
|
||||
5. Build frontend: `cd frontend && npm run build`
|
||||
6. Start services with PM2
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- 📘 [Documentation](https://github.com/the-luap/picpeak)
|
||||
- 🐛 [Report Issues](https://github.com/the-luap/picpeak/issues)
|
||||
- 💬 [Discussions](https://github.com/the-luap/picpeak/discussions)
|
||||
|
||||
---
|
||||
|
||||
**Need help?** Open an issue on GitHub and we'll assist you!
|
||||
@@ -0,0 +1,461 @@
|
||||
# 🚀 PicPeak Deployment Guide
|
||||
|
||||
This guide covers deploying PicPeak using Docker Compose with direct port exposure. For internet-facing deployments, you'll need to add a reverse proxy (nginx, Traefik, Caddy, etc.) for SSL/HTTPS.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Configuration](#configuration)
|
||||
- [Deployment](#deployment)
|
||||
- [Reverse Proxy Setup](#reverse-proxy-setup)
|
||||
- [Maintenance](#maintenance)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- Domain name (for production)
|
||||
- SMTP server credentials for emails
|
||||
- At least 2GB RAM and 20GB storage
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/the-luap/picpeak.git
|
||||
cd picpeak
|
||||
```
|
||||
|
||||
2. **Set up environment**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env # Edit with your values
|
||||
```
|
||||
|
||||
3. **Create required directories**
|
||||
```bash
|
||||
mkdir -p events/active events/archived data logs backup storage
|
||||
chmod -R 755 events data logs backup storage
|
||||
```
|
||||
|
||||
4. **Deploy**
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. **Check logs**
|
||||
```bash
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Essential Environment Variables
|
||||
|
||||
Generate secure values:
|
||||
```bash
|
||||
# JWT Secret
|
||||
openssl rand -base64 64
|
||||
|
||||
# Database Password
|
||||
openssl rand -base64 32
|
||||
|
||||
# Redis Password
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
Update `.env` with:
|
||||
- `JWT_SECRET` - Authentication secret
|
||||
- `DB_PASSWORD` - PostgreSQL password
|
||||
- `REDIS_PASSWORD` - Redis password
|
||||
- `SMTP_*` - Email configuration
|
||||
- `FRONTEND_URL` - Your domain URL
|
||||
- `ADMIN_URL` - Backend admin URL
|
||||
- `VITE_API_URL` - API URL for frontend
|
||||
|
||||
### Email Configuration Examples
|
||||
|
||||
#### Gmail
|
||||
```env
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-specific-password
|
||||
```
|
||||
|
||||
#### SendGrid
|
||||
```env
|
||||
SMTP_HOST=smtp.sendgrid.net
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=apikey
|
||||
SMTP_PASS=your-sendgrid-api-key
|
||||
```
|
||||
|
||||
## 📦 Deployment
|
||||
|
||||
### Build and Start Services
|
||||
|
||||
```bash
|
||||
# Build images
|
||||
docker compose build
|
||||
|
||||
# Start all services
|
||||
docker compose up -d
|
||||
|
||||
# View running containers
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
### Access Points
|
||||
|
||||
By default, services are exposed on:
|
||||
- Frontend: http://localhost:3000
|
||||
- Backend/API: http://localhost:3001
|
||||
- PostgreSQL: localhost:5432 (if needed)
|
||||
- Redis: localhost:6379 (if needed)
|
||||
|
||||
### Initial Admin Setup
|
||||
|
||||
When deploying for the first time, an admin account is automatically created with a secure random password. You need to retrieve this password to access the admin panel.
|
||||
|
||||
#### Finding the Admin Password
|
||||
|
||||
**Option 1: Check the backend logs** (recommended)
|
||||
```bash
|
||||
# View the initial setup logs
|
||||
docker compose logs backend | grep -A 10 "Admin user created"
|
||||
```
|
||||
|
||||
You should see output like:
|
||||
```
|
||||
========================================
|
||||
✅ Admin user created successfully!
|
||||
========================================
|
||||
Email: admin@example.com
|
||||
Password: BraveTiger6231!
|
||||
|
||||
⚠️ IMPORTANT:
|
||||
1. Save these credentials securely
|
||||
2. Please change the password after first login
|
||||
========================================
|
||||
```
|
||||
|
||||
**Note**: You login with the **email address**, not a username!
|
||||
|
||||
**Option 2: Check the saved credentials file**
|
||||
```bash
|
||||
# The password is saved in the backend container
|
||||
docker exec picpeak-backend cat data/ADMIN_CREDENTIALS.txt
|
||||
```
|
||||
|
||||
**Option 3: Use the helper script**
|
||||
```bash
|
||||
# Show current admin username and email (password is hidden)
|
||||
docker exec picpeak-backend node scripts/show-admin-credentials.js
|
||||
|
||||
# Reset the admin password to a new random password
|
||||
docker exec picpeak-backend node scripts/show-admin-credentials.js --reset
|
||||
```
|
||||
|
||||
#### Important Notes
|
||||
|
||||
- **Login requires the email address**, not username
|
||||
- The admin password is only shown once during initial setup
|
||||
- If you lose the password, use the `--reset` option to generate a new one
|
||||
- You must change the password on first login (enforced by the system)
|
||||
- Password requirements: minimum 12 characters, mixed case, numbers, and special characters
|
||||
|
||||
#### Configuring Admin Email
|
||||
|
||||
By default, the admin email is `admin@example.com`. To use a different email address, set it in your `.env` file before first deployment:
|
||||
|
||||
```env
|
||||
# .env
|
||||
ADMIN_EMAIL=your-email@yourdomain.com
|
||||
```
|
||||
|
||||
**Note**: This only works on first deployment. To change the admin email after deployment, you'll need to update it in the database or create a new admin user through the admin panel.
|
||||
|
||||
## 🔒 Reverse Proxy Setup
|
||||
|
||||
For production deployments, you should use a reverse proxy for SSL/HTTPS. The application exposes ports directly, allowing you to use any reverse proxy solution.
|
||||
|
||||
### Option 1: Nginx
|
||||
|
||||
Install nginx and create `/etc/nginx/sites-available/picpeak`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-domain.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
|
||||
|
||||
# Frontend
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
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;
|
||||
}
|
||||
|
||||
# Backend API
|
||||
location /api {
|
||||
proxy_pass http://localhost:3001;
|
||||
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;
|
||||
}
|
||||
|
||||
# Protected photos and uploads
|
||||
location ~ ^/(photos|thumbnails|uploads) {
|
||||
proxy_pass http://localhost:3001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# Admin routes
|
||||
location /admin {
|
||||
proxy_pass http://localhost:3001;
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enable the site:
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/picpeak /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### Option 2: Traefik
|
||||
|
||||
Add labels to `docker-compose.override.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.picpeak.rule=Host(`your-domain.com`)"
|
||||
- "traefik.http.routers.picpeak.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak.loadbalancer.server.port=80"
|
||||
|
||||
backend:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.picpeak-api.rule=Host(`your-domain.com`) && PathPrefix(`/api`)"
|
||||
- "traefik.http.routers.picpeak-api.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak-api.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak-api.loadbalancer.server.port=3001"
|
||||
```
|
||||
|
||||
### Option 3: Caddy
|
||||
|
||||
Create a `Caddyfile`:
|
||||
|
||||
```caddyfile
|
||||
your-domain.com {
|
||||
# Frontend
|
||||
handle /* {
|
||||
reverse_proxy localhost:3000
|
||||
}
|
||||
|
||||
# Backend API and admin
|
||||
handle /api/* {
|
||||
reverse_proxy localhost:3001
|
||||
}
|
||||
|
||||
handle /admin/* {
|
||||
reverse_proxy localhost:3001
|
||||
}
|
||||
|
||||
# Protected resources
|
||||
handle /photos/* {
|
||||
reverse_proxy localhost:3001
|
||||
}
|
||||
|
||||
handle /thumbnails/* {
|
||||
reverse_proxy localhost:3001
|
||||
}
|
||||
|
||||
handle /uploads/* {
|
||||
reverse_proxy localhost:3001
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SSL Certificates
|
||||
|
||||
For any reverse proxy, you can use Let's Encrypt:
|
||||
|
||||
```bash
|
||||
# With Certbot
|
||||
sudo certbot certonly --webroot -w /var/www/certbot -d your-domain.com
|
||||
|
||||
# Or use your reverse proxy's built-in ACME support
|
||||
```
|
||||
|
||||
## 🔧 Maintenance
|
||||
|
||||
### Viewing Logs
|
||||
|
||||
```bash
|
||||
# All services
|
||||
docker compose logs -f
|
||||
|
||||
# Specific service
|
||||
docker compose logs -f backend
|
||||
docker compose logs -f frontend
|
||||
```
|
||||
|
||||
### Backup
|
||||
|
||||
#### Manual Backup
|
||||
```bash
|
||||
# Database backup
|
||||
docker exec picpeak-postgres pg_dump -U picpeak picpeak_prod > backup/db_$(date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
# Files backup
|
||||
tar -czf backup/photos_$(date +%Y%m%d_%H%M%S).tar.gz events/
|
||||
```
|
||||
|
||||
#### Automated Backup
|
||||
The application includes a built-in backup service. Configure it in the admin panel:
|
||||
1. Login to admin panel
|
||||
2. Go to Settings → Backup
|
||||
3. Configure destination and schedule
|
||||
4. Enable backup service
|
||||
|
||||
### Updates
|
||||
|
||||
```bash
|
||||
# Pull latest changes
|
||||
git pull
|
||||
|
||||
# Rebuild and restart
|
||||
docker compose down
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
|
||||
Migrations run automatically on startup, but you can run them manually:
|
||||
|
||||
```bash
|
||||
docker exec picpeak-backend npm run migrate
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Port Already in Use
|
||||
```bash
|
||||
# Check what's using the port
|
||||
sudo lsof -i :3000
|
||||
sudo lsof -i :3001
|
||||
|
||||
# Change ports in .env
|
||||
FRONTEND_PORT=3002
|
||||
BACKEND_PORT=3003
|
||||
```
|
||||
|
||||
#### Permission Errors
|
||||
```bash
|
||||
# Fix ownership
|
||||
sudo chown -R 1000:1000 events data logs backup storage
|
||||
chmod -R 755 events data logs backup storage
|
||||
```
|
||||
|
||||
#### Database Connection Issues
|
||||
```bash
|
||||
# Check if database is running
|
||||
docker compose ps
|
||||
docker compose logs postgres
|
||||
|
||||
# Test connection
|
||||
docker exec picpeak-postgres pg_isready
|
||||
```
|
||||
|
||||
#### Email Not Sending
|
||||
- Verify SMTP settings in .env
|
||||
- Check email queue: `docker exec picpeak-backend psql -U picpeak -d picpeak_prod -c "SELECT * FROM email_queue ORDER BY created_at DESC LIMIT 10;"`
|
||||
- For Gmail, use app-specific password
|
||||
- Check logs: `docker compose logs backend | grep email`
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Backend health
|
||||
curl http://localhost:3001/api/health
|
||||
|
||||
# Frontend health
|
||||
curl http://localhost:3000
|
||||
|
||||
# Database health
|
||||
docker exec picpeak-postgres pg_isready
|
||||
```
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# Enter backend container
|
||||
docker exec -it picpeak-backend sh
|
||||
|
||||
# Enter database
|
||||
docker exec -it picpeak-postgres psql -U picpeak picpeak_prod
|
||||
|
||||
# Reset admin password
|
||||
docker exec picpeak-backend node scripts/show-admin-credentials.js --reset
|
||||
|
||||
# Check disk usage
|
||||
df -h
|
||||
du -sh events/ storage/ backup/
|
||||
|
||||
# View running processes
|
||||
docker compose top
|
||||
```
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
1. **Use HTTPS**: Always use a reverse proxy with SSL in production
|
||||
2. **Firewall**: Only expose necessary ports (80, 443)
|
||||
3. **Secure passwords**: Use strong, unique passwords for all services
|
||||
4. **Regular updates**: Keep Docker images and system packages updated
|
||||
5. **Backup strategy**: Set up automated backups and test restoration
|
||||
6. **Monitor logs**: Regularly check logs for suspicious activity
|
||||
7. **Rate limiting**: The app includes built-in rate limiting, configure as needed
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
- Check logs first: `docker compose logs`
|
||||
- Review documentation in the repository
|
||||
- Check existing issues on GitHub
|
||||
- Create a new issue with:
|
||||
- Error messages
|
||||
- Log output
|
||||
- Environment details (without secrets)
|
||||
- Steps to reproduce
|
||||
@@ -1,378 +0,0 @@
|
||||
# Production Deployment Guide
|
||||
|
||||
This comprehensive guide addresses all production deployment scenarios and common issues.
|
||||
|
||||
## Pre-Deployment Checklist
|
||||
|
||||
### 1. Environment Variables
|
||||
Create a `.env` file with ALL required variables:
|
||||
|
||||
```bash
|
||||
# CRITICAL - Must change these!
|
||||
JWT_SECRET=<generate-with-openssl-rand-base64-32>
|
||||
DB_PASSWORD=<strong-password>
|
||||
|
||||
# Application URLs (your actual domain)
|
||||
ADMIN_URL=https://yourdomain.com
|
||||
FRONTEND_URL=https://yourdomain.com
|
||||
BACKEND_URL=https://yourdomain.com
|
||||
|
||||
# Database (PostgreSQL)
|
||||
DATABASE_CLIENT=pg
|
||||
DB_HOST=postgres # or external host
|
||||
DB_PORT=5432
|
||||
DB_USER=picpeak
|
||||
DB_NAME=picpeak
|
||||
|
||||
# Email Configuration (required for notifications)
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password # Use app-specific password
|
||||
EMAIL_FROM=PicPeak <noreply@yourdomain.com>
|
||||
|
||||
# Port Configuration
|
||||
PORT=3001
|
||||
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# Generate JWT Secret (REQUIRED)
|
||||
openssl rand -base64 32
|
||||
|
||||
# Generate Database Password
|
||||
openssl rand -base64 24
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
### 1. Initial Setup
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/the-luap/wedding-photo-sharing.git
|
||||
cd wedding-photo-sharing
|
||||
|
||||
# Create required directories
|
||||
mkdir -p storage/events/active storage/events/archived storage/thumbnails storage/uploads
|
||||
mkdir -p data logs
|
||||
mkdir -p certbot/conf certbot/www
|
||||
|
||||
# Set permissions (important!)
|
||||
chmod -R 755 storage data logs
|
||||
```
|
||||
|
||||
### 2. Fix Docker Volume Permissions
|
||||
|
||||
Create `docker-compose.override.yml` for local volume configuration:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
volumes:
|
||||
- ./storage:/app/storage:delegated
|
||||
- ./data:/app/data:delegated
|
||||
- ./logs:/app/logs:delegated
|
||||
user: "1001:1001" # nodejs user
|
||||
|
||||
db:
|
||||
volumes:
|
||||
- ./postgres-data:/var/lib/postgresql/data
|
||||
```
|
||||
|
||||
### 3. Build and Deploy
|
||||
|
||||
```bash
|
||||
# Build images
|
||||
docker-compose -f docker-compose.prod.yml build
|
||||
|
||||
# Start services
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# Check logs
|
||||
docker-compose -f docker-compose.prod.yml logs -f backend
|
||||
```
|
||||
|
||||
### 4. Initial Admin Setup
|
||||
|
||||
The admin user is automatically created during database migration:
|
||||
|
||||
```bash
|
||||
# Run migrations (this creates admin user)
|
||||
docker-compose -f docker-compose.prod.yml exec backend npm run migrate
|
||||
|
||||
# Admin credentials will be displayed in console and saved to ADMIN_CREDENTIALS.txt
|
||||
# Example output:
|
||||
# ========================================
|
||||
# ✅ Admin user created successfully!
|
||||
# ========================================
|
||||
# Username: admin
|
||||
# Password: SwiftEagle3847!
|
||||
#
|
||||
# ⚠️ IMPORTANT: Change password on first login
|
||||
# ========================================
|
||||
|
||||
# Retrieve credentials if needed
|
||||
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)
|
||||
|
||||
1. Login to admin panel: https://yourdomain.com/admin
|
||||
2. Go to Settings > Email Configuration
|
||||
3. Enter SMTP details
|
||||
4. Test email sending
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue 1: Migration Failures
|
||||
|
||||
**Error**: "relation already exists"
|
||||
|
||||
**Solution**: The safe migration runner handles this automatically. If issues persist:
|
||||
|
||||
```bash
|
||||
# Reset migrations tracking
|
||||
docker-compose -f docker-compose.prod.yml exec db psql -U picpeak -d picpeak
|
||||
|
||||
# In PostgreSQL:
|
||||
DROP TABLE IF EXISTS migrations;
|
||||
\q
|
||||
|
||||
# Re-run migrations
|
||||
docker-compose -f docker-compose.prod.yml exec backend npm run migrate:safe
|
||||
```
|
||||
|
||||
### Issue 2: Permission Denied Errors
|
||||
|
||||
**Error**: "EACCES: permission denied"
|
||||
|
||||
**Solution**: Fix container permissions:
|
||||
|
||||
```bash
|
||||
# Stop containers
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
|
||||
# Fix permissions on host
|
||||
sudo chown -R 1001:1001 storage data logs
|
||||
|
||||
# Restart
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Issue 3: Database Connection Failed
|
||||
|
||||
**Error**: "no pg_hba.conf entry"
|
||||
|
||||
**Solution**: Already fixed in docker-compose.prod.yml with:
|
||||
- SSL disabled for internal Docker network
|
||||
- Proper authentication method (scram-sha-256)
|
||||
|
||||
### Issue 4: Frontend Can't Connect to Backend
|
||||
|
||||
**Error**: CORS errors or connection refused
|
||||
|
||||
**Solution**: Ensure environment variables match:
|
||||
- Backend: `FRONTEND_URL` must match your frontend URL
|
||||
- Frontend: `VITE_API_URL` must be set during build
|
||||
|
||||
### Issue 5: Email Not Sending
|
||||
|
||||
**Solution**: Check email configuration:
|
||||
|
||||
```bash
|
||||
# Check backend logs
|
||||
docker-compose -f docker-compose.prod.yml logs backend | grep email
|
||||
|
||||
# Verify SMTP settings
|
||||
# Gmail users: Use app password, not regular password
|
||||
# Enable "Less secure app access" or use OAuth2
|
||||
```
|
||||
|
||||
## 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
|
||||
2. Run certbot:
|
||||
|
||||
```bash
|
||||
# Initial certificate
|
||||
docker-compose -f docker-compose.prod.yml run --rm certbot certonly \
|
||||
--webroot --webroot-path=/var/www/certbot \
|
||||
-d yourdomain.com -d www.yourdomain.com
|
||||
|
||||
# Auto-renewal is handled by the certbot container
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Backend health
|
||||
curl http://localhost/api/health
|
||||
|
||||
# Database connection
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql -U picpeak -d picpeak -c "SELECT 1"
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
# All services
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
|
||||
# Specific service
|
||||
docker-compose -f docker-compose.prod.yml logs -f backend
|
||||
```
|
||||
|
||||
## Backup and Restore
|
||||
|
||||
### Backup
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup.sh
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR="./backups/$DATE"
|
||||
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Database
|
||||
docker-compose -f docker-compose.prod.yml exec -T db \
|
||||
pg_dump -U picpeak picpeak > $BACKUP_DIR/database.sql
|
||||
|
||||
# Files
|
||||
tar -czf $BACKUP_DIR/storage.tar.gz storage/
|
||||
|
||||
echo "Backup completed: $BACKUP_DIR"
|
||||
```
|
||||
|
||||
### Restore
|
||||
|
||||
```bash
|
||||
# Database
|
||||
docker-compose -f docker-compose.prod.yml exec -T db \
|
||||
psql -U picpeak picpeak < ./backups/20240713_120000/database.sql
|
||||
|
||||
# Files
|
||||
tar -xzf ./backups/20240713_120000/storage.tar.gz
|
||||
```
|
||||
|
||||
## Production Best Practices
|
||||
|
||||
1. **Always use named volumes** in production for better data persistence
|
||||
2. **Set up monitoring** with Prometheus/Grafana
|
||||
3. **Enable backups** with automated scripts
|
||||
4. **Use a reverse proxy** (Nginx) for SSL termination
|
||||
5. **Implement rate limiting** at the Nginx level
|
||||
6. **Regular updates** - Keep Docker images updated
|
||||
7. **Log rotation** - Configure log rotation for application logs
|
||||
|
||||
## Troubleshooting Commands
|
||||
|
||||
```bash
|
||||
# Check running containers
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# Restart a service
|
||||
docker-compose -f docker-compose.prod.yml restart backend
|
||||
|
||||
# View real-time logs
|
||||
docker-compose -f docker-compose.prod.yml logs -f --tail=100
|
||||
|
||||
# Execute commands in container
|
||||
docker-compose -f docker-compose.prod.yml exec backend sh
|
||||
|
||||
# Database shell
|
||||
docker-compose -f docker-compose.prod.yml exec db psql -U picpeak
|
||||
|
||||
# Clean restart
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Strong JWT_SECRET (min 32 chars)
|
||||
- [ ] Strong database password
|
||||
- [ ] Admin password changed from auto-generated one
|
||||
- [ ] SSL/HTTPS enabled
|
||||
- [ ] Firewall configured (only 80/443 open)
|
||||
- [ ] Regular security updates
|
||||
- [ ] Backup encryption
|
||||
- [ ] Access logs monitored
|
||||
- [ ] Rate limiting enabled (built-in)
|
||||
- [ ] 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
|
||||
|
||||
For issues not covered here:
|
||||
1. Check application logs
|
||||
2. Review error messages carefully
|
||||
3. Ensure all environment variables are set
|
||||
4. Verify file permissions
|
||||
5. Check Docker daemon logs
|
||||
@@ -138,7 +138,7 @@ PicPeak takes security seriously:
|
||||
- 📝 Activity logging
|
||||
- 🔒 Secure file access
|
||||
|
||||
Found a security issue? Please email security@example.com
|
||||
Found a security issue? Please open a [security issue](https://github.com/the-luap/picpeak/issues/new?labels=security) on GitHub
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
@@ -182,8 +182,9 @@ We're constantly improving PicPeak and welcome contributions from our community!
|
||||
| **Backup & Restore** | Comprehensive backup system with S3/MinIO support, automated scheduling, and safe restore functionality | High | ✅ Implemented |
|
||||
| **Gallery Templates** | Additional gallery layouts and themes (masonry, slideshow, story-style) for different event types | Medium | 🔄 Open |
|
||||
| **Face Recognition** | AI-powered face detection to help guests find their photos and create automatic person-based albums | Low | 🔄 Open |
|
||||
| **Gallery Feedback** | Allow guests to like, rate, and comment on photos with admin notifications and moderation | Medium | 🔄 Open |
|
||||
| **Gallery Feedback** | Allow guests to like, rate, and comment on photos with admin notifications and moderation | Medium | ✅ Implemented (not tested) |
|
||||
| **Video Support** | Upload and display videos alongside photos in galleries with streaming support | Low | 🔄 Open |
|
||||
| **Multiple Administrators** | Support for multiple admin accounts with role-based permissions and activity tracking | Low | 📋 Planned |
|
||||
|
||||
**Status Legend:** ✅ Implemented | 🚧 In Progress | 🔄 Open | 📋 Planned
|
||||
|
||||
@@ -191,6 +192,16 @@ We're constantly improving PicPeak and welcome contributions from our community!
|
||||
|
||||
PicPeak is inspired by the best features of commercial platforms while remaining completely open source. Special thanks to all contributors who make this project possible.
|
||||
|
||||
### 🤖 AI-Assisted Development
|
||||
|
||||
This project was generated with the assistance of AI technology, but has been:
|
||||
- ✅ **Fully tested end-to-end** by human developers
|
||||
- 🔒 **Security audited** with comprehensive security checks
|
||||
- 👨💻 **Human-reviewed** for code quality and best practices
|
||||
- 🧪 **Production-tested** in real-world scenarios
|
||||
|
||||
We believe in transparent development practices and the responsible use of AI as a tool to accelerate development while maintaining high standards of quality and security.
|
||||
|
||||
## 📄 License
|
||||
|
||||
PicPeak is released under the [MIT License](LICENSE). Use it freely for personal or commercial projects.
|
||||
|
||||
+10
-7
@@ -15,11 +15,14 @@ We take the security of PicPeak seriously. If you have discovered a security vul
|
||||
|
||||
### 1. **Do NOT create a public GitHub issue**
|
||||
|
||||
### 2. Email us at security@example.com with:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Potential impact
|
||||
- Suggested fix (if any)
|
||||
### 2. Report the vulnerability by:
|
||||
- Opening a [security issue](https://github.com/the-luap/picpeak/issues/new?labels=security) on GitHub
|
||||
- Mark it clearly as "SECURITY" in the title
|
||||
- Include:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Potential impact
|
||||
- Suggested fix (if any)
|
||||
|
||||
### 3. You can expect:
|
||||
- Acknowledgment within 48 hours
|
||||
@@ -79,7 +82,7 @@ We believe in responsible disclosure. Once a vulnerability is fixed:
|
||||
|
||||
## Contact
|
||||
|
||||
- Security issues: security@example.com
|
||||
- General support: https://github.com/the-luap/picpeak/issues
|
||||
- Security issues: [Create a security issue](https://github.com/the-luap/picpeak/issues/new?labels=security) on GitHub
|
||||
- General support: [GitHub Issues](https://github.com/the-luap/picpeak/issues)
|
||||
|
||||
Thank you for helping keep PicPeak and its users safe!
|
||||
@@ -12,6 +12,7 @@ JWT_SECRET=your-very-secure-jwt-secret-at-least-32-characters-long-example123456
|
||||
# URLs (adjust for your domain)
|
||||
ADMIN_URL=https://photos.example.com
|
||||
FRONTEND_URL=https://photos.example.com
|
||||
BACKEND_URL=https://photos.example.com # Or https://api.photos.example.com if separate
|
||||
|
||||
# Database Configuration
|
||||
DATABASE_CLIENT=pg
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Database Migrations
|
||||
|
||||
This directory contains database migrations for the Wedding Photo Sharing platform.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
### `/core`
|
||||
Essential migrations that are always run for new deployments. These include:
|
||||
- `init.js` - Initial database schema creation
|
||||
- Backup service tables (029-035)
|
||||
- Gallery feedback tables (033)
|
||||
|
||||
### `/legacy`
|
||||
Migrations needed only when upgrading from older versions. New deployments can skip these as the core schema already includes all necessary tables and columns.
|
||||
|
||||
## For New Deployments
|
||||
|
||||
If you're deploying this application for the first time:
|
||||
1. The `initializeDatabase()` function in `src/database/db.js` will create all necessary tables
|
||||
2. Only migrations in the `/core` directory will be run
|
||||
3. This ensures a clean, optimized database schema
|
||||
|
||||
## For Existing Deployments
|
||||
|
||||
If you're upgrading from an older version:
|
||||
1. All migrations (both core and legacy) will be run in sequence
|
||||
2. The migration system tracks which migrations have been applied
|
||||
3. Only new migrations will be executed
|
||||
|
||||
## Running Migrations
|
||||
|
||||
```bash
|
||||
# Development
|
||||
npm run migrate
|
||||
|
||||
# Production
|
||||
npm run migrate:prod
|
||||
```
|
||||
|
||||
## Note on Duplicate Migration Numbers
|
||||
|
||||
The legacy directory contains renamed duplicates:
|
||||
- `014_add_host_name_to_events_duplicate.js` (was duplicate of 014)
|
||||
- `027_add_rate_limit_settings_duplicate.js` (was duplicate of 027)
|
||||
|
||||
These have been renamed to avoid conflicts while preserving the migration history.
|
||||
@@ -1,33 +1,38 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const { db, initializeDatabase } = require('../src/database/db');
|
||||
const { generateReadablePassword } = require('../src/utils/passwordGenerator');
|
||||
const { initializeDatabase } = require('../../src/database/db');
|
||||
const { generateReadablePassword } = require('../../src/utils/passwordGenerator');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
async function runMigrations() {
|
||||
console.log('Running database migrations...');
|
||||
exports.up = async function(knex) {
|
||||
console.log('Initializing database schema...');
|
||||
|
||||
try {
|
||||
// Initialize tables
|
||||
await initializeDatabase();
|
||||
|
||||
// Create default admin user if none exists
|
||||
const adminExists = await db('admin_users').first();
|
||||
const adminExists = await knex('admin_users').first();
|
||||
if (!adminExists) {
|
||||
// Generate a secure random password
|
||||
const generatedPassword = generateReadablePassword();
|
||||
const passwordHash = await bcrypt.hash(generatedPassword, 12); // Increased rounds for better security
|
||||
|
||||
await db('admin_users').insert({
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
// Get admin credentials from environment or use defaults
|
||||
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
|
||||
const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com';
|
||||
|
||||
await knex('admin_users').insert({
|
||||
username: adminUsername,
|
||||
email: adminEmail,
|
||||
password_hash: passwordHash,
|
||||
must_change_password: true, // Flag for forcing password change
|
||||
created_at: new Date()
|
||||
});
|
||||
|
||||
// Save the generated password to a file for the user to retrieve
|
||||
const setupInfoPath = path.join(__dirname, '..', '..', 'ADMIN_CREDENTIALS.txt');
|
||||
// Try to save credentials to file, but don't fail if we can't
|
||||
const dataDir = path.join(__dirname, '..', '..', 'data');
|
||||
const setupInfoPath = path.join(dataDir, 'ADMIN_CREDENTIALS.txt');
|
||||
|
||||
const setupInfo = `
|
||||
========================================
|
||||
PicPeak Admin Credentials
|
||||
@@ -35,39 +40,49 @@ PicPeak Admin Credentials
|
||||
|
||||
Your admin account has been created with these credentials:
|
||||
|
||||
Username: admin
|
||||
Email: ${adminEmail}
|
||||
Password: ${generatedPassword}
|
||||
|
||||
IMPORTANT SECURITY NOTES:
|
||||
1. You MUST change this password on first login
|
||||
1. Please change this password after first login
|
||||
2. This file will be created only once
|
||||
3. Store these credentials securely
|
||||
4. Delete this file after noting the password
|
||||
|
||||
Login URL: ${process.env.ADMIN_URL || 'http://localhost:3001'}/admin
|
||||
|
||||
Login with the email address shown above
|
||||
|
||||
Generated on: ${new Date().toISOString()}
|
||||
========================================
|
||||
`;
|
||||
|
||||
await fs.writeFile(setupInfoPath, setupInfo, 'utf8');
|
||||
try {
|
||||
// Try to create directory and write file
|
||||
await fs.mkdir(dataDir, { recursive: true });
|
||||
await fs.writeFile(setupInfoPath, setupInfo, 'utf8');
|
||||
console.log(`📁 Credentials also saved to: data/ADMIN_CREDENTIALS.txt`);
|
||||
} catch (error) {
|
||||
// If we can't write the file, that's okay - credentials are shown in console
|
||||
console.log('⚠️ Could not save credentials to file (permission denied)');
|
||||
console.log(' Please copy the credentials shown above');
|
||||
}
|
||||
|
||||
console.log('\n========================================');
|
||||
console.log('✅ Admin user created successfully!');
|
||||
console.log('========================================');
|
||||
console.log('Username: admin');
|
||||
console.log(`Email: ${adminEmail}`);
|
||||
console.log(`Password: ${generatedPassword}`);
|
||||
console.log('\n⚠️ IMPORTANT:');
|
||||
console.log('1. Save these credentials securely');
|
||||
console.log('2. You will be required to change the password on first login');
|
||||
console.log('3. Credentials are also saved in: ADMIN_CREDENTIALS.txt');
|
||||
console.log('2. Please change the password after first login');
|
||||
console.log('========================================\n');
|
||||
}
|
||||
|
||||
// Create default email templates if none exist
|
||||
const templateExists = await db('email_templates').first();
|
||||
const templateExists = await knex('email_templates').first();
|
||||
if (!templateExists) {
|
||||
await db('email_templates').insert([
|
||||
await knex('email_templates').insert([
|
||||
{
|
||||
template_key: 'gallery_created',
|
||||
subject: 'Your Photo Gallery is Ready!',
|
||||
@@ -101,9 +116,9 @@ Generated on: ${new Date().toISOString()}
|
||||
}
|
||||
|
||||
// Create default email config if none exists
|
||||
const emailConfig = await db('email_configs').first();
|
||||
const emailConfig = await knex('email_configs').first();
|
||||
if (!emailConfig) {
|
||||
await db('email_configs').insert({
|
||||
await knex('email_configs').insert({
|
||||
smtp_host: process.env.SMTP_HOST || 'mailhog',
|
||||
smtp_port: process.env.SMTP_PORT || 1025,
|
||||
smtp_secure: process.env.SMTP_SECURE === 'true',
|
||||
@@ -116,11 +131,13 @@ Generated on: ${new Date().toISOString()}
|
||||
}
|
||||
|
||||
console.log('Migrations completed successfully');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
console.error('Initial setup failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
runMigrations();
|
||||
exports.down = async function(knex) {
|
||||
// This migration cannot be rolled back as it creates the initial schema
|
||||
console.log('Initial setup cannot be rolled back');
|
||||
};
|
||||
+7
-30
@@ -1,4 +1,4 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const { db } = require('../../src/database/db');
|
||||
|
||||
async function up() {
|
||||
console.log('Adding backup service tables and settings...');
|
||||
@@ -156,9 +156,8 @@ async function up() {
|
||||
const backupEmailTemplates = [
|
||||
{
|
||||
template_key: 'backup_failed',
|
||||
subject_en: 'Backup Failed - Immediate Attention Required',
|
||||
subject_de: 'Backup fehlgeschlagen - Sofortige Aufmerksamkeit erforderlich',
|
||||
body_html_en: `<h2>Backup Failed</h2>
|
||||
subject: 'Backup Failed - Immediate Attention Required',
|
||||
body_html: `<h2>Backup Failed</h2>
|
||||
<p>The scheduled backup has failed and requires immediate attention.</p>
|
||||
<p><strong>Error Details:</strong></p>
|
||||
<ul>
|
||||
@@ -167,24 +166,13 @@ async function up() {
|
||||
<li>Error: {{error_message}}</li>
|
||||
</ul>
|
||||
<p>Please check the system logs for more details and resolve the issue as soon as possible.</p>`,
|
||||
body_html_de: `<h2>Backup fehlgeschlagen</h2>
|
||||
<p>Das geplante Backup ist fehlgeschlagen und erfordert sofortige Aufmerksamkeit.</p>
|
||||
<p><strong>Fehlerdetails:</strong></p>
|
||||
<ul>
|
||||
<li>Startzeit: {{start_time}}</li>
|
||||
<li>Backup-Typ: {{backup_type}}</li>
|
||||
<li>Fehler: {{error_message}}</li>
|
||||
</ul>
|
||||
<p>Bitte überprüfen Sie die Systemprotokolle für weitere Details und beheben Sie das Problem so schnell wie möglich.</p>`,
|
||||
body_text_en: 'Backup Failed\n\nThe scheduled backup has failed and requires immediate attention.\n\nStart Time: {{start_time}}\nBackup Type: {{backup_type}}\nError: {{error_message}}\n\nPlease check the system logs for more details.',
|
||||
body_text_de: 'Backup fehlgeschlagen\n\nDas geplante Backup ist fehlgeschlagen und erfordert sofortige Aufmerksamkeit.\n\nStartzeit: {{start_time}}\nBackup-Typ: {{backup_type}}\nFehler: {{error_message}}\n\nBitte überprüfen Sie die Systemprotokolle für weitere Details.',
|
||||
body_text: 'Backup Failed\n\nThe scheduled backup has failed and requires immediate attention.\n\nStart Time: {{start_time}}\nBackup Type: {{backup_type}}\nError: {{error_message}}\n\nPlease check the system logs for more details.',
|
||||
variables: JSON.stringify(['start_time', 'backup_type', 'error_message'])
|
||||
},
|
||||
{
|
||||
template_key: 'backup_completed',
|
||||
subject_en: 'Backup Completed Successfully',
|
||||
subject_de: 'Backup erfolgreich abgeschlossen',
|
||||
body_html_en: `<h2>Backup Completed</h2>
|
||||
subject: 'Backup Completed Successfully',
|
||||
body_html: `<h2>Backup Completed</h2>
|
||||
<p>The scheduled backup has been completed successfully.</p>
|
||||
<p><strong>Backup Summary:</strong></p>
|
||||
<ul>
|
||||
@@ -194,18 +182,7 @@ async function up() {
|
||||
<li>Total Size: {{total_size}}</li>
|
||||
<li>Backup Type: {{backup_type}}</li>
|
||||
</ul>`,
|
||||
body_html_de: `<h2>Backup abgeschlossen</h2>
|
||||
<p>Das geplante Backup wurde erfolgreich abgeschlossen.</p>
|
||||
<p><strong>Backup-Zusammenfassung:</strong></p>
|
||||
<ul>
|
||||
<li>Startzeit: {{start_time}}</li>
|
||||
<li>Dauer: {{duration}}</li>
|
||||
<li>Gesicherte Dateien: {{files_count}}</li>
|
||||
<li>Gesamtgröße: {{total_size}}</li>
|
||||
<li>Backup-Typ: {{backup_type}}</li>
|
||||
</ul>`,
|
||||
body_text_en: 'Backup Completed\n\nThe scheduled backup has been completed successfully.\n\nStart Time: {{start_time}}\nDuration: {{duration}}\nFiles Backed Up: {{files_count}}\nTotal Size: {{total_size}}\nBackup Type: {{backup_type}}',
|
||||
body_text_de: 'Backup abgeschlossen\n\nDas geplante Backup wurde erfolgreich abgeschlossen.\n\nStartzeit: {{start_time}}\nDauer: {{duration}}\nGesicherte Dateien: {{files_count}}\nGesamtgröße: {{total_size}}\nBackup-Typ: {{backup_type}}',
|
||||
body_text: 'Backup Completed\n\nThe scheduled backup has been completed successfully.\n\nStart Time: {{start_time}}\nDuration: {{duration}}\nFiles Backed Up: {{files_count}}\nTotal Size: {{total_size}}\nBackup Type: {{backup_type}}',
|
||||
variables: JSON.stringify(['start_time', 'duration', 'files_count', 'total_size', 'backup_type'])
|
||||
}
|
||||
];
|
||||
+7
-30
@@ -1,4 +1,4 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const { db } = require('../../src/database/db');
|
||||
|
||||
async function up() {
|
||||
console.log('Adding database backup tables and settings...');
|
||||
@@ -96,9 +96,8 @@ async function up() {
|
||||
const databaseBackupEmailTemplates = [
|
||||
{
|
||||
template_key: 'database_backup_failed',
|
||||
subject_en: 'Database Backup Failed - Critical Alert',
|
||||
subject_de: 'Datenbank-Backup fehlgeschlagen - Kritische Warnung',
|
||||
body_html_en: `<h2>Database Backup Failed</h2>
|
||||
subject: 'Database Backup Failed - Critical Alert',
|
||||
body_html: `<h2>Database Backup Failed</h2>
|
||||
<p>The scheduled database backup has failed and requires immediate attention.</p>
|
||||
<p><strong>Error Details:</strong></p>
|
||||
<ul>
|
||||
@@ -107,24 +106,13 @@ async function up() {
|
||||
<li>Error: {{error_message}}</li>
|
||||
</ul>
|
||||
<p>This is a critical issue that could affect disaster recovery. Please investigate immediately.</p>`,
|
||||
body_html_de: `<h2>Datenbank-Backup fehlgeschlagen</h2>
|
||||
<p>Das geplante Datenbank-Backup ist fehlgeschlagen und erfordert sofortige Aufmerksamkeit.</p>
|
||||
<p><strong>Fehlerdetails:</strong></p>
|
||||
<ul>
|
||||
<li>Backup-Typ: {{backup_type}}</li>
|
||||
<li>Zeitstempel: {{timestamp}}</li>
|
||||
<li>Fehler: {{error_message}}</li>
|
||||
</ul>
|
||||
<p>Dies ist ein kritisches Problem, das die Disaster-Recovery beeinträchtigen könnte. Bitte untersuchen Sie es sofort.</p>`,
|
||||
body_text_en: 'Database Backup Failed\n\nThe scheduled database backup has failed.\n\nBackup Type: {{backup_type}}\nTimestamp: {{timestamp}}\nError: {{error_message}}\n\nThis is critical - please investigate immediately.',
|
||||
body_text_de: 'Datenbank-Backup fehlgeschlagen\n\nDas geplante Datenbank-Backup ist fehlgeschlagen.\n\nBackup-Typ: {{backup_type}}\nZeitstempel: {{timestamp}}\nFehler: {{error_message}}\n\nDies ist kritisch - bitte sofort untersuchen.',
|
||||
body_text: 'Database Backup Failed\n\nThe scheduled database backup has failed.\n\nBackup Type: {{backup_type}}\nTimestamp: {{timestamp}}\nError: {{error_message}}\n\nThis is critical - please investigate immediately.',
|
||||
variables: JSON.stringify(['backup_type', 'timestamp', 'error_message'])
|
||||
},
|
||||
{
|
||||
template_key: 'database_backup_completed',
|
||||
subject_en: 'Database Backup Completed Successfully',
|
||||
subject_de: 'Datenbank-Backup erfolgreich abgeschlossen',
|
||||
body_html_en: `<h2>Database Backup Completed</h2>
|
||||
subject: 'Database Backup Completed Successfully',
|
||||
body_html: `<h2>Database Backup Completed</h2>
|
||||
<p>The scheduled database backup has been completed successfully.</p>
|
||||
<p><strong>Backup Summary:</strong></p>
|
||||
<ul>
|
||||
@@ -134,18 +122,7 @@ async function up() {
|
||||
<li>Compression Ratio: {{compression_ratio}}</li>
|
||||
<li>File Path: {{file_path}}</li>
|
||||
</ul>`,
|
||||
body_html_de: `<h2>Datenbank-Backup abgeschlossen</h2>
|
||||
<p>Das geplante Datenbank-Backup wurde erfolgreich abgeschlossen.</p>
|
||||
<p><strong>Backup-Zusammenfassung:</strong></p>
|
||||
<ul>
|
||||
<li>Backup-Typ: {{backup_type}}</li>
|
||||
<li>Dauer: {{duration}}</li>
|
||||
<li>Dateigröße: {{file_size}}</li>
|
||||
<li>Komprimierungsverhältnis: {{compression_ratio}}</li>
|
||||
<li>Dateipfad: {{file_path}}</li>
|
||||
</ul>`,
|
||||
body_text_en: 'Database Backup Completed\n\nThe scheduled database backup has been completed successfully.\n\nBackup Type: {{backup_type}}\nDuration: {{duration}}\nFile Size: {{file_size}}\nCompression Ratio: {{compression_ratio}}\nFile Path: {{file_path}}',
|
||||
body_text_de: 'Datenbank-Backup abgeschlossen\n\nDas geplante Datenbank-Backup wurde erfolgreich abgeschlossen.\n\nBackup-Typ: {{backup_type}}\nDauer: {{duration}}\nDateigröße: {{file_size}}\nKomprimierungsverhältnis: {{compression_ratio}}\nDateipfad: {{file_path}}',
|
||||
body_text: 'Database Backup Completed\n\nThe scheduled database backup has been completed successfully.\n\nBackup Type: {{backup_type}}\nDuration: {{duration}}\nFile Size: {{file_size}}\nCompression Ratio: {{compression_ratio}}\nFile Path: {{file_path}}',
|
||||
variables: JSON.stringify(['backup_type', 'duration', 'file_size', 'compression_ratio', 'file_path'])
|
||||
}
|
||||
];
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const logger = require('../src/utils/logger');
|
||||
const { db } = require('../../src/database/db');
|
||||
const logger = require('../../src/utils/logger');
|
||||
|
||||
async function up() {
|
||||
console.log('Adding backup manifest columns...');
|
||||
+42
-67
@@ -5,7 +5,9 @@
|
||||
*/
|
||||
exports.up = async function(knex) {
|
||||
// Create restore_runs table
|
||||
await knex.schema.createTable('restore_runs', table => {
|
||||
const hasRestoreRunsTable = await knex.schema.hasTable('restore_runs');
|
||||
if (!hasRestoreRunsTable) {
|
||||
await knex.schema.createTable('restore_runs', table => {
|
||||
table.increments('id').primary();
|
||||
|
||||
// Timing
|
||||
@@ -44,10 +46,13 @@ exports.up = async function(knex) {
|
||||
|
||||
table.index(['status', 'started_at']);
|
||||
table.index(['restore_type', 'started_at']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Create restore_file_operations table for tracking individual file operations
|
||||
await knex.schema.createTable('restore_file_operations', table => {
|
||||
const hasRestoreFileOperationsTable = await knex.schema.hasTable('restore_file_operations');
|
||||
if (!hasRestoreFileOperationsTable) {
|
||||
await knex.schema.createTable('restore_file_operations', table => {
|
||||
table.increments('id').primary();
|
||||
|
||||
table.integer('restore_run_id').notNullable()
|
||||
@@ -67,10 +72,13 @@ exports.up = async function(knex) {
|
||||
|
||||
table.index(['restore_run_id', 'status']);
|
||||
table.index(['file_path']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Create restore_validation_results table
|
||||
await knex.schema.createTable('restore_validation_results', table => {
|
||||
const hasRestoreValidationResultsTable = await knex.schema.hasTable('restore_validation_results');
|
||||
if (!hasRestoreValidationResultsTable) {
|
||||
await knex.schema.createTable('restore_validation_results', table => {
|
||||
table.increments('id').primary();
|
||||
|
||||
table.integer('restore_run_id').notNullable()
|
||||
@@ -86,10 +94,11 @@ exports.up = async function(knex) {
|
||||
table.timestamp('validated_at').notNullable().defaultTo(knex.fn.now());
|
||||
|
||||
table.index(['restore_run_id', 'validation_type']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add restore-related settings to app_settings
|
||||
await knex('app_settings').insert([
|
||||
const restoreSettings = [
|
||||
{
|
||||
setting_key: 'restore_allow_force',
|
||||
setting_value: JSON.stringify(false),
|
||||
@@ -120,15 +129,24 @@ exports.up = async function(knex) {
|
||||
setting_value: '30',
|
||||
setting_type: 'restore'
|
||||
}
|
||||
]);
|
||||
];
|
||||
|
||||
for (const setting of restoreSettings) {
|
||||
const exists = await knex('app_settings')
|
||||
.where('setting_key', setting.setting_key)
|
||||
.first();
|
||||
|
||||
if (!exists) {
|
||||
await knex('app_settings').insert(setting);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new email templates for restore notifications
|
||||
const emailTemplates = [
|
||||
{
|
||||
template_key: 'restore_completed',
|
||||
subject_en: '✅ Restore Completed Successfully',
|
||||
subject_de: '✅ Wiederherstellung erfolgreich abgeschlossen',
|
||||
body_html_en: `<h2>Restore Operation Completed</h2>
|
||||
subject: '✅ Restore Completed Successfully',
|
||||
body_html: `<h2>Restore Operation Completed</h2>
|
||||
<p>A restore operation has completed successfully.</p>
|
||||
|
||||
<h3>Details:</h3>
|
||||
@@ -141,20 +159,7 @@ exports.up = async function(knex) {
|
||||
</ul>
|
||||
|
||||
<p>Please verify that all systems are functioning correctly after the restore.</p>`,
|
||||
body_html_de: `<h2>Wiederherstellungsvorgang abgeschlossen</h2>
|
||||
<p>Ein Wiederherstellungsvorgang wurde erfolgreich abgeschlossen.</p>
|
||||
|
||||
<h3>Details:</h3>
|
||||
<ul>
|
||||
<li><strong>Wiederherstellungstyp:</strong> {{restore_type}}</li>
|
||||
<li><strong>Dauer:</strong> {{duration}}</li>
|
||||
<li><strong>Wiederhergestellte Dateien:</strong> {{files_restored}}</li>
|
||||
<li><strong>Backup-ID:</strong> {{backup_id}}</li>
|
||||
<li><strong>Zeitstempel:</strong> {{timestamp}}</li>
|
||||
</ul>
|
||||
|
||||
<p>Bitte überprüfen Sie, ob alle Systeme nach der Wiederherstellung ordnungsgemäß funktionieren.</p>`,
|
||||
body_text_en: `Restore Operation Completed
|
||||
body_text: `Restore Operation Completed
|
||||
|
||||
A restore operation has completed successfully.
|
||||
|
||||
@@ -166,25 +171,12 @@ Details:
|
||||
- Timestamp: {{timestamp}}
|
||||
|
||||
Please verify that all systems are functioning correctly after the restore.`,
|
||||
body_text_de: `Wiederherstellungsvorgang abgeschlossen
|
||||
|
||||
Ein Wiederherstellungsvorgang wurde erfolgreich abgeschlossen.
|
||||
|
||||
Details:
|
||||
- Wiederherstellungstyp: {{restore_type}}
|
||||
- Dauer: {{duration}}
|
||||
- Wiederhergestellte Dateien: {{files_restored}}
|
||||
- Backup-ID: {{backup_id}}
|
||||
- Zeitstempel: {{timestamp}}
|
||||
|
||||
Bitte überprüfen Sie, ob alle Systeme nach der Wiederherstellung ordnungsgemäß funktionieren.`,
|
||||
variables: JSON.stringify(['restore_type', 'duration', 'files_restored', 'backup_id', 'timestamp'])
|
||||
},
|
||||
{
|
||||
template_key: 'restore_failed',
|
||||
subject_en: '❌ Restore Operation Failed',
|
||||
subject_de: '❌ Wiederherstellungsvorgang fehlgeschlagen',
|
||||
body_html_en: `<h2>Restore Operation Failed</h2>
|
||||
subject: '❌ Restore Operation Failed',
|
||||
body_html: `<h2>Restore Operation Failed</h2>
|
||||
<p>A restore operation has failed and requires attention.</p>
|
||||
|
||||
<h3>Details:</h3>
|
||||
@@ -197,20 +189,7 @@ Bitte überprüfen Sie, ob alle Systeme nach der Wiederherstellung ordnungsgemä
|
||||
<p>Please check the system logs for more details and take appropriate action.</p>
|
||||
|
||||
<p><strong>Important:</strong> If a pre-restore backup was created, it may be used for recovery.</p>`,
|
||||
body_html_de: `<h2>Wiederherstellungsvorgang fehlgeschlagen</h2>
|
||||
<p>Ein Wiederherstellungsvorgang ist fehlgeschlagen und erfordert Ihre Aufmerksamkeit.</p>
|
||||
|
||||
<h3>Details:</h3>
|
||||
<ul>
|
||||
<li><strong>Wiederherstellungstyp:</strong> {{restore_type}}</li>
|
||||
<li><strong>Fehler:</strong> {{error_message}}</li>
|
||||
<li><strong>Zeitstempel:</strong> {{timestamp}}</li>
|
||||
</ul>
|
||||
|
||||
<p>Bitte überprüfen Sie die Systemprotokolle für weitere Details und ergreifen Sie entsprechende Maßnahmen.</p>
|
||||
|
||||
<p><strong>Wichtig:</strong> Falls ein Backup vor der Wiederherstellung erstellt wurde, kann es zur Wiederherstellung verwendet werden.</p>`,
|
||||
body_text_en: `Restore Operation Failed
|
||||
body_text: `Restore Operation Failed
|
||||
|
||||
A restore operation has failed and requires attention.
|
||||
|
||||
@@ -222,23 +201,19 @@ Details:
|
||||
Please check the system logs for more details and take appropriate action.
|
||||
|
||||
Important: If a pre-restore backup was created, it may be used for recovery.`,
|
||||
body_text_de: `Wiederherstellungsvorgang fehlgeschlagen
|
||||
|
||||
Ein Wiederherstellungsvorgang ist fehlgeschlagen und erfordert Ihre Aufmerksamkeit.
|
||||
|
||||
Details:
|
||||
- Wiederherstellungstyp: {{restore_type}}
|
||||
- Fehler: {{error_message}}
|
||||
- Zeitstempel: {{timestamp}}
|
||||
|
||||
Bitte überprüfen Sie die Systemprotokolle für weitere Details und ergreifen Sie entsprechende Maßnahmen.
|
||||
|
||||
Wichtig: Falls ein Backup vor der Wiederherstellung erstellt wurde, kann es zur Wiederherstellung verwendet werden.`,
|
||||
variables: JSON.stringify(['restore_type', 'error_message', 'timestamp'])
|
||||
}
|
||||
];
|
||||
|
||||
await knex('email_templates').insert(emailTemplates);
|
||||
for (const template of emailTemplates) {
|
||||
const exists = await knex('email_templates')
|
||||
.where('template_key', template.template_key)
|
||||
.first();
|
||||
|
||||
if (!exists) {
|
||||
await knex('email_templates').insert(template);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const { db } = require('../../src/database/db');
|
||||
|
||||
async function up() {
|
||||
console.log('Adding version tracking to backup tables...');
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const { db } = require('../../src/database/db');
|
||||
|
||||
async function up() {
|
||||
console.log('Enhancing backup system...');
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const { db } = require('../../src/database/db');
|
||||
|
||||
async function up() {
|
||||
console.log('Adding photo categories and CMS tables...');
|
||||
@@ -40,7 +40,7 @@ async function up() {
|
||||
// Add language preference to app_settings for global default
|
||||
await db('app_settings').insert({
|
||||
setting_key: 'default_language',
|
||||
setting_value: 'en',
|
||||
setting_value: JSON.stringify('en'),
|
||||
setting_type: 'general',
|
||||
updated_at: new Date()
|
||||
});
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const { db } = require('../../src/database/db');
|
||||
|
||||
async function up() {
|
||||
// Check if host_name column already exists
|
||||
@@ -27,8 +27,12 @@ async function isMigrationApplied(filename) {
|
||||
|
||||
// Mark migration as applied without running it (for existing schema)
|
||||
async function markMigrationAsApplied(filename) {
|
||||
await db('migrations').insert({ filename });
|
||||
console.log(`Marked migration ${filename} as applied`);
|
||||
// Check if already marked to avoid duplicate key error
|
||||
const isApplied = await isMigrationApplied(filename);
|
||||
if (!isApplied) {
|
||||
await db('migrations').insert({ filename });
|
||||
console.log(`Marked migration ${filename} as applied`);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect existing schema and mark migrations as applied
|
||||
@@ -36,12 +40,14 @@ async function detectExistingSchema() {
|
||||
console.log('Detecting existing schema...');
|
||||
|
||||
const tableChecks = [
|
||||
{ table: 'events', migration: 'init.js' },
|
||||
{ table: 'photos', migration: 'init.js' },
|
||||
{ table: 'events', migration: '001_init.js' },
|
||||
{ table: 'photos', migration: '001_init.js' },
|
||||
{ table: 'photo_categories', migration: '004_add_categories_and_cms.js' },
|
||||
{ table: 'cms_pages', migration: '004_add_categories_and_cms.js' },
|
||||
{ table: 'login_attempts', migration: '015_add_login_attempts_table.js' },
|
||||
{ table: 'token_blacklist', migration: '017_add_token_revocation_tables.js' },
|
||||
{ table: 'backup_runs', migration: '029_add_backup_service_tables.js' },
|
||||
{ table: 'gallery_feedback', migration: '033_add_gallery_feedback.js' },
|
||||
];
|
||||
|
||||
for (const check of tableChecks) {
|
||||
@@ -56,13 +62,14 @@ async function detectExistingSchema() {
|
||||
}
|
||||
|
||||
// Run a single migration safely
|
||||
async function runMigrationSafely(filename) {
|
||||
async function runMigrationSafely(filepath) {
|
||||
try {
|
||||
const migrationPath = path.join(__dirname, filename);
|
||||
const migrationPath = path.join(__dirname, filepath);
|
||||
const migration = require(migrationPath);
|
||||
const filename = path.basename(filepath);
|
||||
|
||||
if (migration.up) {
|
||||
console.log(`Running migration: ${filename}`);
|
||||
console.log(`Running migration: ${filepath}`);
|
||||
|
||||
// Run migration in a transaction if possible
|
||||
if (db.client.config.client === 'pg') {
|
||||
@@ -74,14 +81,14 @@ async function runMigrationSafely(filename) {
|
||||
}
|
||||
|
||||
await db('migrations').insert({ filename });
|
||||
console.log(`Migration ${filename} completed successfully`);
|
||||
console.log(`Migration ${filepath} completed successfully`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Check if error is because schema already exists
|
||||
if (error.code === '42P07' || // PostgreSQL: relation already exists
|
||||
error.code === 'SQLITE_ERROR' && error.message.includes('already exists')) {
|
||||
console.log(`Migration ${filename} - schema already exists, marking as applied`);
|
||||
await markMigrationAsApplied(filename);
|
||||
console.log(`Migration ${filepath} - schema already exists, marking as applied`);
|
||||
await markMigrationAsApplied(path.basename(filepath));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -101,26 +108,79 @@ async function runMigrations() {
|
||||
// Create migrations tracking table
|
||||
await ensureMigrationsTable();
|
||||
|
||||
// Detect and mark existing schema
|
||||
await detectExistingSchema();
|
||||
// Check if essential tables exist to determine if this is truly a new deployment
|
||||
const hasEventsTable = await db.schema.hasTable('events');
|
||||
const hasPhotosTable = await db.schema.hasTable('photos');
|
||||
const hasAdminTable = await db.schema.hasTable('admin_users');
|
||||
const hasActivityLogsTable = await db.schema.hasTable('activity_logs');
|
||||
|
||||
// Get all migration files
|
||||
const files = await fs.readdir(__dirname);
|
||||
const migrationFiles = files
|
||||
.filter(f => f.match(/^\d{3}_.*\.js$/) || f === 'init.js')
|
||||
.sort((a, b) => {
|
||||
// Ensure init.js runs first
|
||||
if (a === 'init.js') return -1;
|
||||
if (b === 'init.js') return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
// Get applied migrations
|
||||
const appliedMigrations = await db('migrations').select('filename');
|
||||
const appliedFilenames = appliedMigrations.map(m => m.filename);
|
||||
|
||||
// Check if this is a new deployment
|
||||
// It's new if no essential tables exist OR no migrations have been applied
|
||||
const isNewDeployment = (!hasEventsTable || !hasPhotosTable || !hasAdminTable || !hasActivityLogsTable) || appliedFilenames.length === 0;
|
||||
|
||||
// Only detect existing schema for truly existing deployments
|
||||
if (!isNewDeployment) {
|
||||
await detectExistingSchema();
|
||||
}
|
||||
|
||||
// Get migration files from appropriate directories
|
||||
let migrationFiles = [];
|
||||
|
||||
if (isNewDeployment) {
|
||||
// For new deployments, only run core migrations
|
||||
console.log('New deployment detected - running core migrations only');
|
||||
const coreDir = path.join(__dirname, 'core');
|
||||
const coreFiles = await fs.readdir(coreDir);
|
||||
migrationFiles = coreFiles
|
||||
.filter(f => f.match(/^\d{3}_.*\.js$/))
|
||||
.map(f => path.join('core', f))
|
||||
.sort((a, b) => {
|
||||
const baseA = path.basename(a);
|
||||
const baseB = path.basename(b);
|
||||
const numA = parseInt(baseA.split('_')[0]);
|
||||
const numB = parseInt(baseB.split('_')[0]);
|
||||
return numA - numB;
|
||||
});
|
||||
} else {
|
||||
// For existing deployments, run all migrations (legacy + core)
|
||||
console.log('Existing deployment detected - checking all migrations');
|
||||
|
||||
// Get legacy migrations
|
||||
const legacyDir = path.join(__dirname, 'legacy');
|
||||
const legacyFiles = await fs.readdir(legacyDir);
|
||||
const legacyMigrations = legacyFiles
|
||||
.filter(f => f.match(/^\d{3}_.*\.js$/))
|
||||
.map(f => path.join('legacy', f));
|
||||
|
||||
// Get core migrations
|
||||
const coreDir = path.join(__dirname, 'core');
|
||||
const coreFiles = await fs.readdir(coreDir);
|
||||
const coreMigrations = coreFiles
|
||||
.filter(f => f.match(/^\d{3}_.*\.js$/))
|
||||
.map(f => path.join('core', f));
|
||||
|
||||
// Combine and sort by number
|
||||
migrationFiles = [...legacyMigrations, ...coreMigrations]
|
||||
.sort((a, b) => {
|
||||
const baseA = path.basename(a);
|
||||
const baseB = path.basename(b);
|
||||
const numA = parseInt(baseA.split('_')[0]);
|
||||
const numB = parseInt(baseB.split('_')[0]);
|
||||
return numA - numB;
|
||||
});
|
||||
}
|
||||
|
||||
// Run pending migrations
|
||||
let pendingCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const file of migrationFiles) {
|
||||
const isApplied = await isMigrationApplied(file);
|
||||
const filename = path.basename(file);
|
||||
const isApplied = appliedFilenames.includes(filename);
|
||||
if (!isApplied) {
|
||||
await runMigrationSafely(file);
|
||||
pendingCount++;
|
||||
|
||||
@@ -22,15 +22,16 @@ async function getAppliedMigrations() {
|
||||
}
|
||||
|
||||
// Run a single migration
|
||||
async function runMigration(filename) {
|
||||
const migrationPath = path.join(__dirname, filename);
|
||||
async function runMigration(filepath) {
|
||||
const migrationPath = path.join(__dirname, filepath);
|
||||
const migration = require(migrationPath);
|
||||
const filename = path.basename(filepath);
|
||||
|
||||
if (migration.up) {
|
||||
console.log(`Running migration: ${filename}`);
|
||||
console.log(`Running migration: ${filepath}`);
|
||||
await migration.up(db);
|
||||
await db('migrations').insert({ filename });
|
||||
console.log(`Migration ${filename} completed`);
|
||||
console.log(`Migration ${filepath} completed`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,19 +51,62 @@ async function runMigrations() {
|
||||
// Create migrations table
|
||||
await createMigrationsTable();
|
||||
|
||||
// Get all migration files
|
||||
const files = await fs.readdir(__dirname);
|
||||
const migrationFiles = files
|
||||
.filter(f => f.match(/^\d{3}_.*\.js$/))
|
||||
.sort();
|
||||
|
||||
// Get applied migrations
|
||||
const appliedMigrations = await getAppliedMigrations();
|
||||
|
||||
// Check if this is a new deployment (no migrations have been applied)
|
||||
const isNewDeployment = appliedMigrations.length === 0;
|
||||
|
||||
// Get migration files from appropriate directories
|
||||
let migrationFiles = [];
|
||||
|
||||
if (isNewDeployment) {
|
||||
// For new deployments, only run core migrations
|
||||
console.log('New deployment detected - running core migrations only');
|
||||
const coreDir = path.join(__dirname, 'core');
|
||||
const coreFiles = await fs.readdir(coreDir);
|
||||
migrationFiles = coreFiles
|
||||
.filter(f => f.match(/^\d{3}_.*\.js$/))
|
||||
.map(f => path.join('core', f))
|
||||
.sort((a, b) => {
|
||||
const baseA = path.basename(a);
|
||||
const baseB = path.basename(b);
|
||||
const numA = parseInt(baseA.split('_')[0]);
|
||||
const numB = parseInt(baseB.split('_')[0]);
|
||||
return numA - numB;
|
||||
});
|
||||
} else {
|
||||
// For existing deployments, run all migrations (legacy + core)
|
||||
console.log('Existing deployment detected - checking all migrations');
|
||||
|
||||
// Get legacy migrations
|
||||
const legacyDir = path.join(__dirname, 'legacy');
|
||||
const legacyFiles = await fs.readdir(legacyDir);
|
||||
const legacyMigrations = legacyFiles
|
||||
.filter(f => f.match(/^\d{3}_.*\.js$/))
|
||||
.map(f => path.join('legacy', f));
|
||||
|
||||
// Get core migrations
|
||||
const coreDir = path.join(__dirname, 'core');
|
||||
const coreFiles = await fs.readdir(coreDir);
|
||||
const coreMigrations = coreFiles
|
||||
.filter(f => f.match(/^\d{3}_.*\.js$/))
|
||||
.map(f => path.join('core', f));
|
||||
|
||||
// Combine and sort by number
|
||||
migrationFiles = [...legacyMigrations, ...coreMigrations]
|
||||
.sort((a, b) => {
|
||||
const numA = parseInt(path.basename(a).split('_')[0]);
|
||||
const numB = parseInt(path.basename(b).split('_')[0]);
|
||||
return numA - numB;
|
||||
});
|
||||
}
|
||||
|
||||
// Run pending migrations
|
||||
let pendingCount = 0;
|
||||
for (const file of migrationFiles) {
|
||||
if (!appliedMigrations.includes(file)) {
|
||||
const filename = path.basename(file);
|
||||
if (!appliedMigrations.includes(filename)) {
|
||||
await runMigration(file);
|
||||
pendingCount++;
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.93",
|
||||
"version": "1.0.99",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.93",
|
||||
"version": "1.0.99",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.850.0",
|
||||
"@aws-sdk/lib-storage": "^3.850.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.93",
|
||||
"version": "1.0.99",
|
||||
"description": "Backend for PicPeak event photo sharing platform",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function checkDatabaseIssues() {
|
||||
console.log('Checking database issues...\n');
|
||||
|
||||
try {
|
||||
// Check email_templates table structure
|
||||
console.log('1. Checking email_templates table structure:');
|
||||
const emailTemplateColumns = await db('email_templates').columnInfo();
|
||||
console.log('Columns:', Object.keys(emailTemplateColumns));
|
||||
|
||||
// Check if any templates exist
|
||||
const templateCount = await db('email_templates').count('* as count');
|
||||
console.log('Template count:', templateCount[0].count);
|
||||
|
||||
// Check for specific template
|
||||
const galleryCreatedTemplate = await db('email_templates')
|
||||
.where('template_key', 'gallery_created')
|
||||
.first();
|
||||
console.log('gallery_created template exists:', !!galleryCreatedTemplate);
|
||||
|
||||
// Check activity_logs table
|
||||
console.log('\n2. Checking activity_logs table:');
|
||||
const activityLogColumns = await db('activity_logs').columnInfo();
|
||||
console.log('Columns:', Object.keys(activityLogColumns));
|
||||
|
||||
// Check migrations table
|
||||
console.log('\n3. Checking migrations status:');
|
||||
const migrations = await db('migrations')
|
||||
.orderBy('id', 'desc')
|
||||
.limit(10);
|
||||
console.log('Latest migrations:');
|
||||
migrations.forEach(m => console.log(` - ${m.filename}`));
|
||||
|
||||
// Test a simple query from notifications route
|
||||
console.log('\n4. Testing notifications query:');
|
||||
try {
|
||||
const notifications = await db('activity_logs')
|
||||
.select(
|
||||
'activity_logs.*',
|
||||
'events.event_name'
|
||||
)
|
||||
.leftJoin('events', 'activity_logs.event_id', 'events.id')
|
||||
.orderBy('activity_logs.created_at', 'desc')
|
||||
.limit(5);
|
||||
console.log(`Found ${notifications.length} notifications`);
|
||||
} catch (error) {
|
||||
console.error('Notifications query failed:', error.message);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
checkDatabaseIssues();
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
|
||||
// Connect to the database
|
||||
const dbPath = '/app/data/photo_sharing.db';
|
||||
console.log(`Connecting to database at: ${dbPath}`);
|
||||
|
||||
const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
|
||||
if (err) {
|
||||
console.error('Error opening database:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Connected to the SQLite database.\n');
|
||||
});
|
||||
|
||||
// Get schema for events table
|
||||
console.log('=== EVENTS TABLE SCHEMA ===');
|
||||
db.all("PRAGMA table_info(events)", [], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error getting events schema:', err.message);
|
||||
} else {
|
||||
rows.forEach(row => {
|
||||
console.log(`${row.name} (${row.type})`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n=== PHOTOS TABLE SCHEMA ===');
|
||||
// Get schema for photos table
|
||||
db.all("PRAGMA table_info(photos)", [], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error getting photos schema:', err.message);
|
||||
} else {
|
||||
rows.forEach(row => {
|
||||
console.log(`${row.name} (${row.type})`);
|
||||
});
|
||||
}
|
||||
|
||||
// Close the database
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
@@ -1,131 +0,0 @@
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function checkEmailEnvironment() {
|
||||
console.log('=== Email Environment Check ===\n');
|
||||
|
||||
// 1. Check environment variables
|
||||
console.log('1. Environment Variables:');
|
||||
const envVars = [
|
||||
'SMTP_HOST',
|
||||
'SMTP_PORT',
|
||||
'SMTP_USER',
|
||||
'SMTP_PASS',
|
||||
'SMTP_FROM',
|
||||
'SMTP_SECURE',
|
||||
'EMAIL_PROCESSOR_ENABLED',
|
||||
'NODE_ENV'
|
||||
];
|
||||
|
||||
envVars.forEach(varName => {
|
||||
const value = process.env[varName];
|
||||
if (varName.includes('PASS')) {
|
||||
console.log(` ${varName}: ${value ? '***' : 'NOT SET'}`);
|
||||
} else {
|
||||
console.log(` ${varName}: ${value || 'NOT SET'}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Check database configuration
|
||||
console.log('\n2. Database Email Configuration:');
|
||||
try {
|
||||
const emailConfig = await db('email_configs').first();
|
||||
if (emailConfig) {
|
||||
console.log(' Email configuration found in database:');
|
||||
console.log(` - SMTP Host: ${emailConfig.smtp_host}`);
|
||||
console.log(` - SMTP Port: ${emailConfig.smtp_port}`);
|
||||
console.log(` - SMTP User: ${emailConfig.smtp_user || 'NOT SET'}`);
|
||||
console.log(` - SMTP Secure: ${emailConfig.smtp_secure}`);
|
||||
console.log(` - From Address: ${emailConfig.smtp_from}`);
|
||||
} else {
|
||||
console.log(' ⚠️ No email configuration found in database!');
|
||||
console.log(' This will prevent the email processor from initializing.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error reading email configuration: ${error.message}`);
|
||||
}
|
||||
|
||||
// 3. Check if the email processor should be disabled
|
||||
console.log('\n3. Email Processor Status:');
|
||||
const isDisabled = process.env.EMAIL_PROCESSOR_ENABLED === 'false';
|
||||
if (isDisabled) {
|
||||
console.log(' ⚠️ Email processor is DISABLED via EMAIL_PROCESSOR_ENABLED=false');
|
||||
} else {
|
||||
console.log(' ✅ Email processor is enabled (default)');
|
||||
}
|
||||
|
||||
// 4. Check pending emails
|
||||
console.log('\n4. Email Queue Status:');
|
||||
try {
|
||||
const pending = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.count('* as count')
|
||||
.first();
|
||||
|
||||
const failed = await db('email_queue')
|
||||
.where('status', 'failed')
|
||||
.where('retry_count', '>=', 3)
|
||||
.count('* as count')
|
||||
.first();
|
||||
|
||||
const sent = await db('email_queue')
|
||||
.where('status', 'sent')
|
||||
.count('* as count')
|
||||
.first();
|
||||
|
||||
console.log(` - Pending emails: ${pending.count}`);
|
||||
console.log(` - Failed emails (max retries): ${failed.count}`);
|
||||
console.log(` - Sent emails: ${sent.count}`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error querying email queue: ${error.message}`);
|
||||
}
|
||||
|
||||
// 5. Test database connection
|
||||
console.log('\n5. Database Connection:');
|
||||
try {
|
||||
await db.raw('SELECT 1');
|
||||
console.log(' ✅ Database connection successful');
|
||||
} catch (error) {
|
||||
console.log(` ❌ Database connection failed: ${error.message}`);
|
||||
}
|
||||
|
||||
// 6. Check for any recent errors
|
||||
console.log('\n6. Recent Email Errors:');
|
||||
try {
|
||||
const recentErrors = await db('email_queue')
|
||||
.whereNotNull('error_message')
|
||||
.orderBy('id', 'desc')
|
||||
.limit(3)
|
||||
.select('id', 'email_type', 'error_message', 'retry_count');
|
||||
|
||||
if (recentErrors.length > 0) {
|
||||
recentErrors.forEach((email, index) => {
|
||||
console.log(` ${index + 1}. Email ID ${email.id} (${email.email_type}):`);
|
||||
console.log(` Retries: ${email.retry_count}`);
|
||||
console.log(` Error: ${email.error_message}`);
|
||||
});
|
||||
} else {
|
||||
console.log(' No recent errors found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error querying recent errors: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log('\n=== Environment check complete ===');
|
||||
console.log('\nRecommendations:');
|
||||
|
||||
const emailConfig = await db('email_configs').first().catch(() => null);
|
||||
if (!emailConfig) {
|
||||
console.log('❗ Configure email settings in the admin panel or add email_configs record');
|
||||
}
|
||||
|
||||
if (!process.env.SMTP_HOST && !emailConfig) {
|
||||
console.log('❗ Set SMTP environment variables or configure in database');
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
}
|
||||
|
||||
checkEmailEnvironment().catch(error => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,157 +0,0 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const winston = require('winston');
|
||||
|
||||
// Create a simple console logger
|
||||
const logger = winston.createLogger({
|
||||
format: winston.format.simple(),
|
||||
transports: [new winston.transports.Console()]
|
||||
});
|
||||
|
||||
async function checkEmailProcessor() {
|
||||
try {
|
||||
logger.info('=== Email Processor Diagnostic Check ===\n');
|
||||
|
||||
// 1. Check pending emails
|
||||
logger.info('1. Checking pending emails in queue...');
|
||||
const pendingEmails = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.orderBy('created_at', 'asc');
|
||||
|
||||
logger.info(`Found ${pendingEmails.length} pending emails\n`);
|
||||
|
||||
if (pendingEmails.length > 0) {
|
||||
logger.info('Pending email details:');
|
||||
pendingEmails.forEach((email, index) => {
|
||||
logger.info(`\nEmail ${index + 1}:`);
|
||||
logger.info(` ID: ${email.id}`);
|
||||
logger.info(` Type: ${email.email_type}`);
|
||||
logger.info(` Recipient: ${email.recipient_email}`);
|
||||
logger.info(` Event ID: ${email.event_id}`);
|
||||
logger.info(` Status: ${email.status}`);
|
||||
logger.info(` Retry Count: ${email.retry_count}`);
|
||||
logger.info(` Scheduled At: ${email.scheduled_at}`);
|
||||
logger.info(` Created At: ${email.created_at}`);
|
||||
logger.info(` Error: ${email.error_message || 'None'}`);
|
||||
|
||||
// Check if email_data needs parsing
|
||||
logger.info(` Email Data Type: ${typeof email.email_data}`);
|
||||
if (email.email_data) {
|
||||
try {
|
||||
const data = typeof email.email_data === 'string'
|
||||
? JSON.parse(email.email_data)
|
||||
: email.email_data;
|
||||
logger.info(` Email Data Keys: ${Object.keys(data).join(', ')}`);
|
||||
} catch (e) {
|
||||
logger.error(` Failed to parse email_data: ${e.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Check failed emails
|
||||
logger.info('\n\n2. Checking failed emails...');
|
||||
const failedEmails = await db('email_queue')
|
||||
.where('status', 'failed')
|
||||
.orderBy('created_at', 'desc')
|
||||
.limit(5);
|
||||
|
||||
logger.info(`Found ${failedEmails.length} failed emails (showing last 5)\n`);
|
||||
|
||||
if (failedEmails.length > 0) {
|
||||
failedEmails.forEach((email, index) => {
|
||||
logger.info(`\nFailed Email ${index + 1}:`);
|
||||
logger.info(` ID: ${email.id}`);
|
||||
logger.info(` Type: ${email.email_type}`);
|
||||
logger.info(` Retry Count: ${email.retry_count}`);
|
||||
logger.info(` Error: ${email.error_message || 'No error message'}`);
|
||||
logger.info(` Last Attempt: ${email.sent_at || 'Never'}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Check if email processor should be running
|
||||
logger.info('\n\n3. Checking email processor configuration...');
|
||||
|
||||
// Check environment variables
|
||||
const emailConfig = {
|
||||
SMTP_HOST: process.env.SMTP_HOST,
|
||||
SMTP_PORT: process.env.SMTP_PORT,
|
||||
SMTP_USER: process.env.SMTP_USER,
|
||||
SMTP_FROM: process.env.SMTP_FROM,
|
||||
SMTP_SECURE: process.env.SMTP_SECURE,
|
||||
EMAIL_PROCESSOR_ENABLED: process.env.EMAIL_PROCESSOR_ENABLED || 'true'
|
||||
};
|
||||
|
||||
logger.info('Email configuration:');
|
||||
Object.entries(emailConfig).forEach(([key, value]) => {
|
||||
if (key === 'SMTP_USER') {
|
||||
logger.info(` ${key}: ${value ? '***' : 'NOT SET'}`);
|
||||
} else {
|
||||
logger.info(` ${key}: ${value || 'NOT SET'}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Test email processor functionality
|
||||
logger.info('\n\n4. Testing email processor functionality...');
|
||||
|
||||
// Import the email processor
|
||||
const { processEmailQueue, testEmailConnection } = require('../src/services/emailProcessor');
|
||||
|
||||
// Test email connection
|
||||
logger.info('Testing email connection...');
|
||||
try {
|
||||
const connectionTest = await testEmailConnection();
|
||||
logger.info(`Email connection test: ${connectionTest ? 'SUCCESS' : 'FAILED'}`);
|
||||
} catch (error) {
|
||||
logger.error(`Email connection test failed: ${error.message}`);
|
||||
}
|
||||
|
||||
// Try to process queue once manually
|
||||
if (pendingEmails.length > 0) {
|
||||
logger.info('\n\n5. Attempting to process email queue manually...');
|
||||
try {
|
||||
await processEmailQueue();
|
||||
logger.info('Manual queue processing completed');
|
||||
|
||||
// Check status after processing
|
||||
const stillPending = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.count('* as count')
|
||||
.first();
|
||||
|
||||
logger.info(`Emails still pending after processing: ${stillPending.count}`);
|
||||
} catch (error) {
|
||||
logger.error(`Error processing queue: ${error.message}`);
|
||||
logger.error(`Stack trace: ${error.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Check for any recent successful emails
|
||||
logger.info('\n\n6. Checking recent successful emails...');
|
||||
const recentSuccess = await db('email_queue')
|
||||
.where('status', 'sent')
|
||||
.orderBy('sent_at', 'desc')
|
||||
.limit(3);
|
||||
|
||||
if (recentSuccess.length > 0) {
|
||||
logger.info(`Last ${recentSuccess.length} successful emails:`);
|
||||
recentSuccess.forEach((email, index) => {
|
||||
logger.info(` ${index + 1}. Type: ${email.email_type}, Sent: ${email.sent_at}`);
|
||||
});
|
||||
} else {
|
||||
logger.info('No successfully sent emails found');
|
||||
}
|
||||
|
||||
logger.info('\n\n=== Diagnostic check complete ===');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error running diagnostic check:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the check
|
||||
checkEmailProcessor();
|
||||
@@ -1,66 +0,0 @@
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function checkEmailTemplates() {
|
||||
try {
|
||||
console.log('=== Email Templates Check ===\n');
|
||||
|
||||
// 1. Check table columns
|
||||
console.log('1. Checking email_templates table structure...');
|
||||
|
||||
// Check which columns exist
|
||||
const columnChecks = [
|
||||
'subject', 'subject_en', 'subject_de',
|
||||
'body_html', 'body_html_en', 'body_html_de',
|
||||
'body_text', 'body_text_en', 'body_text_de'
|
||||
];
|
||||
|
||||
const existingColumns = [];
|
||||
for (const col of columnChecks) {
|
||||
const exists = await db.schema.hasColumn('email_templates', col);
|
||||
if (exists) existingColumns.push(col);
|
||||
}
|
||||
|
||||
console.log(' Existing columns:', existingColumns.join(', '));
|
||||
|
||||
// 2. Get all templates
|
||||
console.log('\n2. Current email templates:');
|
||||
const templates = await db('email_templates').select('*');
|
||||
|
||||
for (const template of templates) {
|
||||
console.log(`\n Template: ${template.template_key}`);
|
||||
console.log(' -------------------');
|
||||
|
||||
// Check which fields have content
|
||||
const fields = ['subject', 'subject_en', 'subject_de',
|
||||
'body_html', 'body_html_en', 'body_html_de',
|
||||
'body_text', 'body_text_en', 'body_text_de'];
|
||||
|
||||
for (const field of fields) {
|
||||
if (template[field]) {
|
||||
const preview = template[field].substring(0, 50) + '...';
|
||||
console.log(` ${field}: ${preview}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for German translations
|
||||
const hasGermanSubject = template.subject_de || template.body_html_de;
|
||||
console.log(` Has German translation: ${hasGermanSubject ? 'YES' : 'NO'}`);
|
||||
}
|
||||
|
||||
// 3. Summary
|
||||
console.log('\n3. Summary:');
|
||||
const totalTemplates = templates.length;
|
||||
const templatesWithGerman = templates.filter(t => t.subject_de || t.body_html_de).length;
|
||||
console.log(` Total templates: ${totalTemplates}`);
|
||||
console.log(` Templates with German: ${templatesWithGerman}`);
|
||||
console.log(` Missing German: ${totalTemplates - templatesWithGerman}`);
|
||||
|
||||
await db.destroy();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkEmailTemplates();
|
||||
@@ -1,53 +0,0 @@
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function checkGermanTemplates() {
|
||||
try {
|
||||
console.log('=== German Email Template Content Check ===\n');
|
||||
|
||||
const templates = await db('email_templates').select('*');
|
||||
|
||||
for (const template of templates) {
|
||||
console.log(`\nTemplate: ${template.template_key}`);
|
||||
console.log('=====================================');
|
||||
|
||||
// Check German subject
|
||||
console.log('\nGERMAN SUBJECT:');
|
||||
console.log(template.subject_de || 'MISSING');
|
||||
|
||||
// Check if German HTML body has English content
|
||||
console.log('\nGERMAN HTML BODY:');
|
||||
const germanHtml = template.body_html_de || '';
|
||||
|
||||
// Check for English phrases in German template
|
||||
const englishPhrases = [
|
||||
'Dear', 'Gallery', 'has been', 'Your photo', 'successfully',
|
||||
'Details:', 'Link:', 'Password:', 'Expires:', 'Event Date:',
|
||||
'Thank you', 'Best regards', 'View Gallery', 'days'
|
||||
];
|
||||
|
||||
const foundEnglish = englishPhrases.filter(phrase =>
|
||||
germanHtml.toLowerCase().includes(phrase.toLowerCase())
|
||||
);
|
||||
|
||||
if (foundEnglish.length > 0) {
|
||||
console.log('⚠️ Found English phrases in German template:', foundEnglish.join(', '));
|
||||
}
|
||||
|
||||
// Show first 500 chars of German HTML
|
||||
console.log(germanHtml.substring(0, 500) + '...\n');
|
||||
|
||||
// Check German text body
|
||||
console.log('GERMAN TEXT BODY:');
|
||||
const germanText = template.body_text_de || '';
|
||||
console.log(germanText.substring(0, 300) + '...\n');
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkGermanTemplates();
|
||||
@@ -1,58 +0,0 @@
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
const { db } = require('../src/database/db');
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
async function createTestEvent() {
|
||||
try {
|
||||
console.log('Creating test event...');
|
||||
|
||||
// Hash a simple password
|
||||
const passwordHash = await bcrypt.hash('test123', 10);
|
||||
|
||||
// Generate share token
|
||||
const shareToken = uuidv4().replace(/-/g, '');
|
||||
const shareLink = `http://localhost:3005/gallery/wedding-test123-2025-07-07/${shareToken}`;
|
||||
|
||||
// Create event
|
||||
const eventData = {
|
||||
slug: 'wedding-test123-2025-07-07',
|
||||
event_type: 'wedding',
|
||||
event_name: 'Test Wedding',
|
||||
event_date: '2025-07-07',
|
||||
host_email: 'host@example.com',
|
||||
admin_email: 'admin@example.com',
|
||||
password_hash: passwordHash,
|
||||
welcome_message: 'Welcome to our test wedding gallery!',
|
||||
color_theme: null, // Use global theme
|
||||
is_active: 1,
|
||||
expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
share_link: shareLink
|
||||
};
|
||||
|
||||
// Delete existing event if it exists
|
||||
await db('events').where('slug', eventData.slug).delete();
|
||||
|
||||
// Insert new event
|
||||
const insertResult = await db('events').insert(eventData).returning('id');
|
||||
const eventId = insertResult[0]?.id || insertResult[0];
|
||||
console.log('Event created with ID:', eventId);
|
||||
|
||||
console.log('\nTest event created successfully!');
|
||||
console.log('Event details:');
|
||||
console.log('- Name:', eventData.event_name);
|
||||
console.log('- Slug:', eventData.slug);
|
||||
console.log('- Password:', 'test123');
|
||||
console.log('- Share link:', shareLink);
|
||||
console.log('\nYou can now access the gallery at the share link above');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error creating test event:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
createTestEvent();
|
||||
@@ -1,92 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function debugEndpoints() {
|
||||
console.log('Debugging 500 errors...\n');
|
||||
|
||||
try {
|
||||
// Test email templates query
|
||||
console.log('1. Testing email templates query:');
|
||||
try {
|
||||
const templates = await db('email_templates')
|
||||
.select('*')
|
||||
.orderBy('template_key');
|
||||
|
||||
console.log(`Found ${templates.length} templates`);
|
||||
if (templates.length > 0) {
|
||||
console.log('First template columns:', Object.keys(templates[0]));
|
||||
console.log('Template keys:', templates.map(t => t.template_key));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Email templates query failed:', error.message);
|
||||
console.error('Error code:', error.code);
|
||||
}
|
||||
|
||||
// Test notifications query
|
||||
console.log('\n2. Testing notifications query:');
|
||||
try {
|
||||
const notifications = await db('activity_logs')
|
||||
.select(
|
||||
'activity_logs.*',
|
||||
'events.event_name'
|
||||
)
|
||||
.leftJoin('events', 'activity_logs.event_id', 'events.id')
|
||||
.whereNull('activity_logs.read_at')
|
||||
.orderBy('activity_logs.created_at', 'desc')
|
||||
.limit(5);
|
||||
|
||||
console.log(`Found ${notifications.length} unread notifications`);
|
||||
} catch (error) {
|
||||
console.error('Notifications query failed:', error.message);
|
||||
console.error('Error code:', error.code);
|
||||
|
||||
// Check if it's a column issue
|
||||
if (error.message.includes('column')) {
|
||||
console.log('\nChecking activity_logs columns:');
|
||||
const columns = await db('activity_logs').columnInfo();
|
||||
console.log('Columns:', Object.keys(columns));
|
||||
}
|
||||
}
|
||||
|
||||
// Test specific template query
|
||||
console.log('\n3. Testing specific template query (gallery_created):');
|
||||
try {
|
||||
const template = await db('email_templates')
|
||||
.where('template_key', 'gallery_created')
|
||||
.first();
|
||||
|
||||
if (template) {
|
||||
console.log('Template found:', template.template_key);
|
||||
console.log('Has subject_en?', template.subject_en !== undefined);
|
||||
console.log('Has subject?', template.subject !== undefined);
|
||||
} else {
|
||||
console.log('Template not found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Template query failed:', error.message);
|
||||
}
|
||||
|
||||
// Check CMS pages
|
||||
console.log('\n4. Checking CMS pages:');
|
||||
try {
|
||||
const pages = await db('cms_pages')
|
||||
.select('slug', 'title', 'is_published')
|
||||
.orderBy('slug');
|
||||
|
||||
console.log(`Found ${pages.length} CMS pages:`);
|
||||
pages.forEach(page => {
|
||||
console.log(` - ${page.slug}: ${page.title} (published: ${page.is_published})`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('CMS pages query failed:', error.message);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('General error:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
debugEndpoints();
|
||||
@@ -1,146 +0,0 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const winston = require('winston');
|
||||
|
||||
// Create a simple console logger
|
||||
const logger = winston.createLogger({
|
||||
format: winston.format.simple(),
|
||||
transports: [new winston.transports.Console()]
|
||||
});
|
||||
|
||||
async function debugEmailQueue() {
|
||||
try {
|
||||
logger.info('=== Email Queue Debug Report ===\n');
|
||||
|
||||
// 1. Count exactly like the admin dashboard does
|
||||
logger.info('1. Admin Dashboard Query (ALL pending, no retry filter):');
|
||||
const [adminCount] = await db('email_queue').where('status', 'pending').count('* as count');
|
||||
logger.info(` Pending emails (admin dashboard view): ${adminCount.count}\n`);
|
||||
|
||||
// 2. Count like the email processor does
|
||||
logger.info('2. Email Processor Query (pending with retry_count < 3):');
|
||||
const [processorCount] = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.count('* as count');
|
||||
logger.info(` Pending emails (processor view): ${processorCount.count}\n`);
|
||||
|
||||
// 3. Show the discrepancy
|
||||
logger.info('3. Discrepancy Analysis:');
|
||||
if (adminCount.count !== processorCount.count) {
|
||||
logger.info(` ⚠️ DISCREPANCY FOUND!`);
|
||||
logger.info(` Admin shows: ${adminCount.count}`);
|
||||
logger.info(` Processor will process: ${processorCount.count}`);
|
||||
logger.info(` Difference: ${adminCount.count - processorCount.count} email(s)\n`);
|
||||
|
||||
// Find the problematic emails
|
||||
logger.info('4. Emails with retry_count >= 3 (still pending):');
|
||||
const stuckEmails = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '>=', 3)
|
||||
.select('*');
|
||||
|
||||
if (stuckEmails.length > 0) {
|
||||
logger.info(` Found ${stuckEmails.length} stuck email(s):\n`);
|
||||
stuckEmails.forEach((email, index) => {
|
||||
logger.info(` Email ${index + 1}:`);
|
||||
logger.info(` ID: ${email.id}`);
|
||||
logger.info(` Type: ${email.email_type}`);
|
||||
logger.info(` Recipient: ${email.recipient_email}`);
|
||||
logger.info(` Status: ${email.status}`);
|
||||
logger.info(` Retry Count: ${email.retry_count} ⚠️`);
|
||||
logger.info(` Created: ${email.created_at}`);
|
||||
logger.info(` Last Error: ${email.error_message || 'None'}\n`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
logger.info(` ✅ No discrepancy - counts match\n`);
|
||||
}
|
||||
|
||||
// 5. Show ALL pending emails with details
|
||||
logger.info('5. ALL Pending Emails (regardless of retry count):');
|
||||
const allPending = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.orderBy('retry_count', 'desc')
|
||||
.orderBy('created_at', 'asc');
|
||||
|
||||
if (allPending.length > 0) {
|
||||
allPending.forEach((email, index) => {
|
||||
const willProcess = email.retry_count < 3;
|
||||
logger.info(`\n Email ${index + 1}: ${willProcess ? '✅ WILL PROCESS' : '❌ STUCK (max retries)'}`);
|
||||
logger.info(` ID: ${email.id}`);
|
||||
logger.info(` Type: ${email.email_type}`);
|
||||
logger.info(` Recipient: ${email.recipient_email}`);
|
||||
logger.info(` Event ID: ${email.event_id}`);
|
||||
logger.info(` Retry Count: ${email.retry_count}/3`);
|
||||
logger.info(` Created: ${email.created_at}`);
|
||||
logger.info(` Scheduled: ${email.scheduled_at}`);
|
||||
if (email.error_message) {
|
||||
logger.info(` Last Error: ${email.error_message}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.info(' No pending emails found');
|
||||
}
|
||||
|
||||
// 6. Show counts by status
|
||||
logger.info('\n\n6. Email Queue Summary by Status:');
|
||||
const statusCounts = await db('email_queue')
|
||||
.select('status')
|
||||
.count('* as count')
|
||||
.groupBy('status')
|
||||
.orderBy('status');
|
||||
|
||||
statusCounts.forEach(row => {
|
||||
logger.info(` ${row.status}: ${row.count}`);
|
||||
});
|
||||
|
||||
// 7. Failed emails summary
|
||||
logger.info('\n7. Failed Emails Summary:');
|
||||
const failedSummary = await db('email_queue')
|
||||
.where('status', 'failed')
|
||||
.select('retry_count')
|
||||
.count('* as count')
|
||||
.groupBy('retry_count')
|
||||
.orderBy('retry_count');
|
||||
|
||||
if (failedSummary.length > 0) {
|
||||
failedSummary.forEach(row => {
|
||||
logger.info(` Retry count ${row.retry_count}: ${row.count} email(s)`);
|
||||
});
|
||||
} else {
|
||||
logger.info(' No failed emails');
|
||||
}
|
||||
|
||||
// 8. Recommendations
|
||||
logger.info('\n\n=== RECOMMENDATIONS ===');
|
||||
|
||||
if (adminCount.count > processorCount.count) {
|
||||
logger.info('\n❗ You have emails stuck with retry_count >= 3');
|
||||
logger.info(' These emails will NOT be processed automatically.');
|
||||
logger.info('\n To fix this, you can:');
|
||||
logger.info(' 1. Reset retry count: UPDATE email_queue SET retry_count = 0 WHERE status = \'pending\' AND retry_count >= 3;');
|
||||
logger.info(' 2. Mark as failed: UPDATE email_queue SET status = \'failed\' WHERE status = \'pending\' AND retry_count >= 3;');
|
||||
logger.info(' 3. Delete them: DELETE FROM email_queue WHERE status = \'pending\' AND retry_count >= 3;');
|
||||
}
|
||||
|
||||
const anyPending = adminCount.count > 0;
|
||||
if (anyPending && processorCount.count === 0) {
|
||||
logger.info('\n❗ All pending emails have exceeded retry limit');
|
||||
logger.info(' The email processor will not attempt to send them.');
|
||||
} else if (anyPending && processorCount.count > 0) {
|
||||
logger.info('\n✅ Email processor should process the pending emails on next run');
|
||||
logger.info(' Make sure the email processor service is running.');
|
||||
}
|
||||
|
||||
logger.info('\n=== Debug report complete ===');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error running debug report:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the debug
|
||||
debugEmailQueue();
|
||||
@@ -1,132 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script to diagnose thumbnail serving issues
|
||||
* Usage: node scripts/diagnose-thumbnails.js <eventId>
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
const STORAGE_PATH = process.env.STORAGE_PATH || path.join(__dirname, '../../storage');
|
||||
const THUMBNAILS_DIR = path.join(STORAGE_PATH, 'thumbnails');
|
||||
|
||||
async function diagnoseThumbnails(eventId) {
|
||||
if (!eventId) {
|
||||
console.error('Usage: node scripts/diagnose-thumbnails.js <eventId>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Diagnosing thumbnails for event ID: ${eventId}`);
|
||||
console.log(`Storage path: ${STORAGE_PATH}`);
|
||||
console.log(`Thumbnails directory: ${THUMBNAILS_DIR}\n`);
|
||||
|
||||
try {
|
||||
// Get event info
|
||||
const event = await db('events').where('id', eventId).first();
|
||||
if (!event) {
|
||||
console.error(`Event not found with ID: ${eventId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Event: ${event.event_name} (${event.slug})`);
|
||||
console.log(`Active: ${event.is_active}, Archived: ${event.is_archived}\n`);
|
||||
|
||||
// Get photos for this event
|
||||
const photos = await db('photos')
|
||||
.where('event_id', eventId)
|
||||
.select('id', 'filename', 'path', 'thumbnail_path');
|
||||
|
||||
console.log(`Found ${photos.length} photos in database\n`);
|
||||
|
||||
let missingThumbnails = 0;
|
||||
let existingThumbnails = 0;
|
||||
let pathIssues = [];
|
||||
|
||||
for (const photo of photos.slice(0, 10)) { // Check first 10 photos
|
||||
console.log(`Photo ID ${photo.id}: ${photo.filename}`);
|
||||
console.log(` Photo path: ${photo.path}`);
|
||||
console.log(` Thumbnail path in DB: ${photo.thumbnail_path}`);
|
||||
|
||||
if (photo.thumbnail_path) {
|
||||
// Expected thumbnail filename
|
||||
const expectedThumbName = `thumb_${photo.filename}`;
|
||||
const expectedThumbPath = path.join(THUMBNAILS_DIR, expectedThumbName);
|
||||
|
||||
// Check if thumbnail exists
|
||||
try {
|
||||
await fs.access(expectedThumbPath);
|
||||
console.log(` ✓ Thumbnail exists at: ${expectedThumbName}`);
|
||||
existingThumbnails++;
|
||||
|
||||
// Check if DB path matches expected path
|
||||
const dbThumbName = path.basename(photo.thumbnail_path);
|
||||
if (dbThumbName !== expectedThumbName) {
|
||||
console.log(` ⚠ Path mismatch! DB has: ${dbThumbName}, Expected: ${expectedThumbName}`);
|
||||
pathIssues.push({
|
||||
photoId: photo.id,
|
||||
dbPath: photo.thumbnail_path,
|
||||
expectedPath: `thumbnails/${expectedThumbName}`
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
console.log(` ✗ Thumbnail missing: ${expectedThumbName}`);
|
||||
missingThumbnails++;
|
||||
}
|
||||
} else {
|
||||
console.log(` ✗ No thumbnail path in database`);
|
||||
missingThumbnails++;
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('--- Summary ---');
|
||||
console.log(`Existing thumbnails: ${existingThumbnails}`);
|
||||
console.log(`Missing thumbnails: ${missingThumbnails}`);
|
||||
console.log(`Path issues: ${pathIssues.length}`);
|
||||
|
||||
if (pathIssues.length > 0) {
|
||||
console.log('\n--- Path Issues ---');
|
||||
console.log('The following photos have incorrect thumbnail paths in the database:');
|
||||
for (const issue of pathIssues) {
|
||||
console.log(`Photo ID ${issue.photoId}:`);
|
||||
console.log(` Current: ${issue.dbPath}`);
|
||||
console.log(` Should be: ${issue.expectedPath}`);
|
||||
}
|
||||
|
||||
console.log('\nTo fix path issues, run:');
|
||||
console.log(`UPDATE photos SET thumbnail_path = 'thumbnails/thumb_' || filename WHERE event_id = ${eventId};`);
|
||||
}
|
||||
|
||||
// Check for any thumbnails in the directory that match this event
|
||||
const files = await fs.readdir(THUMBNAILS_DIR);
|
||||
const eventThumbnails = files.filter(f => {
|
||||
// Try to match thumbnails for this event
|
||||
for (const photo of photos) {
|
||||
if (f === `thumb_${photo.filename}`) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
console.log(`\n--- Filesystem Check ---`);
|
||||
console.log(`Found ${eventThumbnails.length} thumbnails in directory for this event`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during diagnosis:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse command line arguments
|
||||
const eventId = process.argv[2] ? parseInt(process.argv[2]) : null;
|
||||
|
||||
// Run the diagnosis
|
||||
diagnoseThumbnails(eventId).then(async () => {
|
||||
await db.destroy();
|
||||
console.log('\nDiagnosis complete');
|
||||
}).catch(async error => {
|
||||
console.error('Diagnosis failed:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script to diagnose and fix email_queue schema issues
|
||||
* This helps resolve the "column updated_at does not exist" error
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function checkAndFixEmailQueueSchema() {
|
||||
console.log('Checking email_queue table schema...');
|
||||
|
||||
try {
|
||||
// Get column information
|
||||
const columns = await db('email_queue').columnInfo();
|
||||
console.log('\nCurrent email_queue columns:', Object.keys(columns));
|
||||
|
||||
// Check for updated_at column
|
||||
if (columns.updated_at) {
|
||||
console.log('\n⚠️ Found unexpected updated_at column in email_queue table!');
|
||||
console.log('This column should not exist and is causing errors.');
|
||||
|
||||
// Ask for confirmation before removing
|
||||
console.log('\nRemoving updated_at column...');
|
||||
await db.schema.table('email_queue', (table) => {
|
||||
table.dropColumn('updated_at');
|
||||
});
|
||||
console.log('✅ Removed updated_at column from email_queue table');
|
||||
} else {
|
||||
console.log('✅ No updated_at column found (this is correct)');
|
||||
}
|
||||
|
||||
// Verify required columns exist
|
||||
const requiredColumns = [
|
||||
'id', 'event_id', 'recipient_email', 'email_type',
|
||||
'email_data', 'status', 'scheduled_at', 'sent_at',
|
||||
'error_message', 'retry_count', 'created_at'
|
||||
];
|
||||
|
||||
const missingColumns = requiredColumns.filter(col => !columns[col]);
|
||||
if (missingColumns.length > 0) {
|
||||
console.log('\n⚠️ Missing required columns:', missingColumns);
|
||||
} else {
|
||||
console.log('✅ All required columns are present');
|
||||
}
|
||||
|
||||
// Check for any database triggers
|
||||
if (process.env.DATABASE_CLIENT === 'pg') {
|
||||
console.log('\nChecking for PostgreSQL triggers on email_queue...');
|
||||
const triggers = await db.raw(`
|
||||
SELECT trigger_name, event_manipulation, action_statement
|
||||
FROM information_schema.triggers
|
||||
WHERE event_object_table = 'email_queue'
|
||||
AND trigger_schema = current_schema()
|
||||
`);
|
||||
|
||||
if (triggers.rows && triggers.rows.length > 0) {
|
||||
console.log('⚠️ Found triggers on email_queue table:');
|
||||
triggers.rows.forEach(trigger => {
|
||||
console.log(` - ${trigger.trigger_name} (${trigger.event_manipulation})`);
|
||||
});
|
||||
} else {
|
||||
console.log('✅ No triggers found on email_queue table');
|
||||
}
|
||||
}
|
||||
|
||||
// Test update query
|
||||
console.log('\nTesting update query...');
|
||||
const testEmail = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.first();
|
||||
|
||||
if (testEmail) {
|
||||
try {
|
||||
await db('email_queue')
|
||||
.where('id', testEmail.id)
|
||||
.update({
|
||||
retry_count: testEmail.retry_count
|
||||
});
|
||||
console.log('✅ Update query works correctly');
|
||||
} catch (error) {
|
||||
console.log('❌ Update query failed:', error.message);
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ No pending emails to test with');
|
||||
}
|
||||
|
||||
console.log('\nSchema check complete!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error checking schema:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the check
|
||||
checkAndFixEmailQueueSchema();
|
||||
@@ -1,88 +0,0 @@
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function fixFinalGermanTemplates() {
|
||||
try {
|
||||
console.log('Fixing remaining English words in German templates...\n');
|
||||
|
||||
// Get all templates
|
||||
const templates = await db('email_templates').select('*');
|
||||
|
||||
for (const template of templates) {
|
||||
let updated = false;
|
||||
let updates = {};
|
||||
|
||||
// Fix subject_de
|
||||
if (template.subject_de) {
|
||||
updates.subject_de = template.subject_de;
|
||||
}
|
||||
|
||||
// Fix body_html_de
|
||||
if (template.body_html_de) {
|
||||
let html = template.body_html_de;
|
||||
|
||||
// Replace English words with German
|
||||
html = html.replace(/Gallery-Details:/g, 'Galerie-Details:');
|
||||
html = html.replace(/Galerie-Details:/g, 'Galerie-Details:');
|
||||
html = html.replace(/Details:/g, 'Details:');
|
||||
html = html.replace(/Link:/g, 'Link:');
|
||||
html = html.replace(/Gallery-Link:/g, 'Galerie-Link:');
|
||||
html = html.replace(/Galerie-Link:/g, 'Galerie-Link:');
|
||||
html = html.replace(/Archive-Details:/g, 'Archiv-Details:');
|
||||
html = html.replace(/Archiv-Details:/g, 'Archiv-Details:');
|
||||
|
||||
if (html !== template.body_html_de) {
|
||||
updates.body_html_de = html;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix body_text_de
|
||||
if (template.body_text_de) {
|
||||
let text = template.body_text_de;
|
||||
|
||||
text = text.replace(/Gallery-Details:/g, 'Galerie-Details:');
|
||||
text = text.replace(/Galerie-Details:/g, 'Galerie-Details:');
|
||||
text = text.replace(/Details:/g, 'Details:');
|
||||
text = text.replace(/Link:/g, 'Link:');
|
||||
text = text.replace(/Gallery-Link:/g, 'Galerie-Link:');
|
||||
text = text.replace(/Galerie-Link:/g, 'Galerie-Link:');
|
||||
text = text.replace(/Archive-Details:/g, 'Archiv-Details:');
|
||||
text = text.replace(/Archiv-Details:/g, 'Archiv-Details:');
|
||||
|
||||
if (text !== template.body_text_de) {
|
||||
updates.body_text_de = text;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also update the non-language-specific fields to match German
|
||||
if (template.body_html_de) {
|
||||
updates.body_html = template.body_html_de;
|
||||
}
|
||||
if (template.body_text_de) {
|
||||
updates.body_text = template.body_text_de;
|
||||
}
|
||||
if (template.subject_de) {
|
||||
updates.subject = template.subject_de;
|
||||
}
|
||||
|
||||
if (updated || Object.keys(updates).length > 0) {
|
||||
await db('email_templates')
|
||||
.where('template_key', template.template_key)
|
||||
.update(updates);
|
||||
console.log(`✅ Updated ${template.template_key}`);
|
||||
} else {
|
||||
console.log(`⏭️ No changes needed for ${template.template_key}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nDone!');
|
||||
await db.destroy();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fixFinalGermanTemplates();
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Fix migration state by marking migrations as applied if their tables already exist
|
||||
*/
|
||||
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function fixMigrationState() {
|
||||
try {
|
||||
console.log('Checking migration state...');
|
||||
|
||||
// Ensure migrations table exists
|
||||
const hasMigrationsTable = await db.schema.hasTable('migrations');
|
||||
if (!hasMigrationsTable) {
|
||||
await db.schema.createTable('migrations', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('filename').unique().notNullable();
|
||||
table.timestamp('applied_at').defaultTo(db.fn.now());
|
||||
});
|
||||
console.log('Created migrations tracking table');
|
||||
}
|
||||
|
||||
// Check for specific tables and mark their migrations as applied
|
||||
const tableChecks = [
|
||||
{ table: 'restore_runs', migration: '032_add_restore_runs_table.js' },
|
||||
{ table: 'restore_file_operations', migration: '032_add_restore_runs_table.js' },
|
||||
{ table: 'restore_validation_results', migration: '032_add_restore_runs_table.js' },
|
||||
{ table: 'gallery_feedback', migration: '033_add_gallery_feedback.js' },
|
||||
{ table: 'feedback_photos', migration: '033_add_gallery_feedback.js' },
|
||||
];
|
||||
|
||||
for (const check of tableChecks) {
|
||||
const tableExists = await db.schema.hasTable(check.table);
|
||||
if (tableExists) {
|
||||
const migrationApplied = await db('migrations')
|
||||
.where('filename', check.migration)
|
||||
.first();
|
||||
|
||||
if (!migrationApplied) {
|
||||
await db('migrations').insert({
|
||||
filename: check.migration,
|
||||
applied_at: new Date()
|
||||
});
|
||||
console.log(`✅ Marked ${check.migration} as applied (table ${check.table} exists)`);
|
||||
} else {
|
||||
console.log(`ℹ️ ${check.migration} already marked as applied`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nMigration state fixed successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error fixing migration state:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
fixMigrationState();
|
||||
@@ -1,145 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function fixProductionIssues() {
|
||||
console.log('Fixing production database issues...\n');
|
||||
|
||||
try {
|
||||
// 1. Check and fix email_templates structure
|
||||
console.log('1. Checking email_templates structure:');
|
||||
const emailColumns = await db('email_templates').columnInfo();
|
||||
console.log('Current columns:', Object.keys(emailColumns));
|
||||
|
||||
// Check if we need to add basic columns back
|
||||
const hasSubject = 'subject' in emailColumns;
|
||||
const hasSubjectEn = 'subject_en' in emailColumns;
|
||||
|
||||
if (hasSubjectEn && !hasSubject) {
|
||||
console.log('Adding basic columns back to email_templates...');
|
||||
await db.schema.alterTable('email_templates', (table) => {
|
||||
table.string('subject');
|
||||
table.text('body_html');
|
||||
table.text('body_text');
|
||||
});
|
||||
|
||||
// Copy values from _en columns
|
||||
await db('email_templates').update({
|
||||
subject: db.raw('subject_en'),
|
||||
body_html: db.raw('body_html_en'),
|
||||
body_text: db.raw('body_text_en')
|
||||
});
|
||||
console.log('Basic columns added successfully');
|
||||
}
|
||||
|
||||
// 2. Ensure default templates exist
|
||||
console.log('\n2. Checking email templates:');
|
||||
const templateCount = await db('email_templates').count('* as count');
|
||||
console.log('Template count:', templateCount[0].count);
|
||||
|
||||
if (templateCount[0].count === 0) {
|
||||
console.log('No templates found, inserting defaults...');
|
||||
const defaultTemplates = [
|
||||
{
|
||||
template_key: 'gallery_created',
|
||||
subject: 'Your Photo Gallery is Ready!',
|
||||
body_html: '<h2>Gallery Created Successfully</h2>...',
|
||||
body_text: 'Gallery Created Successfully...',
|
||||
variables: JSON.stringify(['host_name', 'event_name', 'event_date', 'gallery_link', 'gallery_password', 'expiry_date'])
|
||||
},
|
||||
{
|
||||
template_key: 'expiration_warning',
|
||||
subject: 'Your Photo Gallery Expires Soon',
|
||||
body_html: '<h2>Gallery Expiring Soon</h2>...',
|
||||
body_text: 'Gallery Expiring Soon...',
|
||||
variables: JSON.stringify(['host_name', 'event_name', 'days_remaining', 'gallery_link'])
|
||||
},
|
||||
{
|
||||
template_key: 'gallery_expired',
|
||||
subject: 'Your Photo Gallery Has Expired',
|
||||
body_html: '<h2>Gallery Expired</h2>...',
|
||||
body_text: 'Gallery Expired...',
|
||||
variables: JSON.stringify(['host_name', 'event_name'])
|
||||
},
|
||||
{
|
||||
template_key: 'archive_complete',
|
||||
subject: 'Gallery Archive Complete',
|
||||
body_html: '<h2>Archive Complete</h2>...',
|
||||
body_text: 'Archive Complete...',
|
||||
variables: JSON.stringify(['host_name', 'event_name', 'archive_size'])
|
||||
}
|
||||
];
|
||||
|
||||
for (const template of defaultTemplates) {
|
||||
// Add language columns if they exist
|
||||
if (hasSubjectEn) {
|
||||
template.subject_en = template.subject;
|
||||
template.body_html_en = template.body_html;
|
||||
template.body_text_en = template.body_text;
|
||||
template.subject_de = template.subject;
|
||||
template.body_html_de = template.body_html;
|
||||
template.body_text_de = template.body_text;
|
||||
}
|
||||
|
||||
await db('email_templates').insert(template);
|
||||
}
|
||||
console.log('Default templates inserted');
|
||||
}
|
||||
|
||||
// 3. Check activity_logs structure
|
||||
console.log('\n3. Checking activity_logs structure:');
|
||||
const activityColumns = await db('activity_logs').columnInfo();
|
||||
console.log('Columns:', Object.keys(activityColumns));
|
||||
|
||||
// Check if read_at exists
|
||||
if (!('read_at' in activityColumns)) {
|
||||
console.log('Adding read_at column to activity_logs...');
|
||||
await db.schema.alterTable('activity_logs', (table) => {
|
||||
table.datetime('read_at').nullable();
|
||||
});
|
||||
console.log('read_at column added');
|
||||
}
|
||||
|
||||
// 4. Check and add CMS pages
|
||||
console.log('\n4. Checking CMS pages:');
|
||||
const cmsColumns = await db('cms_pages').columnInfo();
|
||||
console.log('CMS columns:', Object.keys(cmsColumns));
|
||||
|
||||
const impressum = await db('cms_pages').where('slug', 'impressum').first();
|
||||
const datenschutz = await db('cms_pages').where('slug', 'datenschutz').first();
|
||||
|
||||
if (!impressum) {
|
||||
console.log('Adding Impressum page...');
|
||||
await db('cms_pages').insert({
|
||||
slug: 'impressum',
|
||||
title_en: 'Legal Notice',
|
||||
title_de: 'Impressum',
|
||||
content_en: '<h1>Legal Notice</h1><p>Your legal information here...</p>',
|
||||
content_de: '<h1>Impressum</h1><p>Ihre rechtlichen Informationen hier...</p>',
|
||||
updated_at: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
if (!datenschutz) {
|
||||
console.log('Adding Datenschutz page...');
|
||||
await db('cms_pages').insert({
|
||||
slug: 'datenschutz',
|
||||
title_en: 'Privacy Policy',
|
||||
title_de: 'Datenschutzerklärung',
|
||||
content_en: '<h1>Privacy Policy</h1><p>Your privacy policy here...</p>',
|
||||
content_de: '<h1>Datenschutzerklärung</h1><p>Ihre Datenschutzerklärung hier...</p>',
|
||||
updated_at: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n✅ All fixes applied successfully!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fixing issues:', error);
|
||||
console.error('Stack:', error.stack);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
fixProductionIssues();
|
||||
@@ -1,126 +0,0 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const winston = require('winston');
|
||||
|
||||
// Create a simple console logger
|
||||
const logger = winston.createLogger({
|
||||
format: winston.format.simple(),
|
||||
transports: [new winston.transports.Console()]
|
||||
});
|
||||
|
||||
async function fixStuckEmails() {
|
||||
try {
|
||||
logger.info('=== Fix Stuck Emails Script ===\n');
|
||||
|
||||
// 1. Find stuck emails
|
||||
logger.info('1. Finding stuck emails (pending with retry_count >= 3)...');
|
||||
const stuckEmails = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '>=', 3)
|
||||
.select('*');
|
||||
|
||||
if (stuckEmails.length === 0) {
|
||||
logger.info(' ✅ No stuck emails found!');
|
||||
logger.info('\n=== Script complete ===');
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
logger.info(` Found ${stuckEmails.length} stuck email(s)\n`);
|
||||
|
||||
// 2. Show details
|
||||
logger.info('2. Stuck email details:');
|
||||
stuckEmails.forEach((email, index) => {
|
||||
logger.info(`\n Email ${index + 1}:`);
|
||||
logger.info(` ID: ${email.id}`);
|
||||
logger.info(` Type: ${email.email_type}`);
|
||||
logger.info(` Recipient: ${email.recipient_email}`);
|
||||
logger.info(` Retry Count: ${email.retry_count}`);
|
||||
logger.info(` Last Error: ${email.error_message || 'None'}`);
|
||||
});
|
||||
|
||||
// 3. Ask for action
|
||||
logger.info('\n\n3. Choose an action:');
|
||||
logger.info(' 1. Reset retry count to 0 (emails will be retried)');
|
||||
logger.info(' 2. Mark as failed (emails will not be retried)');
|
||||
logger.info(' 3. Delete these emails');
|
||||
logger.info(' 4. Cancel (do nothing)');
|
||||
|
||||
// Get command line argument
|
||||
const action = process.argv[2];
|
||||
|
||||
if (!action || !['reset', 'fail', 'delete'].includes(action)) {
|
||||
logger.info('\n❗ No valid action specified');
|
||||
logger.info('\nUsage:');
|
||||
logger.info(' node fix-stuck-emails.js reset - Reset retry count to 0');
|
||||
logger.info(' node fix-stuck-emails.js fail - Mark as failed');
|
||||
logger.info(' node fix-stuck-emails.js delete - Delete stuck emails');
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 4. Execute action
|
||||
logger.info(`\n4. Executing action: ${action.toUpperCase()}`);
|
||||
|
||||
const emailIds = stuckEmails.map(e => e.id);
|
||||
|
||||
switch (action) {
|
||||
case 'reset':
|
||||
await db('email_queue')
|
||||
.whereIn('id', emailIds)
|
||||
.update({
|
||||
retry_count: 0,
|
||||
error_message: null
|
||||
});
|
||||
logger.info(` ✅ Reset retry count for ${emailIds.length} email(s)`);
|
||||
logger.info(' These emails will be processed on the next run');
|
||||
break;
|
||||
|
||||
case 'fail':
|
||||
await db('email_queue')
|
||||
.whereIn('id', emailIds)
|
||||
.update({
|
||||
status: 'failed'
|
||||
});
|
||||
logger.info(` ✅ Marked ${emailIds.length} email(s) as failed`);
|
||||
logger.info(' These emails will not be retried');
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
await db('email_queue')
|
||||
.whereIn('id', emailIds)
|
||||
.delete();
|
||||
logger.info(` ✅ Deleted ${emailIds.length} email(s)`);
|
||||
break;
|
||||
}
|
||||
|
||||
// 5. Show updated counts
|
||||
logger.info('\n5. Updated email queue status:');
|
||||
const [pendingCount] = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.count('* as count');
|
||||
const [processableCount] = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.count('* as count');
|
||||
|
||||
logger.info(` Total pending: ${pendingCount.count}`);
|
||||
logger.info(` Processable (retry < 3): ${processableCount.count}`);
|
||||
|
||||
if (pendingCount.count !== processableCount.count) {
|
||||
logger.info(` ⚠️ Still have ${pendingCount.count - processableCount.count} stuck email(s)`);
|
||||
} else {
|
||||
logger.info(' ✅ No stuck emails remaining');
|
||||
}
|
||||
|
||||
logger.info('\n=== Script complete ===');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the fix
|
||||
fixStuckEmails();
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Mark a specific migration as applied without running it
|
||||
* Usage: node scripts/mark-migration-applied.js <migration-filename>
|
||||
*/
|
||||
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function markMigrationAsApplied(filename) {
|
||||
try {
|
||||
// Check if migration is already marked
|
||||
const existing = await db('migrations')
|
||||
.where('filename', filename)
|
||||
.first();
|
||||
|
||||
if (existing) {
|
||||
console.log(`Migration ${filename} is already marked as applied`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as applied
|
||||
await db('migrations').insert({
|
||||
filename,
|
||||
applied_at: new Date()
|
||||
});
|
||||
|
||||
console.log(`✅ Migration ${filename} marked as applied`);
|
||||
} catch (error) {
|
||||
console.error('Error marking migration:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Get migration filename from command line
|
||||
const migrationFile = process.argv[2];
|
||||
|
||||
if (!migrationFile) {
|
||||
console.error('Usage: node scripts/mark-migration-applied.js <migration-filename>');
|
||||
console.error('Example: node scripts/mark-migration-applied.js 032_add_restore_runs_table.js');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
markMigrationAsApplied(migrationFile);
|
||||
@@ -1,111 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { db } = require('../src/database/db');
|
||||
const {
|
||||
initializeTransporter,
|
||||
processEmailQueue,
|
||||
testEmailConnection
|
||||
} = require('../src/services/emailProcessor');
|
||||
const winston = require('winston');
|
||||
|
||||
// Create a simple console logger
|
||||
const logger = winston.createLogger({
|
||||
format: winston.format.simple(),
|
||||
transports: [new winston.transports.Console()]
|
||||
});
|
||||
|
||||
async function runEmailProcessor(runOnce = false) {
|
||||
try {
|
||||
logger.info('=== Starting Email Processor ===\n');
|
||||
|
||||
// Initialize transporter
|
||||
logger.info('Initializing email transporter...');
|
||||
await initializeTransporter();
|
||||
|
||||
// Test connection
|
||||
logger.info('Testing email connection...');
|
||||
const connectionOk = await testEmailConnection();
|
||||
|
||||
if (!connectionOk) {
|
||||
logger.error('Email connection test failed! Check your SMTP configuration.');
|
||||
logger.info('\nRequired environment variables:');
|
||||
logger.info('- SMTP_HOST');
|
||||
logger.info('- SMTP_PORT');
|
||||
logger.info('- SMTP_USER');
|
||||
logger.info('- SMTP_PASS');
|
||||
logger.info('- SMTP_FROM');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.info('Email connection test successful!\n');
|
||||
|
||||
if (runOnce) {
|
||||
// Process queue once
|
||||
logger.info('Processing email queue once...');
|
||||
await processEmailQueue();
|
||||
logger.info('Email processing complete');
|
||||
|
||||
// Show final status
|
||||
const pendingCount = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.count('* as count')
|
||||
.first();
|
||||
|
||||
logger.info(`\nEmails still pending: ${pendingCount.count}`);
|
||||
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Run continuously
|
||||
logger.info('Starting continuous email processor...');
|
||||
logger.info('Processing emails every 60 seconds. Press Ctrl+C to stop.\n');
|
||||
|
||||
// Process immediately
|
||||
await processEmailQueue();
|
||||
|
||||
// Then every minute
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await processEmailQueue();
|
||||
} catch (error) {
|
||||
logger.error('Error processing email queue:', error);
|
||||
}
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Fatal error:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
logger.info('\n\nShutting down email processor...');
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Check command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const runOnce = args.includes('--once') || args.includes('-o');
|
||||
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log(`
|
||||
Email Processor Runner
|
||||
|
||||
Usage: node run-email-processor.js [options]
|
||||
|
||||
Options:
|
||||
--once, -o Process the email queue once and exit
|
||||
--help, -h Show this help message
|
||||
|
||||
By default, the processor runs continuously, checking for emails every 60 seconds.
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run the processor
|
||||
runEmailProcessor(runOnce);
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Run database migrations using existing db connection
|
||||
*/
|
||||
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function runMigrations() {
|
||||
console.log('Running database migrations...\n');
|
||||
|
||||
try {
|
||||
// Run all pending migrations
|
||||
const result = await db.migrate.latest({
|
||||
directory: './migrations'
|
||||
});
|
||||
|
||||
if (result[1].length === 0) {
|
||||
console.log('✓ Database is already up to date');
|
||||
} else {
|
||||
console.log(`✓ Ran ${result[1].length} migrations:`);
|
||||
result[1].forEach(migration => {
|
||||
console.log(` - ${migration}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Show current migration status
|
||||
const list = await db.migrate.list();
|
||||
console.log(`\nCurrent status: ${list[0].length} completed migrations`);
|
||||
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Migration error:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runMigrations();
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Show or reset admin credentials
|
||||
*/
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
const { db } = require('../src/database/db');
|
||||
const { generateReadablePassword } = require('../src/utils/passwordGenerator');
|
||||
|
||||
async function showAdminCredentials(resetPassword = false) {
|
||||
try {
|
||||
// Get admin user
|
||||
const admin = await db('admin_users')
|
||||
.where('username', 'admin')
|
||||
.first();
|
||||
|
||||
if (!admin) {
|
||||
console.error('❌ No admin user found in database');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\n========================================');
|
||||
console.log('PicPeak Admin Credentials');
|
||||
console.log('========================================');
|
||||
console.log(`Username: ${admin.username}`);
|
||||
console.log(`Email: ${admin.email}`);
|
||||
|
||||
if (resetPassword) {
|
||||
// Generate new password
|
||||
const newPassword = generateReadablePassword();
|
||||
const passwordHash = await bcrypt.hash(newPassword, 12);
|
||||
|
||||
// Update password
|
||||
await db('admin_users')
|
||||
.where('id', admin.id)
|
||||
.update({
|
||||
password_hash: passwordHash,
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
console.log(`Password: ${newPassword} (NEWLY RESET)`);
|
||||
console.log('\n⚠️ IMPORTANT: Please save this password securely!');
|
||||
} else {
|
||||
console.log('Password: [hidden - use --reset flag to generate new password]');
|
||||
}
|
||||
|
||||
console.log('\nLogin URL: http://localhost:3001/admin');
|
||||
console.log('========================================\n');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Check for reset flag
|
||||
const resetPassword = process.argv.includes('--reset');
|
||||
|
||||
if (resetPassword) {
|
||||
console.log('🔄 Resetting admin password...');
|
||||
}
|
||||
|
||||
showAdminCredentials(resetPassword);
|
||||
@@ -1,192 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test script for backup manifest generator
|
||||
* Demonstrates all features of the manifest generator
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const backupManifest = require('../src/services/backupManifest');
|
||||
const logger = require('../src/utils/logger');
|
||||
|
||||
async function testManifestGeneration() {
|
||||
console.log('=== Testing Backup Manifest Generator ===\n');
|
||||
|
||||
try {
|
||||
// 1. Generate a full backup manifest
|
||||
console.log('1. Generating full backup manifest...');
|
||||
|
||||
const fullManifestOptions = {
|
||||
backupType: 'full',
|
||||
backupPath: '/backup/full/2025-01-21',
|
||||
files: [
|
||||
{
|
||||
path: '/storage/events/active/wedding-smith-2025/DSC_001.jpg',
|
||||
relativePath: 'events/active/wedding-smith-2025/DSC_001.jpg',
|
||||
size: 2456789,
|
||||
modified: new Date('2025-01-20T10:30:00Z'),
|
||||
checksum: 'a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890',
|
||||
permissions: '644'
|
||||
},
|
||||
{
|
||||
path: '/storage/events/active/wedding-smith-2025/DSC_002.jpg',
|
||||
relativePath: 'events/active/wedding-smith-2025/DSC_002.jpg',
|
||||
size: 2156789,
|
||||
modified: new Date('2025-01-20T10:31:00Z'),
|
||||
checksum: 'b2c3d4e5f67890123456789012345678901234567890123456789012345678901',
|
||||
permissions: '644'
|
||||
},
|
||||
{
|
||||
path: '/storage/thumbnails/wedding-smith-2025/thumb_DSC_001.jpg',
|
||||
relativePath: 'thumbnails/wedding-smith-2025/thumb_DSC_001.jpg',
|
||||
size: 45678,
|
||||
modified: new Date('2025-01-20T10:35:00Z'),
|
||||
checksum: 'c3d4e5f678901234567890123456789012345678901234567890123456789012',
|
||||
permissions: '644'
|
||||
}
|
||||
],
|
||||
databaseInfo: {
|
||||
type: 'sqlite',
|
||||
backupFile: 'database-backup-20250121-103000.sql.gz',
|
||||
size: 1048576,
|
||||
checksum: 'd4e5f6789012345678901234567890123456789012345678901234567890123',
|
||||
tables: {
|
||||
events: 156,
|
||||
photos: 4523,
|
||||
access_logs: 12456,
|
||||
admin_users: 3
|
||||
},
|
||||
rowCounts: {
|
||||
events: 156,
|
||||
photos: 4523,
|
||||
access_logs: 12456,
|
||||
admin_users: 3
|
||||
}
|
||||
},
|
||||
format: 'json',
|
||||
customMetadata: {
|
||||
operator: 'admin@example.com',
|
||||
reason: 'Scheduled daily backup',
|
||||
retentionDays: 30,
|
||||
compressionType: 'gzip'
|
||||
}
|
||||
};
|
||||
|
||||
const fullManifest = await backupManifest.generateManifest(fullManifestOptions);
|
||||
|
||||
// Save in both formats
|
||||
const jsonPath = path.join(__dirname, 'test-manifest-full.json');
|
||||
const yamlPath = path.join(__dirname, 'test-manifest-full.yaml');
|
||||
|
||||
await backupManifest.saveManifest(fullManifest, jsonPath, 'json');
|
||||
await backupManifest.saveManifest(fullManifest, yamlPath, 'yaml');
|
||||
|
||||
console.log('✓ Full backup manifest generated and saved\n');
|
||||
|
||||
// 2. Generate summary report
|
||||
console.log('2. Generating summary report...');
|
||||
const summaryReport = backupManifest.generateSummaryReport(fullManifest);
|
||||
console.log(summaryReport);
|
||||
console.log('\n');
|
||||
|
||||
// 3. Load and validate manifest
|
||||
console.log('3. Loading and validating manifest...');
|
||||
const loadedManifest = await backupManifest.loadManifest(jsonPath);
|
||||
console.log('✓ Manifest loaded and validated successfully\n');
|
||||
|
||||
// 4. Generate incremental backup manifest
|
||||
console.log('4. Generating incremental backup manifest...');
|
||||
|
||||
const incrementalOptions = {
|
||||
backupType: 'incremental',
|
||||
backupPath: '/backup/incremental/2025-01-22',
|
||||
parentBackupId: fullManifest.backup.id,
|
||||
files: [
|
||||
// Original files with same checksums (unchanged)
|
||||
fullManifestOptions.files[0],
|
||||
fullManifestOptions.files[2],
|
||||
// Modified file
|
||||
{
|
||||
...fullManifestOptions.files[1],
|
||||
size: 2256789,
|
||||
modified: new Date('2025-01-21T14:00:00Z'),
|
||||
checksum: 'e5f678901234567890123456789012345678901234567890123456789012345'
|
||||
},
|
||||
// New file
|
||||
{
|
||||
path: '/storage/events/active/wedding-smith-2025/DSC_003.jpg',
|
||||
relativePath: 'events/active/wedding-smith-2025/DSC_003.jpg',
|
||||
size: 2356789,
|
||||
modified: new Date('2025-01-21T14:30:00Z'),
|
||||
checksum: 'f6789012345678901234567890123456789012345678901234567890123456',
|
||||
permissions: '644'
|
||||
}
|
||||
],
|
||||
databaseInfo: {
|
||||
...fullManifestOptions.databaseInfo,
|
||||
size: 1148576,
|
||||
checksum: 'g7890123456789012345678901234567890123456789012345678901234567',
|
||||
rowCounts: {
|
||||
events: 158,
|
||||
photos: 4567,
|
||||
access_logs: 12789,
|
||||
admin_users: 3
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const incrementalManifest = await backupManifest.generateIncrementalManifest(
|
||||
incrementalOptions,
|
||||
fullManifest
|
||||
);
|
||||
|
||||
const incrementalJsonPath = path.join(__dirname, 'test-manifest-incremental.json');
|
||||
await backupManifest.saveManifest(incrementalManifest, incrementalJsonPath, 'json');
|
||||
|
||||
console.log('✓ Incremental backup manifest generated');
|
||||
console.log(` - Added files: ${incrementalManifest.incremental.changes.added_files_count}`);
|
||||
console.log(` - Modified files: ${incrementalManifest.incremental.changes.modified_files_count}`);
|
||||
console.log(` - Deleted files: ${incrementalManifest.incremental.changes.deleted_files_count}`);
|
||||
console.log(` - Size difference: ${(incrementalManifest.incremental.changes.size_difference / 1024).toFixed(2)} KB\n`);
|
||||
|
||||
// 5. Compare manifests
|
||||
console.log('5. Comparing manifests...');
|
||||
const comparison = backupManifest.compareManifests(incrementalManifest, fullManifest);
|
||||
console.log('Comparison results:');
|
||||
console.log(` - Added: ${comparison.added_files.length} files`);
|
||||
console.log(` - Modified: ${comparison.modified_files.length} files`);
|
||||
console.log(` - Deleted: ${comparison.deleted_files.length} files`);
|
||||
console.log(` - Unchanged: ${comparison.unchanged_files.length} files`);
|
||||
console.log(` - Database changed: ${comparison.database_changes.checksum_changed ? 'Yes' : 'No'}\n`);
|
||||
|
||||
// 6. Test manifest integrity
|
||||
console.log('6. Testing manifest integrity...');
|
||||
|
||||
// Corrupt the manifest
|
||||
const corruptedManifest = JSON.parse(JSON.stringify(incrementalManifest));
|
||||
corruptedManifest.files.manifest[0].size = 9999999; // Change a file size
|
||||
|
||||
try {
|
||||
backupManifest.validateManifest(corruptedManifest);
|
||||
console.log('✗ Validation should have failed for corrupted manifest');
|
||||
} catch (error) {
|
||||
console.log('✓ Correctly detected corrupted manifest:', error.message);
|
||||
}
|
||||
|
||||
console.log('\n=== All tests completed successfully! ===');
|
||||
|
||||
// Clean up test files
|
||||
await fs.unlink(jsonPath).catch(() => {});
|
||||
await fs.unlink(yamlPath).catch(() => {});
|
||||
await fs.unlink(incrementalJsonPath).catch(() => {});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error);
|
||||
logger.error('Manifest test failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests
|
||||
testManifestGeneration().catch(console.error);
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* Test script to verify CMS and email formatting improvements
|
||||
*/
|
||||
|
||||
const { formatWelcomeMessage, nl2br } = require('../src/utils/formatters');
|
||||
|
||||
console.log('Testing CMS and Email Formatting Improvements\n');
|
||||
|
||||
// Test 1: Basic line break conversion
|
||||
console.log('Test 1: Basic line break conversion');
|
||||
const basicText = `Hello,
|
||||
This is line 1.
|
||||
This is line 2.
|
||||
|
||||
This is line 4 with an extra break.`;
|
||||
|
||||
console.log('Input:');
|
||||
console.log(basicText);
|
||||
console.log('\nOutput (nl2br):');
|
||||
console.log(nl2br(basicText));
|
||||
console.log('\n---\n');
|
||||
|
||||
// Test 2: Welcome message formatting
|
||||
console.log('Test 2: Welcome message formatting');
|
||||
const welcomeMessage = `Dear guests,
|
||||
|
||||
We're so excited to share these special moments with you!
|
||||
|
||||
Please note:
|
||||
- Download your photos before the expiration date
|
||||
- The password is case-sensitive
|
||||
- Contact us if you have any issues
|
||||
|
||||
Thank you for being part of our special day!
|
||||
|
||||
Best regards,
|
||||
Sarah & John`;
|
||||
|
||||
console.log('Input:');
|
||||
console.log(welcomeMessage);
|
||||
console.log('\nOutput (formatWelcomeMessage):');
|
||||
console.log(formatWelcomeMessage(welcomeMessage));
|
||||
console.log('\n---\n');
|
||||
|
||||
// Test 3: Empty and edge cases
|
||||
console.log('Test 3: Edge cases');
|
||||
console.log('Empty string:', formatWelcomeMessage(''));
|
||||
console.log('Null:', formatWelcomeMessage(null));
|
||||
console.log('Only spaces:', formatWelcomeMessage(' \n \n '));
|
||||
console.log('Single line:', formatWelcomeMessage('This is a single line message'));
|
||||
|
||||
console.log('\nAll tests completed!');
|
||||
@@ -1,85 +0,0 @@
|
||||
const { db } = require('../src/database/db');
|
||||
const { processTemplate } = require('../src/services/emailProcessor');
|
||||
|
||||
async function testGermanEmails() {
|
||||
try {
|
||||
console.log('=== Testing German Email Templates ===\n');
|
||||
|
||||
// Test variables
|
||||
const testVars = {
|
||||
host_name: 'Max Mustermann',
|
||||
event_name: 'Hochzeit Schmidt',
|
||||
event_date: '15.07.2024',
|
||||
gallery_link: 'https://example.com/gallery/test',
|
||||
gallery_password: 'test1234',
|
||||
expiry_date: '15.08.2024',
|
||||
days_remaining: '7',
|
||||
welcome_message: 'Herzlich willkommen zu unserer Hochzeitsgalerie!',
|
||||
archive_size: '250 MB',
|
||||
archive_date: '16.08.2024',
|
||||
photo_count: '347',
|
||||
admin_email: 'support@example.com',
|
||||
eventId: 1
|
||||
};
|
||||
|
||||
const templates = await db('email_templates').select('*');
|
||||
|
||||
for (const template of templates) {
|
||||
console.log(`\n========== ${template.template_key.toUpperCase()} ==========`);
|
||||
|
||||
// Process German version
|
||||
const germanResult = await processGermanTemplate(template, testVars);
|
||||
|
||||
console.log('\n--- GERMAN VERSION ---');
|
||||
console.log('Subject:', germanResult.subject);
|
||||
console.log('\nHTML Preview (first 500 chars):');
|
||||
console.log(germanResult.htmlBody.substring(0, 500) + '...\n');
|
||||
|
||||
// Check for any remaining English text
|
||||
const englishWords = ['Dear', 'Gallery', 'Details:', 'Link:', 'Password:', 'days', 'Thank you'];
|
||||
const foundEnglish = englishWords.filter(word =>
|
||||
germanResult.htmlBody.includes(word) || germanResult.subject.includes(word)
|
||||
);
|
||||
|
||||
if (foundEnglish.length > 0) {
|
||||
console.log('⚠️ WARNING: Found English words:', foundEnglish.join(', '));
|
||||
} else {
|
||||
console.log('✅ No English words found in German template');
|
||||
}
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function processGermanTemplate(template, variables) {
|
||||
// Process template as German
|
||||
const subjectField = 'subject_de';
|
||||
const htmlField = 'body_html_de';
|
||||
const textField = 'body_text_de';
|
||||
|
||||
let subject = template[subjectField] || template.subject || '';
|
||||
let htmlBody = template[htmlField] || template.body_html || '';
|
||||
let textBody = template[textField] || template.body_text || '';
|
||||
|
||||
// Replace variables
|
||||
Object.keys(variables).forEach(key => {
|
||||
const regex = new RegExp(`{{${key}}}`, 'g');
|
||||
subject = subject.replace(regex, variables[key]);
|
||||
htmlBody = htmlBody.replace(regex, variables[key]);
|
||||
textBody = textBody.replace(regex, variables[key]);
|
||||
});
|
||||
|
||||
// Handle conditionals (simplified)
|
||||
htmlBody = htmlBody.replace(/{{#if welcome_message}}[\s\S]*?{{\/if}}/g, (match) => {
|
||||
return variables.welcome_message ? match.replace(/{{#if welcome_message}}|{{\/if}}/g, '') : '';
|
||||
});
|
||||
|
||||
return { subject, htmlBody, textBody };
|
||||
}
|
||||
|
||||
testGermanEmails();
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script to test photo authentication
|
||||
* Usage: node scripts/test-photo-auth.js <jwt-token>
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
async function testPhotoAuth(token) {
|
||||
if (!token) {
|
||||
console.error('Usage: node scripts/test-photo-auth.js <jwt-token>');
|
||||
console.error('\nTo get a token, login to a gallery and check localStorage for gallery_token_<slug>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const baseUrl = process.env.API_URL || 'http://localhost:3001';
|
||||
|
||||
console.log(`Testing photo authentication with token: ${token.substring(0, 20)}...`);
|
||||
console.log(`Base URL: ${baseUrl}\n`);
|
||||
|
||||
// Test URLs
|
||||
const tests = [
|
||||
{
|
||||
name: 'Thumbnail via static route',
|
||||
url: `${baseUrl}/thumbnails/thumb_Test_Gallery_uncategorized_5210.jpg`,
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
},
|
||||
{
|
||||
name: 'Photo via static route',
|
||||
url: `${baseUrl}/photos/wedding-test-gallery-2025-07-14-1/Test_Gallery_uncategorized_5210.jpg`,
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
},
|
||||
{
|
||||
name: 'Gallery photos API',
|
||||
url: `${baseUrl}/api/gallery/wedding-test-gallery-2025-07-14-1/photos`,
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of tests) {
|
||||
console.log(`Testing: ${test.name}`);
|
||||
console.log(`URL: ${test.url}`);
|
||||
|
||||
try {
|
||||
const response = await axios.get(test.url, {
|
||||
headers: test.headers,
|
||||
validateStatus: () => true // Don't throw on any status
|
||||
});
|
||||
|
||||
console.log(`Status: ${response.status}`);
|
||||
console.log(`Headers:`, response.headers['content-type']);
|
||||
|
||||
if (response.status === 200) {
|
||||
if (test.name.includes('API')) {
|
||||
console.log(`Photos count: ${response.data.photos?.length || 0}`);
|
||||
} else {
|
||||
console.log(`Content length: ${response.headers['content-length']} bytes`);
|
||||
}
|
||||
} else {
|
||||
console.log(`Error:`, response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Network error:`, error.message);
|
||||
}
|
||||
|
||||
console.log('---\n');
|
||||
}
|
||||
|
||||
// Decode token to show info
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
||||
console.log('Token payload:', payload);
|
||||
} catch (error) {
|
||||
console.log('Failed to decode token');
|
||||
}
|
||||
}
|
||||
|
||||
// Get token from command line
|
||||
const token = process.argv[2];
|
||||
|
||||
testPhotoAuth(token).catch(error => {
|
||||
console.error('Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test script to verify security logging is working correctly
|
||||
* Run with: node scripts/test-security-logging.js
|
||||
*/
|
||||
|
||||
require('dotenv').config({ path: '../.env' });
|
||||
const logger = require('../src/utils/logger');
|
||||
|
||||
console.log('Testing Security Logging...\n');
|
||||
|
||||
// Test 1: Basic logging
|
||||
console.log('1. Testing basic logging levels:');
|
||||
logger.info('Test info message', { test: true });
|
||||
logger.warn('Test warning message', { test: true });
|
||||
logger.error('Test error message', { test: true });
|
||||
|
||||
// Test 2: Security event logging
|
||||
console.log('\n2. Testing security event logging:');
|
||||
|
||||
// Rate limit exceeded
|
||||
logger.warn('Rate limit exceeded', {
|
||||
ip: '192.168.1.100',
|
||||
path: '/api/admin/login',
|
||||
method: 'POST',
|
||||
authenticated: false,
|
||||
userAgent: 'Mozilla/5.0 Test',
|
||||
timestamp: new Date().toISOString(),
|
||||
rateLimitInfo: {
|
||||
limit: 5,
|
||||
current: 6,
|
||||
remaining: 0,
|
||||
resetTime: new Date(Date.now() + 900000).toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
// Auth rate limit
|
||||
logger.warn('Auth rate limit exceeded', {
|
||||
ip: '192.168.1.101',
|
||||
path: '/api/auth/admin/login',
|
||||
method: 'POST',
|
||||
userAgent: 'Mozilla/5.0 Test',
|
||||
authType: 'admin',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Failed login
|
||||
logger.warn('Failed login attempt', {
|
||||
username: 'testuser',
|
||||
ip: '192.168.1.102',
|
||||
userAgent: 'Mozilla/5.0 Test',
|
||||
reason: 'invalid_credentials',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// JWT validation failure
|
||||
logger.warn('JWT validation failed', {
|
||||
ip: '192.168.1.103',
|
||||
path: '/api/admin/events',
|
||||
method: 'GET',
|
||||
userAgent: 'Mozilla/5.0 Test',
|
||||
error: 'TokenExpiredError',
|
||||
message: 'jwt expired',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Account lockout
|
||||
logger.warn('Login attempt on locked account', {
|
||||
username: 'lockeduser',
|
||||
ip: '192.168.1.104',
|
||||
remainingLockTime: 1200,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Suspicious activity
|
||||
logger.warn('Suspicious login activity detected', {
|
||||
username: 'suspicioususer',
|
||||
ips: ['192.168.1.105', '192.168.1.106', '192.168.1.107'],
|
||||
timeWindow: '15 minutes',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
console.log('\n3. Check log files:');
|
||||
console.log('- logs/security.log - Should contain all security warnings');
|
||||
console.log('- logs/error.log - Should contain error messages');
|
||||
console.log('- logs/combined.log - Should contain all messages');
|
||||
|
||||
console.log('\n✅ Security logging test complete!');
|
||||
console.log('Review the log files to ensure all events are properly captured.');
|
||||
|
||||
// Give logger time to flush
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 1000);
|
||||
@@ -1,110 +0,0 @@
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function verifyTemplateEquality() {
|
||||
try {
|
||||
console.log('Verifying template equality between German and English versions...\n');
|
||||
|
||||
const templates = await db('email_templates').select('*');
|
||||
|
||||
for (const template of templates) {
|
||||
console.log(`\n=== ${template.template_key.toUpperCase()} ===`);
|
||||
|
||||
// Check subject length similarity
|
||||
const subjectEnLength = template.subject_en?.length || 0;
|
||||
const subjectDeLength = template.subject_de?.length || 0;
|
||||
console.log(`Subject length - EN: ${subjectEnLength}, DE: ${subjectDeLength}`);
|
||||
|
||||
// Check HTML content features
|
||||
const htmlEn = template.body_html_en || '';
|
||||
const htmlDe = template.body_html_de || '';
|
||||
|
||||
// Check for key features in both versions
|
||||
const features = [
|
||||
{ name: 'Handlebars conditionals', pattern: /{{#if/g },
|
||||
{ name: 'Styled divs', pattern: /style="/g },
|
||||
{ name: 'Background colors', pattern: /background-color:/g },
|
||||
{ name: 'Buttons/CTAs', pattern: /<a.*style.*background-color.*>/g },
|
||||
{ name: 'Icons/Emojis', pattern: /[📧📞✅⚠️]/g },
|
||||
{ name: 'Lists', pattern: /<ul/g },
|
||||
{ name: 'Strong emphasis', pattern: /<strong>/g }
|
||||
];
|
||||
|
||||
console.log('\nFeature comparison:');
|
||||
for (const feature of features) {
|
||||
const enCount = (htmlEn.match(feature.pattern) || []).length;
|
||||
const deCount = (htmlDe.match(feature.pattern) || []).length;
|
||||
const status = enCount === deCount ? '✅' : '❌';
|
||||
console.log(`${status} ${feature.name}: EN=${enCount}, DE=${deCount}`);
|
||||
}
|
||||
|
||||
// Check text content length
|
||||
const textEn = template.body_text_en || '';
|
||||
const textDe = template.body_text_de || '';
|
||||
console.log(`\nText content length - EN: ${textEn.length}, DE: ${textDe.length}`);
|
||||
|
||||
// Check for specific variables usage
|
||||
const variables = [
|
||||
'host_name', 'event_name', 'event_date', 'gallery_link',
|
||||
'gallery_password', 'expiry_date', 'welcome_message',
|
||||
'days_remaining', 'support_email', 'support_phone',
|
||||
'archive_date', 'photo_count', 'archive_size'
|
||||
];
|
||||
|
||||
const missingInEn = [];
|
||||
const missingInDe = [];
|
||||
|
||||
for (const variable of variables) {
|
||||
const varPattern = new RegExp(`{{${variable}}}`, 'g');
|
||||
const inEn = varPattern.test(htmlEn) || varPattern.test(textEn);
|
||||
const inDe = varPattern.test(htmlDe) || varPattern.test(textDe);
|
||||
|
||||
if (inDe && !inEn) missingInEn.push(variable);
|
||||
if (inEn && !inDe) missingInDe.push(variable);
|
||||
}
|
||||
|
||||
if (missingInEn.length > 0) {
|
||||
console.log(`\n⚠️ Variables in DE but missing in EN: ${missingInEn.join(', ')}`);
|
||||
}
|
||||
if (missingInDe.length > 0) {
|
||||
console.log(`\n⚠️ Variables in EN but missing in DE: ${missingInDe.join(', ')}`);
|
||||
}
|
||||
|
||||
// Overall quality score
|
||||
const enScore = [
|
||||
htmlEn.includes('style='),
|
||||
htmlEn.includes('{{#if'),
|
||||
htmlEn.includes('background-color'),
|
||||
htmlEn.includes('<strong>'),
|
||||
htmlEn.includes('margin:'),
|
||||
htmlEn.includes('padding:')
|
||||
].filter(Boolean).length;
|
||||
|
||||
const deScore = [
|
||||
htmlDe.includes('style='),
|
||||
htmlDe.includes('{{#if'),
|
||||
htmlDe.includes('background-color'),
|
||||
htmlDe.includes('<strong>'),
|
||||
htmlDe.includes('margin:'),
|
||||
htmlDe.includes('padding:')
|
||||
].filter(Boolean).length;
|
||||
|
||||
console.log(`\nQuality score (out of 6) - EN: ${enScore}, DE: ${deScore}`);
|
||||
console.log(enScore === deScore ? '✅ Templates have equal quality!' : '❌ Quality mismatch');
|
||||
}
|
||||
|
||||
console.log('\n\nSummary:');
|
||||
console.log('The English templates have been updated to match the German templates in:');
|
||||
console.log('- HTML styling and structure');
|
||||
console.log('- Conditional content blocks');
|
||||
console.log('- Visual elements (buttons, alerts, icons)');
|
||||
console.log('- Information completeness');
|
||||
console.log('- Professional formatting');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
verifyTemplateEquality();
|
||||
@@ -447,9 +447,14 @@ router.delete('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
|
||||
if (photo.thumbnail_path) {
|
||||
const thumbPath = path.join(storagePath, 'events/active', photo.thumbnail_path);
|
||||
try {
|
||||
// Check if file exists before attempting to delete
|
||||
await fs.access(thumbPath);
|
||||
await fs.unlink(thumbPath);
|
||||
} catch (error) {
|
||||
console.error('Error deleting thumbnail:', error);
|
||||
// Only log if it's not a "file not found" error
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error('Error deleting thumbnail:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,9 +539,14 @@ router.post('/:eventId/photos/bulk-delete', adminAuth, async (req, res) => {
|
||||
if (photo.thumbnail_path) {
|
||||
const thumbPath = path.join(storagePath, photo.thumbnail_path);
|
||||
try {
|
||||
// Check if file exists before attempting to delete
|
||||
await fs.access(thumbPath);
|
||||
await fs.unlink(thumbPath);
|
||||
} catch (error) {
|
||||
console.error('Error deleting thumbnail:', error);
|
||||
// Only log if it's not a "file not found" error
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error('Error deleting thumbnail:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ const ATTEMPT_WINDOW = 15 * 60 * 1000; // 15 minutes window for counting attempt
|
||||
*/
|
||||
async function trackFailedAttempt(identifier, ipAddress, userAgent) {
|
||||
try {
|
||||
// Check if table exists first
|
||||
const tableExists = await db.schema.hasTable('login_attempts');
|
||||
if (!tableExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
await db('login_attempts').insert({
|
||||
identifier,
|
||||
ip_address: ipAddress,
|
||||
@@ -48,6 +54,12 @@ async function trackFailedAttempt(identifier, ipAddress, userAgent) {
|
||||
*/
|
||||
async function trackSuccessfulLogin(identifier, ipAddress, userAgent) {
|
||||
try {
|
||||
// Check if table exists first
|
||||
const tableExists = await db.schema.hasTable('login_attempts');
|
||||
if (!tableExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
await db('login_attempts').insert({
|
||||
identifier,
|
||||
ip_address: ipAddress,
|
||||
@@ -75,6 +87,12 @@ async function trackSuccessfulLogin(identifier, ipAddress, userAgent) {
|
||||
*/
|
||||
async function checkAccountLockout(identifier) {
|
||||
try {
|
||||
// Check if table exists first
|
||||
const tableExists = await db.schema.hasTable('login_attempts');
|
||||
if (!tableExists) {
|
||||
return { isLocked: false };
|
||||
}
|
||||
|
||||
const recentWindow = new Date(Date.now() - ATTEMPT_WINDOW);
|
||||
|
||||
// Get recent failed attempts
|
||||
@@ -114,6 +132,12 @@ async function checkAccountLockout(identifier) {
|
||||
*/
|
||||
async function checkSuspiciousActivity(identifier, ipAddress) {
|
||||
try {
|
||||
// Check if table exists first
|
||||
const tableExists = await db.schema.hasTable('login_attempts');
|
||||
if (!tableExists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for rapid attempts from different IPs
|
||||
const recentWindow = new Date(Date.now() - 5 * 60 * 1000); // 5 minutes
|
||||
|
||||
@@ -153,6 +177,13 @@ function getGenericAuthError() {
|
||||
*/
|
||||
async function cleanupOldAttempts() {
|
||||
try {
|
||||
// Check if table exists first
|
||||
const tableExists = await db.schema.hasTable('login_attempts');
|
||||
if (!tableExists) {
|
||||
// Table doesn't exist, skip cleanup
|
||||
return;
|
||||
}
|
||||
|
||||
const cutoffDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); // 7 days
|
||||
|
||||
const deleted = await db('login_attempts')
|
||||
|
||||
@@ -1,478 +0,0 @@
|
||||
# Comprehensive Backup & Restore Implementation Plan
|
||||
|
||||
## Introduction
|
||||
|
||||
This plan extends the existing backup system to include full database backups, S3/MinIO support, intelligent change detection, and complete restore functionality. The system will create versioned, encrypted backups with manifests for easy restoration while minimizing storage usage through incremental backups and smart scheduling.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Enhanced Database Schema & Core Infrastructure
|
||||
|
||||
### Task 1.1: Create Enhanced Database Migration
|
||||
**File:** `backend/migrations/030_enhance_backup_system.js`
|
||||
**Purpose:** Add tables for database backups, restore operations, and backup manifests
|
||||
|
||||
```sql
|
||||
-- backup_manifests table
|
||||
- id (primary key)
|
||||
- backup_run_id (FK to backup_runs)
|
||||
- manifest_version (e.g., "1.0.0")
|
||||
- created_at
|
||||
- database_dump_path
|
||||
- database_checksum
|
||||
- files_manifest (JSON with all file paths/checksums)
|
||||
- metadata (JSON with system info, versions, etc.)
|
||||
|
||||
-- restore_operations table
|
||||
- id (primary key)
|
||||
- started_at
|
||||
- completed_at
|
||||
- status (pending, running, completed, failed)
|
||||
- restore_type (full, partial, database_only, files_only)
|
||||
- source_backup_id (FK to backup_runs)
|
||||
- restored_by (FK to admin_users)
|
||||
- error_message
|
||||
- restore_log (detailed log)
|
||||
|
||||
-- backup_change_tracking table
|
||||
- id (primary key)
|
||||
- table_name
|
||||
- last_change_timestamp
|
||||
- row_count
|
||||
- checksum
|
||||
- last_backed_up
|
||||
```
|
||||
|
||||
### Task 1.2: Add S3 Configuration Settings
|
||||
**File:** Update migration `029_add_backup_service_tables.js`
|
||||
**Add settings:**
|
||||
- `backup_s3_use_ssl` (boolean)
|
||||
- `backup_s3_path_style` (for MinIO compatibility)
|
||||
- `backup_encryption_enabled` (boolean)
|
||||
- `backup_encryption_key` (encrypted storage)
|
||||
- `backup_database_included` (boolean)
|
||||
- `backup_incremental_enabled` (boolean)
|
||||
- `backup_versioning_enabled` (boolean)
|
||||
- `backup_versions_to_keep` (number)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: S3/MinIO Implementation
|
||||
|
||||
### Task 2.1: Install S3 Dependencies
|
||||
**File:** `backend/package.json`
|
||||
**Command:** `npm install @aws-sdk/client-s3 @aws-sdk/lib-storage mime-types`
|
||||
**Purpose:** AWS SDK v3 for S3-compatible storage
|
||||
|
||||
### Task 2.2: Create S3 Storage Adapter
|
||||
**File:** `backend/src/services/storage/s3Storage.js`
|
||||
**Implementation:**
|
||||
```javascript
|
||||
class S3StorageAdapter {
|
||||
constructor(config)
|
||||
connect() // Test connection
|
||||
uploadFile(localPath, remotePath, metadata)
|
||||
uploadStream(stream, remotePath, metadata)
|
||||
downloadFile(remotePath, localPath)
|
||||
listFiles(prefix)
|
||||
deleteFile(remotePath)
|
||||
getSignedUrl(remotePath, expiresIn)
|
||||
createMultipartUpload(remotePath) // For large files
|
||||
uploadPart(uploadId, partNumber, data)
|
||||
completeMultipartUpload(uploadId, parts)
|
||||
}
|
||||
```
|
||||
|
||||
### Task 2.3: Implement MinIO Compatibility Layer
|
||||
**File:** `backend/src/services/storage/minioCompat.js`
|
||||
**Features:**
|
||||
- Path-style URL handling
|
||||
- Custom endpoint configuration
|
||||
- SSL/TLS options
|
||||
- Bucket creation if not exists
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Database Backup Integration
|
||||
|
||||
### Task 3.1: Create Database Dump Service
|
||||
**File:** `backend/src/services/databaseBackup.js`
|
||||
**Implementation:**
|
||||
```javascript
|
||||
class DatabaseBackupService {
|
||||
async createBackup(format = 'sql') // sql or json
|
||||
async dumpSQLite(outputPath)
|
||||
async dumpPostgreSQL(outputPath)
|
||||
async compressBackup(inputPath, outputPath)
|
||||
async encryptBackup(inputPath, outputPath, key)
|
||||
async validateBackup(backupPath)
|
||||
async getTableChecksums() // For change detection
|
||||
}
|
||||
```
|
||||
|
||||
### Task 3.2: Implement Change Detection for Database
|
||||
**File:** `backend/src/services/changeDetection.js`
|
||||
**Features:**
|
||||
- Track table modifications using triggers
|
||||
- Calculate table checksums
|
||||
- Compare with last backup state
|
||||
- Intelligent backup decision making
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Enhanced Backup Service
|
||||
|
||||
### Task 4.1: Refactor Backup Service for S3
|
||||
**File:** `backend/src/services/backupService.js`
|
||||
**Modifications:**
|
||||
- Add `performS3Backup()` implementation
|
||||
- Support multipart uploads for large files
|
||||
- Add progress tracking callbacks
|
||||
- Implement retry logic with exponential backoff
|
||||
|
||||
### Task 4.2: Create Backup Manifest Generator
|
||||
**File:** `backend/src/services/backupManifest.js`
|
||||
**Structure:**
|
||||
```javascript
|
||||
{
|
||||
version: "1.0.0",
|
||||
created_at: "2024-01-20T10:00:00Z",
|
||||
system_info: {
|
||||
app_version: "1.0.74",
|
||||
node_version: "18.x",
|
||||
database_type: "sqlite|postgresql"
|
||||
},
|
||||
database: {
|
||||
dump_file: "database/dump.sql.gz",
|
||||
checksum: "sha256:...",
|
||||
tables: { /* table info */ }
|
||||
},
|
||||
files: {
|
||||
count: 1234,
|
||||
total_size: 5678901234,
|
||||
entries: [
|
||||
{
|
||||
path: "events/active/...",
|
||||
checksum: "sha256:...",
|
||||
size: 12345,
|
||||
modified: "2024-01-20T09:00:00Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
settings: { /* app settings snapshot */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Task 4.3: Implement Incremental Backup Logic
|
||||
**File:** `backend/src/services/incrementalBackup.js`
|
||||
**Features:**
|
||||
- Track changed files since last full backup
|
||||
- Create incremental manifest
|
||||
- Link to parent backup
|
||||
- Merge incremental backups
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Restore Functionality
|
||||
|
||||
### Task 5.1: Create Restore Service
|
||||
**File:** `backend/src/services/restoreService.js`
|
||||
**Implementation:**
|
||||
```javascript
|
||||
class RestoreService {
|
||||
async validateBackup(backupId)
|
||||
async prepareRestore(backupId, options)
|
||||
async restoreDatabase(manifestPath)
|
||||
async restoreFiles(manifestPath, options)
|
||||
async performFullRestore(backupId)
|
||||
async performPartialRestore(backupId, selections)
|
||||
async rollbackRestore(restoreId)
|
||||
async verifyRestore(restoreId)
|
||||
}
|
||||
```
|
||||
|
||||
### Task 5.2: Implement Safe Restore Process
|
||||
**File:** `backend/src/services/restoreValidation.js`
|
||||
**Safety Features:**
|
||||
- Pre-restore backup creation
|
||||
- Validation checksums
|
||||
- Atomic operations
|
||||
- Rollback capability
|
||||
- Post-restore verification
|
||||
|
||||
### Task 5.3: Create Restore CLI Tool
|
||||
**File:** `backend/scripts/restore-backup.js`
|
||||
**Purpose:** Emergency restore without running application
|
||||
**Features:**
|
||||
- Interactive mode
|
||||
- Dry-run option
|
||||
- Progress display
|
||||
- Validation reports
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Admin API Extensions
|
||||
|
||||
### Task 6.1: Add Restore Endpoints
|
||||
**File:** `backend/src/routes/adminBackup.js`
|
||||
**New Endpoints:**
|
||||
```javascript
|
||||
POST /api/admin/backup/restore/validate
|
||||
POST /api/admin/backup/restore/start
|
||||
GET /api/admin/backup/restore/:id/status
|
||||
POST /api/admin/backup/restore/:id/cancel
|
||||
GET /api/admin/backup/manifests/:backupId
|
||||
GET /api/admin/backup/download/:backupId
|
||||
```
|
||||
|
||||
### Task 6.2: Add S3 Management Endpoints
|
||||
**File:** `backend/src/routes/adminBackup.js`
|
||||
**New Endpoints:**
|
||||
```javascript
|
||||
GET /api/admin/backup/s3/buckets
|
||||
GET /api/admin/backup/s3/files
|
||||
DELETE /api/admin/backup/s3/cleanup
|
||||
POST /api/admin/backup/s3/test-upload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Frontend Implementation
|
||||
|
||||
### Task 7.1: Create Backup Management Page
|
||||
**File:** `frontend/src/pages/admin/BackupManagement.jsx`
|
||||
**Components:**
|
||||
- Backup configuration form
|
||||
- Backup history table
|
||||
- Manual backup trigger
|
||||
- Restore interface
|
||||
- Progress indicators
|
||||
|
||||
### Task 7.2: Create Backup Status Dashboard
|
||||
**File:** `frontend/src/components/admin/BackupDashboard.jsx`
|
||||
**Features:**
|
||||
- Real-time backup status
|
||||
- Storage usage charts
|
||||
- Backup success rate
|
||||
- Next scheduled backup
|
||||
- Recent backup/restore operations
|
||||
|
||||
### Task 7.3: Implement Restore Wizard
|
||||
**File:** `frontend/src/components/admin/RestoreWizard.jsx`
|
||||
**Steps:**
|
||||
1. Select backup to restore
|
||||
2. Choose restore type (full/partial)
|
||||
3. Select components (database/files/settings)
|
||||
4. Review and confirm
|
||||
5. Monitor progress
|
||||
6. Verify results
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Background Job Enhancements
|
||||
|
||||
### Task 8.1: Implement Smart Scheduling
|
||||
**File:** `backend/src/services/smartScheduler.js`
|
||||
**Features:**
|
||||
- Skip backup if no changes detected
|
||||
- Adaptive scheduling based on activity
|
||||
- Priority queuing for critical backups
|
||||
- Resource usage monitoring
|
||||
|
||||
### Task 8.2: Create Backup Monitor Service
|
||||
**File:** `backend/src/services/backupMonitor.js`
|
||||
**Purpose:** Monitor backup health and alert on issues
|
||||
**Features:**
|
||||
- Check last successful backup age
|
||||
- Verify backup integrity periodically
|
||||
- Monitor storage usage
|
||||
- Alert on failures or anomalies
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: Security & Encryption
|
||||
|
||||
### Task 9.1: Implement Backup Encryption
|
||||
**File:** `backend/src/utils/encryption.js`
|
||||
**Features:**
|
||||
- AES-256-GCM encryption
|
||||
- Key derivation from master key
|
||||
- Encrypted manifest headers
|
||||
- Secure key storage
|
||||
|
||||
### Task 9.2: Add Access Control
|
||||
**File:** `backend/src/middleware/backupAuth.js`
|
||||
**Features:**
|
||||
- Separate permissions for backup/restore
|
||||
- Audit logging for all operations
|
||||
- IP whitelist for restore operations
|
||||
- Two-factor authentication for restore
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: Testing & Validation
|
||||
|
||||
### Task 10.1: Create Backup Test Suite
|
||||
**File:** `backend/__tests__/services/backup.test.js`
|
||||
**Tests:**
|
||||
- Unit tests for each backup method
|
||||
- Integration tests with real S3/MinIO
|
||||
- Database backup/restore cycles
|
||||
- Encryption/decryption validation
|
||||
- Manifest generation and parsing
|
||||
|
||||
### Task 10.2: Create Restore Test Suite
|
||||
**File:** `backend/__tests__/services/restore.test.js`
|
||||
**Tests:**
|
||||
- Full restore scenarios
|
||||
- Partial restore validation
|
||||
- Rollback testing
|
||||
- Corruption recovery
|
||||
- Cross-version compatibility
|
||||
|
||||
### Task 10.3: Create E2E Backup/Restore Tests
|
||||
**File:** `backend/__tests__/e2e/backupRestore.test.js`
|
||||
**Scenarios:**
|
||||
- Complete backup/restore cycle
|
||||
- Disaster recovery simulation
|
||||
- Performance benchmarks
|
||||
- Storage optimization validation
|
||||
|
||||
---
|
||||
|
||||
## Phase 11: Documentation & Deployment
|
||||
|
||||
### Task 11.1: Create Backup Administrator Guide
|
||||
**File:** `docs/backup-admin-guide.md`
|
||||
**Contents:**
|
||||
- Configuration guide
|
||||
- Best practices
|
||||
- Troubleshooting
|
||||
- Recovery procedures
|
||||
- Performance tuning
|
||||
|
||||
### Task 11.2: Update Docker Configuration
|
||||
**Files:** `docker-compose.yml`, `Dockerfile`
|
||||
**Changes:**
|
||||
- Add S3/MinIO service for development
|
||||
- Volume mappings for backups
|
||||
- Environment variable templates
|
||||
- Health checks for backup service
|
||||
|
||||
### Task 11.3: Create Backup Playbook
|
||||
**File:** `docs/backup-playbook.md`
|
||||
**Scenarios:**
|
||||
- Daily backup verification
|
||||
- Disaster recovery steps
|
||||
- Migration procedures
|
||||
- Troubleshooting flowchart
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order & Priority
|
||||
|
||||
### Critical Path (Must Have):
|
||||
1. S3 Storage Adapter (Task 2.2)
|
||||
2. Database Backup Service (Task 3.1)
|
||||
3. Enhanced Backup Service (Task 4.1)
|
||||
4. Basic Restore Service (Task 5.1)
|
||||
5. Admin API Extensions (Task 6.1)
|
||||
6. Backup Test Suite (Task 10.1)
|
||||
|
||||
### High Priority (Should Have):
|
||||
1. Backup Manifest Generator (Task 4.2)
|
||||
2. Change Detection (Task 3.2)
|
||||
3. Frontend Backup Page (Task 7.1)
|
||||
4. Encryption Implementation (Task 9.1)
|
||||
5. Restore Wizard (Task 7.3)
|
||||
|
||||
### Nice to Have:
|
||||
1. Incremental Backups (Task 4.3)
|
||||
2. Smart Scheduling (Task 8.1)
|
||||
3. Advanced Monitoring (Task 8.2)
|
||||
4. MinIO Compatibility (Task 2.3)
|
||||
|
||||
---
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### S3 Configuration:
|
||||
```javascript
|
||||
{
|
||||
backup_destination_type: "s3",
|
||||
backup_s3_endpoint: "https://s3.amazonaws.com",
|
||||
backup_s3_bucket: "wedding-backups",
|
||||
backup_s3_access_key: "AKIA...",
|
||||
backup_s3_secret_key: "secret",
|
||||
backup_s3_region: "us-east-1",
|
||||
backup_s3_use_ssl: true,
|
||||
backup_s3_path_style: false
|
||||
}
|
||||
```
|
||||
|
||||
### MinIO Configuration:
|
||||
```javascript
|
||||
{
|
||||
backup_destination_type: "s3",
|
||||
backup_s3_endpoint: "https://minio.example.com:9000",
|
||||
backup_s3_bucket: "picpeak-backups",
|
||||
backup_s3_access_key: "minioadmin",
|
||||
backup_s3_secret_key: "minioadmin",
|
||||
backup_s3_region: "us-east-1",
|
||||
backup_s3_use_ssl: true,
|
||||
backup_s3_path_style: true // Required for MinIO
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Implementation
|
||||
|
||||
1. **Change Detection**: Use database triggers and file checksums to detect changes
|
||||
2. **Compression**: Always compress before encryption for better ratios
|
||||
3. **Chunking**: Split large backups into manageable chunks
|
||||
4. **Versioning**: Keep multiple backup versions with rotation
|
||||
5. **Validation**: Verify every backup immediately after creation
|
||||
6. **Monitoring**: Alert on backup failures within 5 minutes
|
||||
7. **Testing**: Perform monthly restore drills
|
||||
8. **Documentation**: Log every backup/restore operation with details
|
||||
|
||||
---
|
||||
|
||||
## Key Features of This Implementation
|
||||
|
||||
### Intelligent Change Detection
|
||||
- Only backs up when changes are detected
|
||||
- Tracks database modifications via checksums
|
||||
- Monitors file system changes
|
||||
- Reduces unnecessary backup operations
|
||||
|
||||
### Comprehensive Backup Scope
|
||||
- Full database dumps (SQLite/PostgreSQL)
|
||||
- All event photos and thumbnails
|
||||
- Application settings and configuration
|
||||
- Email templates and user data
|
||||
- Complete system state capture
|
||||
|
||||
### Flexible Storage Options
|
||||
- Local directory backup
|
||||
- Remote server via rsync
|
||||
- S3-compatible storage (AWS, MinIO, etc.)
|
||||
- Encrypted storage for security
|
||||
- Compression for space efficiency
|
||||
|
||||
### Robust Restore Capabilities
|
||||
- Full system restore
|
||||
- Partial restore (specific events/data)
|
||||
- Point-in-time recovery
|
||||
- Pre-restore validation
|
||||
- Rollback on failure
|
||||
|
||||
### Enterprise-Grade Features
|
||||
- Backup manifests for verification
|
||||
- Incremental backup support
|
||||
- Version retention policies
|
||||
- Automated cleanup of old backups
|
||||
- Comprehensive audit logging
|
||||
|
||||
This comprehensive plan provides a robust, enterprise-grade backup solution with full disaster recovery capabilities.
|
||||
@@ -0,0 +1,51 @@
|
||||
# docker-compose.override.yml.example
|
||||
#
|
||||
# Copy this file to docker-compose.override.yml for local production customizations
|
||||
# docker-compose.override.yml is git-ignored and will be automatically loaded by Docker Compose
|
||||
#
|
||||
# Example customizations:
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Example: Expose backend port for debugging
|
||||
# backend:
|
||||
# ports:
|
||||
# - "3001:3001"
|
||||
|
||||
# Example: Expose database port for local tools
|
||||
# db:
|
||||
# ports:
|
||||
# - "5432:5432"
|
||||
|
||||
# Example: Custom nginx ports
|
||||
# nginx:
|
||||
# ports:
|
||||
# - "8080:80"
|
||||
# - "8443:443"
|
||||
|
||||
# Example: Enable Umami web interface
|
||||
# umami:
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
|
||||
# Example: Use different storage paths
|
||||
# backend:
|
||||
# volumes:
|
||||
# - /mnt/photos:/app/storage
|
||||
# - /mnt/data:/app/data
|
||||
|
||||
# Example: Development-like setup with code mounting
|
||||
# backend:
|
||||
# volumes:
|
||||
# - ./backend:/app
|
||||
# - /app/node_modules
|
||||
# command: npm run dev
|
||||
|
||||
# Example: Add Mailhog for email testing
|
||||
# mailhog:
|
||||
# image: mailhog/mailhog:latest
|
||||
# ports:
|
||||
# - "1025:1025"
|
||||
# - "8025:8025"
|
||||
# restart: unless-stopped
|
||||
@@ -1,120 +0,0 @@
|
||||
# docker-compose.prod.yml - Production configuration
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
image: picpeak-backend:latest
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ADMIN_URL=${ADMIN_URL}
|
||||
- FRONTEND_URL=${FRONTEND_URL}
|
||||
# Database
|
||||
- DATABASE_CLIENT=pg
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
- DB_USER=${DB_USER:-picpeak}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_NAME=${DB_NAME:-picpeak}
|
||||
# Email
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_SECURE=${SMTP_SECURE}
|
||||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASS=${SMTP_PASS}
|
||||
- EMAIL_FROM=${EMAIL_FROM}
|
||||
# Analytics
|
||||
- UMAMI_URL=${UMAMI_URL}
|
||||
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
|
||||
# Storage paths
|
||||
- STORAGE_PATH=/app/storage
|
||||
- EVENTS_PATH=/app/storage/events
|
||||
- ARCHIVE_PATH=/app/storage/events/archived
|
||||
volumes:
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- picpeak
|
||||
|
||||
frontend:
|
||||
image: picpeak-frontend:latest
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- VITE_API_URL=/api
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- picpeak
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./nginx/sites-enabled:/etc/nginx/sites-enabled
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
networks:
|
||||
- picpeak
|
||||
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
|
||||
|
||||
certbot:
|
||||
image: certbot/certbot
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
|
||||
|
||||
db:
|
||||
image: postgres:14-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=${DB_USER:-picpeak}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME:-picpeak}
|
||||
# Allow connections from any host with password authentication
|
||||
- POSTGRES_HOST_AUTH_METHOD=scram-sha-256
|
||||
- POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=trust
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- picpeak
|
||||
# Allow connections without SSL requirement from Docker network
|
||||
command: postgres -c ssl=off
|
||||
|
||||
umami:
|
||||
image: ghcr.io/umami-software/umami:postgresql-latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${DB_USER:-picpeak}:${DB_PASSWORD}@db:5432/umami
|
||||
DATABASE_TYPE: postgresql
|
||||
HASH_SALT: ${UMAMI_HASH_SALT}
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- picpeak
|
||||
|
||||
networks:
|
||||
picpeak:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
+106
-73
@@ -1,92 +1,125 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: picpeak-postgres
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-picpeak}
|
||||
POSTGRES_USER: ${DB_USER:-picpeak}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-picpeak}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-picpeak}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Backend API
|
||||
backend:
|
||||
build: ./backend
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: picpeak-backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-production}
|
||||
- PORT=3001
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
|
||||
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}
|
||||
- DATABASE_CLIENT=pg
|
||||
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
|
||||
- DB_TYPE=postgresql
|
||||
- DB_HOST=postgres
|
||||
- DB_PORT=5432
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_NAME=${DB_NAME}
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_SECURE=${SMTP_SECURE:-false}
|
||||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASS=${SMTP_PASS}
|
||||
- EMAIL_FROM=${EMAIL_FROM:-noreply@picpeak.local}
|
||||
- FRONTEND_URL=${FRONTEND_URL:-http://localhost:3000}
|
||||
- ADMIN_URL=${ADMIN_URL:-http://localhost:3001}
|
||||
- TZ=${TZ:-UTC}
|
||||
- STORAGE_PATH=/app/storage
|
||||
volumes:
|
||||
- ./events:/app/events
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
- ./backup:/backup
|
||||
- ./storage:/app/storage
|
||||
ports:
|
||||
- "${BACKEND_PORT:-3001}:3001"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
DATABASE_CLIENT: pg
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_NAME: ${DB_NAME:-picpeak}
|
||||
DB_USER: ${DB_USER:-picpeak}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-picpeak}
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- picpeak-network
|
||||
|
||||
# Frontend
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
args:
|
||||
VITE_API_URL: ${VITE_API_URL:-/api}
|
||||
VITE_UMAMI_URL: ${VITE_UMAMI_URL}
|
||||
VITE_UMAMI_WEBSITE_ID: ${VITE_UMAMI_WEBSITE_ID}
|
||||
container_name: picpeak-frontend
|
||||
depends_on:
|
||||
- backend
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: picpeak-postgres
|
||||
restart: unless-stopped
|
||||
|
||||
# Nginx Reverse Proxy
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: picpeak-nginx
|
||||
environment:
|
||||
- POSTGRES_USER=${DB_USER}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME}
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
- TZ=${TZ:-UTC}
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
|
||||
- "${DB_PORT:-5432}:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
networks:
|
||||
- picpeak-network
|
||||
|
||||
# Certbot for SSL
|
||||
certbot:
|
||||
image: certbot/certbot
|
||||
container_name: picpeak-certbot
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: picpeak-redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-picpeak_redis_pass}
|
||||
volumes:
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
|
||||
- redis-data:/data
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
networks:
|
||||
- picpeak-network
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- VITE_API_URL=${VITE_API_URL:-http://localhost:3001/api}
|
||||
- VITE_UMAMI_URL=${VITE_UMAMI_URL:-}
|
||||
- VITE_UMAMI_WEBSITE_ID=${VITE_UMAMI_WEBSITE_ID:-}
|
||||
- VITE_UMAMI_SHARE_URL=${VITE_UMAMI_SHARE_URL:-}
|
||||
container_name: picpeak-frontend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-production}
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-3000}:80"
|
||||
depends_on:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
networks:
|
||||
- picpeak-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
postgres-data:
|
||||
driver: local
|
||||
redis-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: picpeak-network
|
||||
picpeak-network:
|
||||
driver: bridge
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Create umami database if it doesn't exist
|
||||
-- This runs as the postgres superuser during initialization
|
||||
|
||||
SELECT 'CREATE DATABASE umami'
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'umami')\gexec
|
||||
|
||||
-- Grant all privileges on umami database to the application user
|
||||
GRANT ALL PRIVILEGES ON DATABASE umami TO "${POSTGRES_USER}";
|
||||
@@ -1,263 +0,0 @@
|
||||
# PicPeak Security Scan Report
|
||||
|
||||
**Date**: January 12, 2025
|
||||
**Scan Type**: Comprehensive Security Audit
|
||||
**Platform**: PicPeak Photo Sharing Platform
|
||||
**Scanner**: Claude Code Security Scanner
|
||||
|
||||
## Executive Summary
|
||||
|
||||
A comprehensive security scan of the PicPeak photo sharing platform reveals **critical vulnerabilities** that require immediate attention. While the application implements some security best practices, several high-severity issues could lead to data breaches, unauthorized access, and system compromise.
|
||||
|
||||
### Overall Risk Assessment: **HIGH** 🔴
|
||||
|
||||
**Critical Issues Found**: 8
|
||||
**High-Risk Issues**: 7
|
||||
**Medium-Risk Issues**: 6
|
||||
**Low-Risk Issues**: 2
|
||||
|
||||
## Critical Vulnerabilities Requiring Immediate Action
|
||||
|
||||
### 1. Hardcoded Secrets and Credentials 🔴
|
||||
|
||||
#### JWT Secret Fallback
|
||||
- **Location**: `backend/src/routes/protectedImages.js:15,27`
|
||||
- **Severity**: CRITICAL
|
||||
- **Impact**: Complete authentication bypass if environment variable not set
|
||||
```javascript
|
||||
const secret = process.env.JWT_SECRET || 'your-secret-key'; // VULNERABLE
|
||||
```
|
||||
|
||||
#### Default Admin Password
|
||||
- **Location**: `backend/migrations/init.js:14`, `setup-remaining-files.sh:121`
|
||||
- **Severity**: HIGH
|
||||
- **Impact**: Known default credentials allow unauthorized admin access
|
||||
- **Current**: Hardcoded `admin123` password
|
||||
|
||||
### 2. SQL Injection Vulnerabilities 🔴
|
||||
|
||||
#### Direct Template Literal Interpolation
|
||||
- **Location**: `backend/src/routes/adminDashboard.js:214,221,227,252,269`
|
||||
- **Severity**: HIGH
|
||||
- **Impact**: Potential database compromise
|
||||
```javascript
|
||||
.whereRaw(`timestamp >= datetime("now", "-${days} days")`) // VULNERABLE
|
||||
```
|
||||
|
||||
#### LIKE Query Injection
|
||||
- **Locations**:
|
||||
- `backend/src/routes/adminPhotos.js:476`
|
||||
- `backend/src/routes/adminEvents.js:156-158`
|
||||
- **Severity**: MEDIUM
|
||||
- **Impact**: Query manipulation through special characters
|
||||
|
||||
### 3. Authentication & Authorization Flaws 🔴
|
||||
|
||||
#### Missing Token Type Validation
|
||||
- **Location**: Admin middleware
|
||||
- **Severity**: HIGH
|
||||
- **Impact**: Gallery tokens could potentially access admin endpoints
|
||||
|
||||
#### Weak Password Requirements
|
||||
- **Current**: Only 6 characters minimum
|
||||
- **Severity**: MEDIUM
|
||||
- **Impact**: Vulnerable to brute force attacks
|
||||
|
||||
#### Rate Limiting Bypass
|
||||
- **Location**: `backend/server.js:57-73`
|
||||
- **Severity**: HIGH
|
||||
- **Impact**: Invalid JWT tokens bypass rate limiting
|
||||
|
||||
### 4. Cross-Site Scripting (XSS) 🔴
|
||||
|
||||
#### Stored XSS in CMS
|
||||
- **Location**: `frontend/src/pages/public/LegalPage.tsx:106`
|
||||
- **Severity**: CRITICAL
|
||||
- **Impact**: Malicious scripts execute for all visitors
|
||||
```tsx
|
||||
dangerouslySetInnerHTML={{ __html: page.content }} // VULNERABLE
|
||||
```
|
||||
|
||||
### 5. File Upload Vulnerabilities 🟡
|
||||
|
||||
#### Path Traversal Risk
|
||||
- **Location**: `backend/server.js:104-110`
|
||||
- **Severity**: HIGH
|
||||
- **Impact**: Access to files outside intended directories
|
||||
|
||||
#### Insufficient MIME Type Validation
|
||||
- **Multiple locations**
|
||||
- **Severity**: MEDIUM
|
||||
- **Impact**: Malicious file upload bypass
|
||||
|
||||
### 6. Security Headers & Configuration 🟡
|
||||
|
||||
#### Missing Critical Headers
|
||||
- **Missing**: CSP, X-Frame-Options, Strict-Transport-Security
|
||||
- **Severity**: MEDIUM
|
||||
- **Impact**: Reduced defense against various attacks
|
||||
|
||||
#### Permissive CORS Configuration
|
||||
- **Location**: `backend/server.js:30-49`
|
||||
- **Severity**: MEDIUM
|
||||
- **Impact**: Allows multiple origins including localhost
|
||||
|
||||
## Dependency Analysis
|
||||
|
||||
### NPM Audit Results ✅
|
||||
- **Backend**: 0 vulnerabilities found
|
||||
- **Frontend**: 0 vulnerabilities found
|
||||
- **Status**: All dependencies are up to date
|
||||
|
||||
## Detailed Findings by Category
|
||||
|
||||
### Authentication Security
|
||||
|
||||
1. **JWT Implementation Issues**:
|
||||
- No refresh token mechanism
|
||||
- 24-hour token expiration for all types
|
||||
- No token revocation capability
|
||||
- Hardcoded fallback secret
|
||||
|
||||
2. **Session Management**:
|
||||
- In-memory session storage (not scalable)
|
||||
- No Redis implementation despite comments
|
||||
- Incomplete session cleanup
|
||||
|
||||
3. **Password Security**:
|
||||
- Weak requirements (6 chars minimum)
|
||||
- Fixed bcrypt rounds (10)
|
||||
- No password complexity requirements
|
||||
- No breach checking
|
||||
|
||||
### Data Security
|
||||
|
||||
1. **SQL Injection Risks**:
|
||||
- Template literal interpolation in whereRaw()
|
||||
- Unescaped LIKE queries
|
||||
- Missing input validation on some parameters
|
||||
|
||||
2. **XSS Vulnerabilities**:
|
||||
- Stored XSS in CMS content
|
||||
- No Content Security Policy
|
||||
- Missing output encoding in some areas
|
||||
|
||||
3. **Information Disclosure**:
|
||||
- Detailed error messages exposed
|
||||
- Console.error statements with sensitive data
|
||||
- No audit logging for security events
|
||||
|
||||
### Infrastructure Security
|
||||
|
||||
1. **File Upload Issues**:
|
||||
- Path traversal vulnerability
|
||||
- Weak MIME type validation
|
||||
- No virus scanning
|
||||
- Missing content validation
|
||||
|
||||
2. **Network Security**:
|
||||
- Missing security headers
|
||||
- Permissive CORS policy
|
||||
- No HTTPS enforcement
|
||||
- Rate limiting can be bypassed
|
||||
|
||||
## Recommended Fixes
|
||||
|
||||
### Priority 1: Critical (Implement Immediately)
|
||||
|
||||
1. **Remove Hardcoded Secrets**
|
||||
```javascript
|
||||
// Replace fallback with error
|
||||
const secret = process.env.JWT_SECRET;
|
||||
if (!secret) {
|
||||
throw new Error('JWT_SECRET environment variable is required');
|
||||
}
|
||||
```
|
||||
|
||||
2. **Fix SQL Injection**
|
||||
```javascript
|
||||
// Use parameterized queries
|
||||
.whereRaw('timestamp >= datetime("now", ? || " days")', [`-${days}`])
|
||||
```
|
||||
|
||||
3. **Sanitize CMS Content**
|
||||
```javascript
|
||||
import DOMPurify from 'dompurify';
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(page.content) }}
|
||||
```
|
||||
|
||||
### Priority 2: High (Implement Within 1 Week)
|
||||
|
||||
1. **Add Token Type Validation**
|
||||
```javascript
|
||||
if (decoded.type !== 'admin') {
|
||||
return res.status(401).json({ error: 'Invalid token type' });
|
||||
}
|
||||
```
|
||||
|
||||
2. **Implement Security Headers**
|
||||
```javascript
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
},
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
3. **Fix Rate Limiting Bypass**
|
||||
```javascript
|
||||
// Check token validity before skipping rate limit
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
return decoded && decoded.type === 'admin';
|
||||
} catch (err) {
|
||||
return false; // Apply rate limiting on invalid tokens
|
||||
}
|
||||
```
|
||||
|
||||
### Priority 3: Medium (Implement Within 1 Month)
|
||||
|
||||
1. **Enhance Password Security**
|
||||
- Minimum 12 characters
|
||||
- Complexity requirements
|
||||
- Breach checking integration
|
||||
|
||||
2. **Implement File Security**
|
||||
- Content-based validation
|
||||
- Path traversal protection
|
||||
- Virus scanning
|
||||
|
||||
3. **Add Security Monitoring**
|
||||
- Audit logging
|
||||
- Failed login tracking
|
||||
- Anomaly detection
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Remove all hardcoded secrets
|
||||
- [ ] Fix SQL injection vulnerabilities
|
||||
- [ ] Add XSS protection (DOMPurify)
|
||||
- [ ] Implement proper token validation
|
||||
- [ ] Add all security headers
|
||||
- [ ] Fix rate limiting bypass
|
||||
- [ ] Enhance password requirements
|
||||
- [ ] Add file upload security
|
||||
- [ ] Implement audit logging
|
||||
- [ ] Set up security monitoring
|
||||
- [ ] Document security procedures
|
||||
- [ ] Conduct penetration testing
|
||||
|
||||
## Conclusion
|
||||
|
||||
The PicPeak platform has significant security vulnerabilities that need immediate attention. The most critical issues are hardcoded secrets, SQL injection risks, and stored XSS vulnerabilities. While the codebase shows some security awareness (bcrypt hashing, JWT usage, input validation), the implementation has serious flaws that could lead to system compromise.
|
||||
|
||||
**Recommended Action**: Address all critical vulnerabilities immediately before deploying to production. Consider a professional security audit after implementing these fixes.
|
||||
|
||||
---
|
||||
*Generated by Claude Code Security Scanner*
|
||||
*Scan completed: 2025-01-12*
|
||||
+7
-33
@@ -1,45 +1,19 @@
|
||||
# Build stage
|
||||
FROM node:18-alpine AS builder
|
||||
# Dockerfile.dev - Development configuration for frontend
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Accept build arguments
|
||||
ARG VITE_API_URL
|
||||
ARG VITE_UMAMI_URL
|
||||
ARG VITE_UMAMI_WEBSITE_ID
|
||||
|
||||
# Set environment variables for build
|
||||
ENV VITE_API_URL=$VITE_API_URL
|
||||
ENV VITE_UMAMI_URL=$VITE_UMAMI_URL
|
||||
ENV VITE_UMAMI_WEBSITE_ID=$VITE_UMAMI_WEBSITE_ID
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --legacy-peer-deps
|
||||
|
||||
# Copy source files
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
# Expose the development server port
|
||||
EXPOSE 3005
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy custom nginx config
|
||||
COPY nginx.dev.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Copy built application from builder stage
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost/health || exit 1
|
||||
|
||||
# Expose port
|
||||
EXPOSE 80
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
# Start development server
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "3005"]
|
||||
@@ -1,69 +0,0 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.93",
|
||||
"version": "1.0.99",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.93",
|
||||
"version": "1.0.99",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@tiptap/extension-character-count": "^2.26.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.93",
|
||||
"version": "1.0.99",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -23,7 +23,7 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
const [selectedPhotos, setSelectedPhotos] = useState<Set<number>>(new Set());
|
||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [deletingPhotoId, setDeletingPhotoId] = useState<number | null>(null);
|
||||
const [deletingPhotos, setDeletingPhotos] = useState<Set<number>>(new Set());
|
||||
|
||||
const handlePhotoSelect = (photoId: number, e?: React.MouseEvent) => {
|
||||
if (e) {
|
||||
@@ -54,15 +54,18 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingPhotoId(photo.id);
|
||||
setDeletingPhotos(prev => new Set(prev).add(photo.id));
|
||||
try {
|
||||
await photosService.deletePhoto(eventId, photo.id);
|
||||
toast.success('Photo deleted successfully');
|
||||
onPhotosDeleted();
|
||||
} catch (error) {
|
||||
toast.error('Failed to delete photo');
|
||||
} finally {
|
||||
setDeletingPhotoId(null);
|
||||
setDeletingPhotos(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(photo.id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,14 +78,18 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
const selectedIds = Array.from(selectedPhotos);
|
||||
setDeletingPhotos(new Set(selectedIds));
|
||||
|
||||
try {
|
||||
await photosService.deletePhotos(eventId, Array.from(selectedPhotos));
|
||||
await photosService.deletePhotos(eventId, selectedIds);
|
||||
toast.success(`${count} photo${count > 1 ? 's' : ''} deleted successfully`);
|
||||
setSelectedPhotos(new Set());
|
||||
setIsSelectionMode(false);
|
||||
onPhotosDeleted();
|
||||
} catch (error) {
|
||||
toast.error('Failed to delete photos');
|
||||
setDeletingPhotos(new Set());
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
@@ -155,13 +162,15 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
|
||||
{/* Photo Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||
{photos.map((photo, index) => (
|
||||
<div
|
||||
key={photo.id}
|
||||
className={`relative group cursor-pointer rounded-lg overflow-hidden bg-neutral-100 ${
|
||||
isSelectionMode ? 'ring-2 ring-offset-2 ' + (selectedPhotos.has(photo.id) ? 'ring-primary-500' : 'ring-transparent') : ''
|
||||
}`}
|
||||
onClick={() => isSelectionMode ? handlePhotoSelect(photo.id) : onPhotoClick(photo, index)}
|
||||
{photos.map((photo, index) => {
|
||||
const isDeleting = deletingPhotos.has(photo.id);
|
||||
return (
|
||||
<div
|
||||
key={photo.id}
|
||||
className={`relative group cursor-pointer rounded-lg overflow-hidden bg-neutral-100 transition-opacity ${
|
||||
isSelectionMode ? 'ring-2 ring-offset-2 ' + (selectedPhotos.has(photo.id) ? 'ring-primary-500' : 'ring-transparent') : ''
|
||||
} ${isDeleting ? 'opacity-50' : ''}`}
|
||||
onClick={() => !isDeleting && (isSelectionMode ? handlePhotoSelect(photo.id) : onPhotoClick(photo, index))}
|
||||
>
|
||||
{/* Selection Checkbox */}
|
||||
{isSelectionMode && (
|
||||
@@ -219,8 +228,8 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => handleDeleteSingle(photo, e)}
|
||||
className="p-1 text-white hover:bg-white/20 rounded"
|
||||
disabled={deletingPhotoId === photo.id}
|
||||
className="p-1 text-white hover:bg-white/20 rounded disabled:opacity-50"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
@@ -238,7 +247,8 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{photos.length === 0 && (
|
||||
|
||||
@@ -256,7 +256,7 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
|
||||
<div className="flex justify-between text-sm text-neutral-600 mb-1">
|
||||
<span>
|
||||
{t('upload.uploading')}
|
||||
{totalChunks > 1 && ` (${t('common.chunk') || 'Chunk'} ${currentChunk}/${totalChunks})`}
|
||||
{totalChunks > 1 && ` (${t('common.chunk')} ${currentChunk}/${totalChunks})`}
|
||||
</span>
|
||||
<span>{uploadProgress}%</span>
|
||||
</div>
|
||||
@@ -268,7 +268,7 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
|
||||
</div>
|
||||
{totalChunks > 1 && (
|
||||
<p className="text-xs text-neutral-500 mt-1">
|
||||
{t('upload.uploadingChunks') || `Uploading ${selectedFiles.length} files in ${totalChunks} batches...`}
|
||||
{t('upload.uploadingChunks', { count: selectedFiles.length, total: totalChunks })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"days": "Tage",
|
||||
"customize": "Anpassen",
|
||||
"hide": "Ausblenden",
|
||||
"unknown": "Unbekannt"
|
||||
"unknown": "Unbekannt",
|
||||
"chunk": "Teil"
|
||||
},
|
||||
"upload": {
|
||||
"photoCategory": "Fotokategorie",
|
||||
@@ -51,7 +52,8 @@
|
||||
"uploadPhotos": "Fotos hochladen",
|
||||
"maxFilesReached": "Maximal 500 Dateien erlaubt",
|
||||
"someFilesSkipped": "Einige Dateien wurden übersprungen (500 Dateien Limit)",
|
||||
"tooManyFiles": "Maximal 500 Dateien können gleichzeitig hochgeladen werden"
|
||||
"tooManyFiles": "Maximal 500 Dateien können gleichzeitig hochgeladen werden",
|
||||
"uploadingChunks": "Lade {{count}} Dateien in {{total}} Teilen hoch..."
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Dashboard",
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"days": "days",
|
||||
"customize": "Customize",
|
||||
"hide": "Hide",
|
||||
"unknown": "Unknown"
|
||||
"unknown": "Unknown",
|
||||
"chunk": "Chunk"
|
||||
},
|
||||
"upload": {
|
||||
"photoCategory": "Photo Category",
|
||||
@@ -51,7 +52,8 @@
|
||||
"uploadPhotos": "Upload Photos",
|
||||
"maxFilesReached": "Maximum 500 files allowed",
|
||||
"someFilesSkipped": "Some files were skipped (500 file limit)",
|
||||
"tooManyFiles": "Maximum 500 files can be uploaded at once"
|
||||
"tooManyFiles": "Maximum 500 files can be uploaded at once",
|
||||
"uploadingChunks": "Uploading {{count}} files in {{total}} batches..."
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Dashboard",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user