Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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*
|
||||
@@ -0,0 +1,40 @@
|
||||
# Development Environment with PostgreSQL
|
||||
# Copy this to .env for PostgreSQL development with Docker Compose
|
||||
|
||||
# JWT Secret (development only)
|
||||
JWT_SECRET=dev-secret-key-do-not-use-in-production
|
||||
|
||||
# Database Configuration (PostgreSQL)
|
||||
DATABASE_CLIENT=pg
|
||||
DB_USER=picpeak_dev
|
||||
DB_PASSWORD=dev_password_123
|
||||
DB_NAME=picpeak_dev
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_PASSWORD=dev_redis_pass
|
||||
|
||||
# Admin Account (initial setup)
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=admin@localhost
|
||||
|
||||
# Email Configuration (Disabled for development)
|
||||
# To enable email, configure a real SMTP server
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
EMAIL_FROM=noreply@picpeak.local
|
||||
|
||||
# Application URLs
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
ADMIN_URL=http://localhost:3001
|
||||
VITE_API_URL=http://localhost:3001/api
|
||||
|
||||
# Timezone
|
||||
TZ=UTC
|
||||
|
||||
# Analytics (Optional - leave empty for development)
|
||||
VITE_UMAMI_URL=
|
||||
VITE_UMAMI_WEBSITE_ID=
|
||||
VITE_UMAMI_SHARE_URL=
|
||||
+26
-8
@@ -13,11 +13,16 @@ ADMIN_URL=http://localhost:3005
|
||||
FRONTEND_URL=http://localhost:3005
|
||||
BACKEND_URL=http://localhost:3001
|
||||
|
||||
# Database Configuration (SQLite for development)
|
||||
DATABASE_CLIENT=sqlite3
|
||||
DATABASE_PATH=./data/photo_sharing.db
|
||||
# Database Configuration (PostgreSQL for development)
|
||||
DATABASE_CLIENT=pg
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
DB_NAME=picpeak
|
||||
DB_USER=picpeak
|
||||
DB_PASSWORD=picpeak
|
||||
|
||||
# Email Configuration (Mailhog for development)
|
||||
# Email Configuration
|
||||
# For development with docker-compose.dev.yml:
|
||||
# Access Mailhog UI at: http://localhost:8025
|
||||
SMTP_HOST=mailhog
|
||||
SMTP_PORT=1025
|
||||
@@ -26,6 +31,14 @@ SMTP_USER=
|
||||
SMTP_PASS=
|
||||
EMAIL_FROM=noreply@localhost
|
||||
|
||||
# For development without Docker, use real SMTP:
|
||||
# SMTP_HOST=smtp.gmail.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_SECURE=false
|
||||
# SMTP_USER=your-email@gmail.com
|
||||
# SMTP_PASS=your-app-password
|
||||
# EMAIL_FROM=PicPeak Dev <your-email@gmail.com>
|
||||
|
||||
# Backend Port Configuration
|
||||
PORT=3001
|
||||
|
||||
@@ -40,8 +53,13 @@ PORT=3001
|
||||
NODE_ENV=development
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# Storage Settings (optional)
|
||||
DEFAULT_EXPIRATION_DAYS=30
|
||||
WARNING_DAYS_BEFORE_EXPIRY=7
|
||||
|
||||
# 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)
|
||||
# 1. Run 'docker-compose -f docker-compose.dev.yml up -d'
|
||||
# 2. Run 'docker-compose -f docker-compose.dev.yml exec backend npm run migrate'
|
||||
# 3. Admin credentials will be auto-generated
|
||||
# 4. Check backend/ADMIN_CREDENTIALS.txt for login details
|
||||
# 5. Change password on first login (required)
|
||||
@@ -20,7 +20,7 @@ ADMIN_URL=https://your-domain.com
|
||||
|
||||
# PostgreSQL Configuration (Recommended for production)
|
||||
DATABASE_CLIENT=pg
|
||||
DB_HOST=postgres # or your database host
|
||||
DB_HOST=db # Use 'db' for Docker Compose, or external host
|
||||
DB_PORT=5432
|
||||
DB_NAME=picpeak
|
||||
DB_USER=picpeak
|
||||
@@ -61,7 +61,7 @@ EMAIL_FROM=PicPeak <noreply@your-domain.com>
|
||||
# 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
|
||||
# UMAMI_HASH_SALT=your-hash-salt # Required if using Umami
|
||||
|
||||
# Frontend Analytics (Optional - Fallback values)
|
||||
# VITE_UMAMI_URL=https://analytics.your-domain.com
|
||||
@@ -76,6 +76,13 @@ NODE_ENV=production
|
||||
PORT=3001
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Backend URL (if different from frontend)
|
||||
# BACKEND_URL=https://api.your-domain.com
|
||||
|
||||
# Storage Settings
|
||||
DEFAULT_EXPIRATION_DAYS=30
|
||||
WARNING_DAYS_BEFORE_EXPIRY=7
|
||||
|
||||
# Security Settings (Defaults are secure)
|
||||
BCRYPT_ROUNDS=12
|
||||
SESSION_TIMEOUT_MINUTES=60
|
||||
|
||||
@@ -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
|
||||
@@ -28,25 +28,14 @@ jobs:
|
||||
|
||||
# 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"
|
||||
|
||||
|
||||
@@ -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,778 @@
|
||||
# 🚀 PicPeak Complete Deployment Guide
|
||||
|
||||
This comprehensive guide covers all deployment methods for PicPeak, including Docker, PM2, manual installation, and deployment without a reverse proxy.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Security Requirements](#security-requirements)
|
||||
- [Quick Start (Docker)](#quick-start-docker)
|
||||
- [Deployment Methods](#deployment-methods)
|
||||
- [Method 1: Docker Compose (Recommended)](#method-1-docker-compose-recommended)
|
||||
- [Method 2: PM2 (Node.js Process Manager)](#method-2-pm2-nodejs-process-manager)
|
||||
- [Method 3: Manual Installation](#method-3-manual-installation)
|
||||
- [Method 4: Without Nginx (Direct Access)](#method-4-without-nginx-direct-access)
|
||||
- [Environment Configuration](#environment-configuration)
|
||||
- [Admin Setup](#admin-setup)
|
||||
- [SSL/HTTPS Configuration](#sslhttps-configuration)
|
||||
- [Maintenance & Operations](#maintenance--operations)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Security Checklist](#security-checklist)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Basic Requirements
|
||||
- Linux server (Ubuntu 20.04+ or similar)
|
||||
- Domain name (for SSL certificates)
|
||||
- SMTP credentials for email notifications
|
||||
- Basic command line knowledge
|
||||
|
||||
### Software Requirements (varies by method)
|
||||
- **Docker method**: Docker and Docker Compose
|
||||
- **PM2 method**: Node.js 18+, PostgreSQL 14+
|
||||
- **Manual method**: Node.js 18+, PostgreSQL 14+, nginx (optional)
|
||||
|
||||
### Development Setup
|
||||
For local development, use `docker-compose.dev.yml` which includes Mailhog for email testing:
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
### Production Customization
|
||||
For local production customizations, copy `docker-compose.override.yml.example` to `docker-compose.override.yml`:
|
||||
```bash
|
||||
cp docker-compose.override.yml.example docker-compose.override.yml
|
||||
# Edit docker-compose.override.yml with your customizations
|
||||
```
|
||||
|
||||
## 🔐 Security Requirements
|
||||
|
||||
### Critical: JWT Secret Setup
|
||||
|
||||
**NEVER use the default JWT secret in production!** The application will refuse to start if JWT_SECRET is not properly configured.
|
||||
|
||||
Generate a secure JWT secret:
|
||||
```bash
|
||||
# Generate a 64-character secret
|
||||
openssl rand -base64 32
|
||||
|
||||
# Or for even more security (recommended)
|
||||
openssl rand -base64 64
|
||||
|
||||
# Or use the included script
|
||||
./scripts/generate-jwt-secret.sh
|
||||
```
|
||||
|
||||
### Critical: Database Password
|
||||
|
||||
Generate a strong database password:
|
||||
```bash
|
||||
openssl rand -base64 24
|
||||
```
|
||||
|
||||
## 🚀 Quick Start (Docker)
|
||||
|
||||
The fastest way to deploy PicPeak in production:
|
||||
|
||||
```bash
|
||||
# 1. Clone the repository
|
||||
git clone https://github.com/the-luap/picpeak.git
|
||||
cd picpeak
|
||||
|
||||
# 2. Use the automated install script (recommended)
|
||||
sudo ./scripts/install.sh
|
||||
|
||||
# Or manually:
|
||||
# 2. Copy production environment template
|
||||
cp .env.production.example .env
|
||||
|
||||
# 3. Generate and add JWT secret
|
||||
echo "JWT_SECRET=$(openssl rand -base64 32)" >> .env
|
||||
|
||||
# 4. Edit configuration
|
||||
nano .env # Update all required values
|
||||
|
||||
# 5. Create directories
|
||||
mkdir -p storage/events/active storage/events/archived storage/thumbnails storage/uploads
|
||||
mkdir -p data logs certbot/conf certbot/www
|
||||
|
||||
# 6. Deploy
|
||||
docker-compose up -d
|
||||
|
||||
# 7. Check logs
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
## 📦 Deployment Methods
|
||||
|
||||
### Method 1: Docker Compose (Recommended)
|
||||
|
||||
#### Step 1: Environment Configuration
|
||||
|
||||
Create `.env` file with all required variables:
|
||||
|
||||
```env
|
||||
# SECURITY - MUST CHANGE ALL!
|
||||
JWT_SECRET=<your-64-character-secret-from-openssl>
|
||||
DB_PASSWORD=<your-secure-database-password>
|
||||
|
||||
# Application URLs
|
||||
FRONTEND_URL=https://your-domain.com
|
||||
BACKEND_URL=https://your-domain.com
|
||||
ADMIN_URL=https://your-domain.com
|
||||
|
||||
# Database (PostgreSQL for Docker)
|
||||
DATABASE_CLIENT=pg
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
DB_NAME=picpeak
|
||||
DB_USER=picpeak
|
||||
|
||||
# Email (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>
|
||||
|
||||
# Application
|
||||
NODE_ENV=production
|
||||
PORT=3001
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Backend URL (if different from frontend)
|
||||
# BACKEND_URL=https://api.your-domain.com
|
||||
```
|
||||
|
||||
#### Step 2: Docker Volume Permissions
|
||||
|
||||
Create `docker-compose.override.yml` for proper permissions:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
#### Step 3: Build and Deploy
|
||||
|
||||
```bash
|
||||
# Set correct permissions
|
||||
chmod -R 755 storage data logs
|
||||
|
||||
# Build images
|
||||
docker-compose build
|
||||
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
|
||||
# Run database migrations
|
||||
docker-compose exec backend npm run migrate
|
||||
|
||||
# Admin credentials will be displayed and saved to /data/ADMIN_CREDENTIALS.txt
|
||||
```
|
||||
|
||||
#### Step 4: Configure Nginx
|
||||
|
||||
Update `nginx/sites-enabled/default` with your domain:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# Redirect to HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-domain.com;
|
||||
|
||||
# SSL configuration (managed by Certbot)
|
||||
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://frontend:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# API proxy
|
||||
location /api {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Protected images
|
||||
location /photos {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Thumbnails
|
||||
location /thumbnails {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Public uploads
|
||||
location /uploads {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Method 2: PM2 (Node.js Process Manager)
|
||||
|
||||
#### Step 1: Install Dependencies
|
||||
|
||||
```bash
|
||||
# Install Node.js 18+
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
|
||||
# Install PostgreSQL
|
||||
sudo apt-get install -y postgresql postgresql-contrib
|
||||
|
||||
# Install PM2 globally
|
||||
sudo npm install -g pm2
|
||||
|
||||
# Install nginx (if using reverse proxy)
|
||||
sudo apt-get install -y nginx
|
||||
```
|
||||
|
||||
#### Step 2: Setup Database
|
||||
|
||||
```bash
|
||||
# Create database and user
|
||||
sudo -u postgres psql
|
||||
CREATE DATABASE picpeak;
|
||||
CREATE USER picpeak WITH ENCRYPTED PASSWORD 'your-secure-password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE picpeak TO picpeak;
|
||||
\q
|
||||
```
|
||||
|
||||
#### Step 3: Clone and Configure
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/the-luap/picpeak.git
|
||||
cd picpeak
|
||||
|
||||
# Install dependencies
|
||||
cd backend && npm install
|
||||
cd ../frontend && npm install
|
||||
|
||||
# Configure environment
|
||||
cd ..
|
||||
cp .env.production.example .env
|
||||
nano .env # Update all values
|
||||
```
|
||||
|
||||
#### Step 4: Build Frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run build
|
||||
cd ..
|
||||
```
|
||||
|
||||
#### Step 5: Start with PM2
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# Start application
|
||||
pm2 start ecosystem.config.js
|
||||
|
||||
# Save PM2 configuration
|
||||
pm2 save
|
||||
|
||||
# Setup startup script
|
||||
pm2 startup
|
||||
```
|
||||
|
||||
#### Step 6: Configure Nginx
|
||||
|
||||
Create `/etc/nginx/sites-available/picpeak`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# Frontend (static files)
|
||||
location / {
|
||||
root /path/to/picpeak/frontend/dist;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API proxy
|
||||
location /api {
|
||||
proxy_pass http://localhost:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# Protected photos
|
||||
location /photos {
|
||||
proxy_pass http://localhost:3001;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Other proxied paths
|
||||
location ~ ^/(thumbnails|uploads) {
|
||||
proxy_pass http://localhost:3001;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enable the site:
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/picpeak /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl restart nginx
|
||||
```
|
||||
|
||||
### Method 3: Manual Installation
|
||||
|
||||
Similar to PM2 method but using systemd instead:
|
||||
|
||||
#### Create Systemd Service
|
||||
|
||||
Create `/etc/systemd/system/picpeak.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=PicPeak Photo Sharing
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=picpeak
|
||||
WorkingDirectory=/home/picpeak/picpeak/backend
|
||||
ExecStart=/usr/bin/node server.js
|
||||
Restart=on-failure
|
||||
Environment="NODE_ENV=production"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Start the service:
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable picpeak
|
||||
sudo systemctl start picpeak
|
||||
```
|
||||
|
||||
### Method 4: Without Nginx (Direct Access)
|
||||
|
||||
For deployments without a reverse proxy:
|
||||
|
||||
#### Option A: Direct Backend Access
|
||||
|
||||
1. **Configure environment for direct access**:
|
||||
```env
|
||||
# .env
|
||||
FRONTEND_URL=http://your-domain.com:5173
|
||||
BACKEND_URL=http://your-domain.com:3001
|
||||
ADMIN_URL=http://your-domain.com:5173
|
||||
|
||||
# Enable CORS for direct access
|
||||
CORS_ENABLED=true
|
||||
```
|
||||
|
||||
2. **Run backend directly**:
|
||||
```bash
|
||||
cd backend
|
||||
NODE_ENV=production node server.js
|
||||
```
|
||||
|
||||
3. **Run frontend development server** (not recommended for production):
|
||||
```bash
|
||||
cd frontend
|
||||
VITE_API_URL=http://your-domain.com:3001/api npm run dev -- --host
|
||||
```
|
||||
|
||||
#### Option B: Backend Serves Frontend
|
||||
|
||||
1. **Build frontend**:
|
||||
```bash
|
||||
cd frontend
|
||||
VITE_API_URL=/api npm run build
|
||||
```
|
||||
|
||||
2. **Configure backend to serve frontend**:
|
||||
```javascript
|
||||
// Add to backend/server.js after API routes
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.use(express.static(path.join(__dirname, '../frontend/dist')));
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. **Access everything on backend port**:
|
||||
```bash
|
||||
# Application available at http://your-domain.com:3001
|
||||
NODE_ENV=production node server.js
|
||||
```
|
||||
|
||||
#### Option C: Using Node.js HTTP Proxy
|
||||
|
||||
Create a simple proxy server:
|
||||
|
||||
```javascript
|
||||
// proxy-server.js
|
||||
const express = require('express');
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Serve frontend static files
|
||||
app.use(express.static(path.join(__dirname, 'frontend/dist')));
|
||||
|
||||
// Proxy API requests
|
||||
app.use('/api', createProxyMiddleware({
|
||||
target: 'http://localhost:3001',
|
||||
changeOrigin: true
|
||||
}));
|
||||
|
||||
// Proxy other backend routes
|
||||
app.use(['/photos', '/thumbnails', '/uploads'], createProxyMiddleware({
|
||||
target: 'http://localhost:3001',
|
||||
changeOrigin: true
|
||||
}));
|
||||
|
||||
// Catch all - serve frontend
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'frontend/dist/index.html'));
|
||||
});
|
||||
|
||||
app.listen(80);
|
||||
```
|
||||
|
||||
## 🔧 Environment Configuration
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `JWT_SECRET` | **CRITICAL** - Authentication secret (min 32 chars) | Use `openssl rand -base64 32` |
|
||||
| `DATABASE_CLIENT` | Database type | `pg` for PostgreSQL, `sqlite3` for SQLite |
|
||||
| `DB_HOST` | Database host | `localhost` or `db` (Docker) |
|
||||
| `DB_PORT` | Database port | `5432` |
|
||||
| `DB_NAME` | Database name | `picpeak` |
|
||||
| `DB_USER` | Database user | `picpeak` |
|
||||
| `DB_PASSWORD` | Database password | Strong password |
|
||||
| `SMTP_HOST` | Email server | `smtp.gmail.com` |
|
||||
| `SMTP_PORT` | Email port | `587` |
|
||||
| `SMTP_USER` | Email username | `your-email@gmail.com` |
|
||||
| `SMTP_PASS` | Email password | App-specific password |
|
||||
| `EMAIL_FROM` | From address | `PicPeak <noreply@domain.com>` |
|
||||
| `FRONTEND_URL` | Frontend URL | `https://your-domain.com` |
|
||||
| `BACKEND_URL` | Backend URL | `https://your-domain.com` |
|
||||
| `ADMIN_URL` | Admin panel URL | `https://your-domain.com` |
|
||||
|
||||
### Optional Configuration
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `NODE_ENV` | Environment | `production` |
|
||||
| `PORT` | Backend port | `3001` |
|
||||
| `LOG_LEVEL` | Logging level | `info` |
|
||||
| `SESSION_TIMEOUT_MINUTES` | Session timeout | `60` |
|
||||
| `RATE_LIMIT_WINDOW_MS` | Rate limit window | `900000` (15 min) |
|
||||
| `RATE_LIMIT_MAX_REQUESTS` | Max requests | `100` |
|
||||
| `DB_POOL_MIN` | Min DB connections | `5` |
|
||||
| `DB_POOL_MAX` | Max DB connections | `25` |
|
||||
| `DEFAULT_EXPIRATION_DAYS` | Gallery expiration | `30` |
|
||||
| `WARNING_DAYS_BEFORE_EXPIRY` | Warning period | `7` |
|
||||
|
||||
### Frontend Environment
|
||||
|
||||
For production builds:
|
||||
```bash
|
||||
# frontend/.env.production
|
||||
VITE_API_URL=/api # For reverse proxy
|
||||
# or
|
||||
VITE_API_URL=https://api.your-domain.com # For direct access
|
||||
```
|
||||
|
||||
## 👤 Admin Setup
|
||||
|
||||
### Automatic Admin Creation
|
||||
|
||||
When you run migrations for the first time, an admin account is automatically created:
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
docker-compose exec backend npm run migrate
|
||||
|
||||
# PM2/Manual
|
||||
cd backend && npm run migrate
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
========================================
|
||||
✅ Admin user created successfully!
|
||||
========================================
|
||||
Username: admin
|
||||
Password: SwiftEagle3847!
|
||||
|
||||
⚠️ IMPORTANT: Change password on first login
|
||||
========================================
|
||||
```
|
||||
|
||||
### Important Admin Notes
|
||||
|
||||
1. **Credentials are saved** to `backend/ADMIN_CREDENTIALS.txt`
|
||||
2. **Must change password** on first login (enforced)
|
||||
3. **Password requirements**:
|
||||
- Minimum 12 characters
|
||||
- Uppercase and lowercase letters
|
||||
- Numbers and special characters
|
||||
- Not a common password
|
||||
|
||||
### Lost Admin Password
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
docker-compose exec backend node scripts/reset-admin-password.js
|
||||
|
||||
# PM2/Manual
|
||||
cd backend && node scripts/reset-admin-password.js
|
||||
```
|
||||
|
||||
## 🔒 SSL/HTTPS Configuration
|
||||
|
||||
### Option 1: Let's Encrypt with Certbot
|
||||
|
||||
```bash
|
||||
# Initial certificate
|
||||
docker-compose run --rm certbot certonly \
|
||||
--webroot --webroot-path=/var/www/certbot \
|
||||
-d your-domain.com -d www.your-domain.com
|
||||
|
||||
# Auto-renewal is handled by certbot container
|
||||
```
|
||||
|
||||
### Option 2: Using Traefik
|
||||
|
||||
Add to `docker-compose.override.yml`:
|
||||
|
||||
```yaml
|
||||
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"
|
||||
```
|
||||
|
||||
### Option 3: CloudFlare or Other CDN
|
||||
|
||||
1. Set up your domain in CloudFlare
|
||||
2. Enable "Full SSL/TLS encryption mode"
|
||||
3. Use CloudFlare's origin certificates
|
||||
|
||||
## 🔧 Maintenance & Operations
|
||||
|
||||
### Backup Procedures
|
||||
|
||||
Use the included backup script or create your own:
|
||||
|
||||
```bash
|
||||
# Use the provided backup script
|
||||
./scripts/backup.sh
|
||||
|
||||
# Or create custom backup script:
|
||||
#!/bin/bash
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR="./backups/$DATE"
|
||||
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Database backup
|
||||
docker-compose exec -T db \
|
||||
pg_dump -U picpeak picpeak > $BACKUP_DIR/database.sql
|
||||
|
||||
# Files backup
|
||||
tar -czf $BACKUP_DIR/storage.tar.gz storage/
|
||||
|
||||
echo "Backup completed: $BACKUP_DIR"
|
||||
```
|
||||
|
||||
### Automated Backups
|
||||
|
||||
The application includes a built-in backup service. Configure via Admin Panel:
|
||||
- Settings → Backup Configuration
|
||||
- Set schedule (cron expression)
|
||||
- Configure destination (local, rsync, S3)
|
||||
- Enable email notifications
|
||||
|
||||
### Updates
|
||||
|
||||
```bash
|
||||
# Docker method
|
||||
git pull
|
||||
docker-compose build
|
||||
docker-compose up -d
|
||||
|
||||
# PM2 method
|
||||
git pull
|
||||
cd backend && npm install
|
||||
cd ../frontend && npm install && npm run build
|
||||
pm2 restart picpeak
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
#### Health Checks
|
||||
```bash
|
||||
# API health
|
||||
curl https://your-domain.com/api/health
|
||||
|
||||
# Database connection
|
||||
docker-compose exec backend \
|
||||
psql -U picpeak -d picpeak -c "SELECT 1"
|
||||
|
||||
# Service status
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
#### Logs
|
||||
```bash
|
||||
# Docker logs
|
||||
docker-compose logs -f
|
||||
|
||||
# PM2 logs
|
||||
pm2 logs picpeak
|
||||
|
||||
# System logs
|
||||
tail -f /var/log/nginx/error.log
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### JWT Secret Errors
|
||||
**Error**: "Missing required environment variable: JWT_SECRET"
|
||||
- **Solution**: Set JWT_SECRET in your .env file
|
||||
- **Generate**: `openssl rand -base64 32`
|
||||
|
||||
**Error**: "JWT_SECRET is set to the insecure default value"
|
||||
- **Solution**: Change from default to secure value
|
||||
|
||||
#### Database Connection Failed
|
||||
**Error**: "connect ECONNREFUSED"
|
||||
- **Check**: Database is running
|
||||
- **Check**: Correct host/port in .env
|
||||
- **Docker**: Use `db` as host, not `localhost`
|
||||
|
||||
#### Permission Errors
|
||||
**Error**: "EACCES: permission denied"
|
||||
```bash
|
||||
# Fix Docker permissions
|
||||
sudo chown -R 1001:1001 storage data logs
|
||||
|
||||
# Fix PM2/Manual permissions
|
||||
sudo chown -R $USER:$USER storage data logs
|
||||
chmod -R 755 storage
|
||||
```
|
||||
|
||||
#### Email Not Sending
|
||||
- **Check**: SMTP credentials are correct
|
||||
- **Gmail**: Use app-specific password
|
||||
- **Test**: Admin Panel → Settings → Email → Test Email
|
||||
- **Logs**: Check `email_queue` table for errors
|
||||
|
||||
#### Photos Not Appearing
|
||||
- **Check**: File watcher is running
|
||||
- **Permissions**: `chmod -R 755 storage/`
|
||||
- **Logs**: `grep watcher` in backend logs
|
||||
|
||||
#### Frontend Can't Connect to Backend
|
||||
- **CORS**: Ensure FRONTEND_URL matches in backend .env
|
||||
- **Proxy**: Check nginx configuration
|
||||
- **Direct**: Set CORS_ENABLED=true for non-proxy setup
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check all services
|
||||
docker-compose ps
|
||||
|
||||
# Backend shell access
|
||||
docker-compose exec backend sh
|
||||
|
||||
# Database access
|
||||
docker-compose exec db psql -U picpeak
|
||||
|
||||
# Test API
|
||||
curl -I http://localhost:3001/api/health
|
||||
|
||||
# Check disk space
|
||||
df -h storage/
|
||||
|
||||
# View running processes
|
||||
ps aux | grep node
|
||||
```
|
||||
|
||||
## ✅ Security Checklist
|
||||
|
||||
- [ ] **JWT_SECRET** is randomly generated (min 32 chars)
|
||||
- [ ] **Database password** is strong and unique
|
||||
- [ ] **Admin password** changed from auto-generated
|
||||
- [ ] **SSL/HTTPS** enabled and working
|
||||
- [ ] **Firewall** configured (only 80/443 open)
|
||||
- [ ] **File permissions** set correctly (755 for storage)
|
||||
- [ ] **Rate limiting** enabled (default: 100 req/15min)
|
||||
- [ ] **CORS** properly configured
|
||||
- [ ] **Environment files** not in version control
|
||||
- [ ] **Backups** configured and tested
|
||||
- [ ] **Monitoring** alerts set up
|
||||
- [ ] **Updates** scheduled regularly
|
||||
- [ ] **Access logs** being monitored
|
||||
- [ ] **Email** using app-specific passwords
|
||||
- [ ] **Umami analytics** configured (optional)
|
||||
|
||||
## 📞 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?** Check the logs first, then open an issue with:
|
||||
- Deployment method used
|
||||
- Error messages
|
||||
- Relevant log output
|
||||
- Environment (without secrets)
|
||||
@@ -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,34 @@
|
||||
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({
|
||||
await knex('admin_users').insert({
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
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
|
||||
@@ -39,7 +40,7 @@ Username: admin
|
||||
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
|
||||
@@ -50,7 +51,16 @@ 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!');
|
||||
@@ -59,15 +69,14 @@ Generated on: ${new Date().toISOString()}
|
||||
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 +110,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 +125,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.94",
|
||||
"version": "1.0.97",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.94",
|
||||
"version": "1.0.97",
|
||||
"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.94",
|
||||
"version": "1.0.97",
|
||||
"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();
|
||||
@@ -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.
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to clean git history - removes all commits before July 17, 2025
|
||||
# WARNING: This is destructive and will rewrite history!
|
||||
|
||||
set -e
|
||||
|
||||
echo "⚠️ WARNING: This script will permanently rewrite git history!"
|
||||
echo "⚠️ All commits before July 17, 2025 will be removed."
|
||||
echo "⚠️ This action cannot be undone!"
|
||||
echo ""
|
||||
read -p "Are you sure you want to continue? (type 'yes' to proceed): " confirmation
|
||||
|
||||
if [ "$confirmation" != "yes" ]; then
|
||||
echo "Operation cancelled."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create backup branch
|
||||
echo "Creating backup branch..."
|
||||
git checkout -b backup-before-cleanup-$(date +%Y%m%d-%H%M%S)
|
||||
git checkout main
|
||||
|
||||
# Find the first commit on or after July 17, 2025
|
||||
echo "Finding first commit after July 17, 2025..."
|
||||
FIRST_COMMIT=$(git log --since="2025-07-17" --reverse --format="%H" | head -1)
|
||||
|
||||
if [ -z "$FIRST_COMMIT" ]; then
|
||||
echo "ERROR: No commits found after July 17, 2025"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "First commit to keep: $FIRST_COMMIT"
|
||||
echo "Commit details: $(git log --oneline -1 $FIRST_COMMIT)"
|
||||
|
||||
# Get all commits we want to keep
|
||||
COMMITS_TO_KEEP=$(git log --since="2025-07-17" --reverse --format="%H")
|
||||
COMMIT_COUNT=$(echo "$COMMITS_TO_KEEP" | wc -l)
|
||||
echo "Total commits to preserve: $COMMIT_COUNT"
|
||||
|
||||
# Create new orphan branch
|
||||
echo "Creating new clean history..."
|
||||
git checkout --orphan new-main
|
||||
|
||||
# Clean the working directory
|
||||
git rm -rf . || true
|
||||
|
||||
# Get the tree from the first commit
|
||||
git checkout $FIRST_COMMIT -- .
|
||||
|
||||
# Create new initial commit with same content but new message
|
||||
ORIGINAL_MESSAGE=$(git log --format="%B" -1 $FIRST_COMMIT)
|
||||
ORIGINAL_AUTHOR=$(git log --format="%an <%ae>" -1 $FIRST_COMMIT)
|
||||
ORIGINAL_DATE=$(git log --format="%ad" -1 $FIRST_COMMIT)
|
||||
|
||||
GIT_AUTHOR_NAME=$(echo "$ORIGINAL_AUTHOR" | cut -d'<' -f1 | xargs)
|
||||
GIT_AUTHOR_EMAIL=$(echo "$ORIGINAL_AUTHOR" | cut -d'<' -f2 | cut -d'>' -f1)
|
||||
GIT_AUTHOR_DATE="$ORIGINAL_DATE"
|
||||
export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
|
||||
|
||||
git add -A
|
||||
git commit -m "Initial commit - Project start (July 17, 2025)
|
||||
|
||||
Original: $ORIGINAL_MESSAGE"
|
||||
|
||||
# Cherry-pick remaining commits
|
||||
echo "Applying remaining commits..."
|
||||
REMAINING_COMMITS=$(git log --since="2025-07-17" --reverse --format="%H" $FIRST_COMMIT..main)
|
||||
|
||||
if [ -n "$REMAINING_COMMITS" ]; then
|
||||
for commit in $REMAINING_COMMITS; do
|
||||
echo "Applying: $(git log --oneline -1 $commit)"
|
||||
git cherry-pick $commit || {
|
||||
echo "ERROR: Failed to cherry-pick $commit"
|
||||
echo "You may need to resolve conflicts and continue manually"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ New history created successfully!"
|
||||
echo "Total commits in new history: $(git rev-list --count HEAD)"
|
||||
echo ""
|
||||
echo "To finalize the cleanup, run these commands:"
|
||||
echo " git branch -D main"
|
||||
echo " git branch -m main"
|
||||
echo " git push origin main --force"
|
||||
echo ""
|
||||
echo "⚠️ WARNING: Force pushing will overwrite the remote repository!"
|
||||
echo "⚠️ Make sure you have a backup and all team members are aware!"
|
||||
@@ -0,0 +1,130 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: picpeak-backend-dev
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- 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:
|
||||
- "3001:3001"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- picpeak-network
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: picpeak-postgres-dev
|
||||
restart: unless-stopped
|
||||
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:
|
||||
- "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
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: picpeak-redis-dev
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-picpeak_redis_pass}
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
ports:
|
||||
- "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.dev
|
||||
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-dev
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
ports:
|
||||
- "3000:3005"
|
||||
depends_on:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3005"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
networks:
|
||||
- picpeak-network
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
driver: local
|
||||
redis-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
picpeak-network:
|
||||
driver: bridge
|
||||
@@ -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:
|
||||
+87
-54
@@ -1,92 +1,125 @@
|
||||
# docker-compose.yml - Production configuration
|
||||
#
|
||||
# For development, use: docker-compose -f docker-compose.dev.yml up -d
|
||||
# For local customizations, create docker-compose.override.yml (see docker-compose.override.yml.example)
|
||||
#
|
||||
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
|
||||
container_name: picpeak-backend
|
||||
image: picpeak-backend:latest
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
- db
|
||||
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
|
||||
- NODE_ENV=production
|
||||
- PORT=3001
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ADMIN_URL=${ADMIN_URL}
|
||||
- FRONTEND_URL=${FRONTEND_URL}
|
||||
- BACKEND_URL=${BACKEND_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
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- picpeak
|
||||
|
||||
# Frontend
|
||||
frontend:
|
||||
build:
|
||||
image: picpeak-frontend:latest
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
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
|
||||
- VITE_API_URL=/api
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- picpeak
|
||||
|
||||
# Nginx Reverse Proxy
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: picpeak-nginx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./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
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- picpeak
|
||||
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
|
||||
|
||||
# Certbot for SSL
|
||||
certbot:
|
||||
image: certbot/certbot
|
||||
container_name: picpeak-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;'"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
db:
|
||||
image: postgres:15-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:
|
||||
default:
|
||||
name: picpeak-network
|
||||
picpeak:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -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.94",
|
||||
"version": "1.0.97",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.94",
|
||||
"version": "1.0.97",
|
||||
"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.94",
|
||||
"version": "1.0.97",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
const knex = require('knex');
|
||||
|
||||
exports.up = async function(db) {
|
||||
console.log('Adding watermark settings...');
|
||||
|
||||
// Add watermark settings to app_settings table
|
||||
const watermarkSettings = [
|
||||
{
|
||||
setting_key: 'branding_watermark_logo_path',
|
||||
setting_value: JSON.stringify(null),
|
||||
setting_type: 'branding'
|
||||
},
|
||||
{
|
||||
setting_key: 'branding_watermark_logo_url',
|
||||
setting_value: JSON.stringify(null),
|
||||
setting_type: 'branding'
|
||||
},
|
||||
{
|
||||
setting_key: 'branding_watermark_position',
|
||||
setting_value: JSON.stringify('bottom-right'),
|
||||
setting_type: 'branding'
|
||||
},
|
||||
{
|
||||
setting_key: 'branding_watermark_opacity',
|
||||
setting_value: JSON.stringify(50),
|
||||
setting_type: 'branding'
|
||||
},
|
||||
{
|
||||
setting_key: 'branding_watermark_size',
|
||||
setting_value: JSON.stringify(15),
|
||||
setting_type: 'branding'
|
||||
}
|
||||
];
|
||||
|
||||
for (const setting of watermarkSettings) {
|
||||
// Check if setting already exists
|
||||
const existing = await db('app_settings')
|
||||
.where('setting_key', setting.setting_key)
|
||||
.first();
|
||||
|
||||
if (!existing) {
|
||||
await db('app_settings').insert(setting);
|
||||
console.log(`Added setting: ${setting.setting_key}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Watermark settings migration completed');
|
||||
};
|
||||
|
||||
exports.down = async function(db) {
|
||||
// Remove watermark settings
|
||||
await db('app_settings')
|
||||
.whereIn('setting_key', [
|
||||
'branding_watermark_logo_path',
|
||||
'branding_watermark_logo_url',
|
||||
'branding_watermark_position',
|
||||
'branding_watermark_opacity',
|
||||
'branding_watermark_size'
|
||||
])
|
||||
.del();
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
# Package Upgrade Summary - Production System
|
||||
Date: 2025-07-22
|
||||
|
||||
## ✅ Successfully Upgraded (8 packages)
|
||||
|
||||
### Phase 1 (Low Risk):
|
||||
**Backend:**
|
||||
- i18next: 25.3.1 → 25.3.2
|
||||
- bcrypt: 5.1.1 → 6.0.0
|
||||
- nodemailer: 6.10.1 → 7.0.5
|
||||
|
||||
**Frontend:**
|
||||
- date-fns: 2.30.0 → 4.1.0
|
||||
- lucide-react: 0.292.0 → 0.525.0
|
||||
|
||||
### Phase 2 (Medium Risk - Carefully Tested):
|
||||
**Backend:**
|
||||
- sharp: 0.32.6 → 0.34.3
|
||||
- chokidar: 3.6.0 → 4.0.3
|
||||
|
||||
**Frontend:**
|
||||
- react-toastify: 9.1.3 → 11.0.5
|
||||
|
||||
## 🚫 Deferred Upgrades (High Risk)
|
||||
|
||||
### Critical Bug Found:
|
||||
- **archiver**: MUST stay at 5.3.2 (v7 has append() bug that breaks watermarks)
|
||||
|
||||
### Major Breaking Changes:
|
||||
- express 4 → 5
|
||||
- knex 2 → 3
|
||||
- React 18 → 19
|
||||
- tailwindcss 3 → 4
|
||||
|
||||
## Security Status
|
||||
- **npm audit vulnerabilities: 0** ✅
|
||||
- All upgraded packages tested and working
|
||||
- No known security issues in current packages
|
||||
|
||||
## Backup Locations
|
||||
- Phase 1: `/backups/phase1-upgrade-20250722-103923/`
|
||||
- Phase 2: `/backups/phase2-upgrade-20250722-104940/`
|
||||
|
||||
## Production Ready
|
||||
All upgrades have been tested and are ready for production deployment. Monitor closely for 48 hours after deployment.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user