Compare commits

..

192 Commits

Author SHA1 Message Date
Gitea Actions Bot 769dff4364 chore: bump backend version to 1.0.80
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-23 05:30:06 +00:00
paul d2e0e75e9a fix: force github-release plugin to use GitHub API instead of Gitea
Mirror to GitHub / mirror (push) Successful in 35s
Test and Lint / backend-test (push) Successful in 1m29s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m3s
The plugin was auto-detecting the Gitea instance and using its API
instead of GitHub's. Fixed by:
- Adding explicit environment variables to override detection
- Removing deprecated github_url/github_upload_url parameters
- Setting DRONE_REMOTE_URL to point to GitHub

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 07:29:14 +02:00
paul 041f3b58f4 fix: remove unused formatBoolean import from migration 033
Mirror to GitHub / mirror (push) Successful in 33s
Test and Lint / backend-test (push) Successful in 1m24s
Test and Lint / frontend-test (push) Has been cancelled
Version and Release / version-bump (push) Successful in 42s
continuous-integration/drone/push Build is passing
Version and Release / trigger-drone (push) Successful in 3s
Removed unnecessary import that could cause issues if helpers.js
doesn't define formatBoolean. Migration already uses correct
boolean syntax without the helper.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 07:26:30 +02:00
paul 0f4db08883 fix: remove formatBoolean calls from migration 032 - critical production fix
Mirror to GitHub / mirror (push) Successful in 30s
Test and Lint / frontend-test (push) Has been cancelled
Test and Lint / backend-test (push) Has been cancelled
Version and Release / version-bump (push) Has been cancelled
Version and Release / trigger-drone (push) Has been cancelled
continuous-integration/drone/push Build is passing
Migration was failing with "formatBoolean is not a function" error,
preventing backend startup. Fixed by:
- Removing formatBoolean import
- Using direct boolean values for column defaults
- Using JSON.stringify for setting values

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 07:25:10 +02:00
Gitea Actions Bot e0bd8f0707 chore: bump backend version to 1.0.79
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-22 20:36:13 +00:00
paul a8d4500481 fix: resolve migration conflicts and duplicate numbering
Mirror to GitHub / mirror (push) Successful in 33s
Test and Lint / backend-test (push) Successful in 1m24s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m15s
Version and Release / version-bump (push) Successful in 41s
Version and Release / trigger-drone (push) Successful in 3s
- Rename conflicting migrations to sequential numbers
- Update 035_enhance_backup_system.js to check for existing columns
- Prevent 'column already exists' errors during migration
- Add proper column existence checks before alterations
2025-07-22 22:30:59 +02:00
paul 12f7901d63 fix: correct GitHub repository path in Drone CI release config
Mirror to GitHub / mirror (push) Successful in 33s
Test and Lint / backend-test (push) Successful in 1m18s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m2s
- Remove deprecated base_url and upload_url parameters
- Use correct GitHub repository: the-luap/picpeak
- This should resolve the 404 error when creating releases
2025-07-22 15:31:24 +02:00
Gitea Actions Bot 18ddca6c27 chore: bump version to 1.0.78 (backend + frontend)
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-22 13:14:14 +00:00
paul b31f7e6f34 feat: implement gallery feedback system with version tracking for backups
Mirror to GitHub / mirror (push) Successful in 38s
Test and Lint / backend-test (push) Successful in 1m26s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m9s
Version and Release / version-bump (push) Successful in 48s
Version and Release / trigger-drone (push) Successful in 3s
Gallery Feedback Features:
- Add feedback system allowing ratings, likes, comments, and favorites on photos
- Implement admin controls for enabling/disabling feedback per event
- Add content moderation with word filters and spam detection
- Implement rate limiting to prevent abuse (10 requests/15min per type)
- Create comprehensive admin interface for feedback management
- Add analytics dashboard for feedback insights
- Export feedback data when archiving events

Frontend Components:
- PhotoRating: 5-star rating system with optimistic updates
- PhotoLikes: Like/unlike with animation
- PhotoComments: Threaded comments with moderation
- PhotoFavorites: Bookmark functionality
- FeedbackSettings: Admin configuration panel
- EventFeedbackPage: Complete management interface

Backend Implementation:
- Database migration 033: 4 new tables for feedback system
- RESTful API with proper authorization
- Guest identification via SHA256(IP+UserAgent)
- Automatic backup integration
- Email notification support

Backup Version Tracking:
- Migration 034: Add version columns to backup tables
- Track app version, Node.js version, and DB schema version
- Create restore_history table for tracking restore attempts
- Add version compatibility checking for safe restores
- Configurable version matching requirements

Security & Performance:
- Input validation and sanitization
- Rate limiting per feedback type
- Content moderation system
- Optimistic UI updates
- Efficient database queries with proper indexes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-22 15:08:52 +02:00
paul 60fdd5d6ef fix: configure github-release plugin to use GitHub API instead of Gitea
Mirror to GitHub / mirror (push) Successful in 29s
Test and Lint / backend-test (push) Successful in 1m34s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m7s
- Add base_url and upload_url pointing to GitHub API
- Explicitly set repo and owner for GitHub repository
- Fixes 401 authentication error in release pipeline
2025-07-22 11:31:13 +02:00
Gitea Actions Bot 0ee7eeaa59 chore: bump version to 1.0.77 (backend + frontend)
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-22 09:20:43 +00:00
paul 811230fb30 Merge branch 'security-updates-form-data-multer' into main
Mirror to GitHub / mirror (push) Successful in 31s
Test and Lint / backend-test (push) Successful in 1m26s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m9s
Version and Release / version-bump (push) Successful in 43s
Version and Release / trigger-drone (push) Successful in 3s
Upgrades npm packages for improved security and stability:
- 8 packages upgraded across backend and frontend
- 0 npm audit vulnerabilities maintained
- All upgrades tested and production-ready
2025-07-22 11:15:47 +02:00
paul 761b7791f9 chore: upgrade npm packages for security and stability
Backend upgrades:
- i18next: 25.3.1 → 25.3.2 (patch)
- bcrypt: 5.1.1 → 6.0.0 (maintains compatibility)
- nodemailer: 6.10.1 → 7.0.5 (no AWS SES impact)
- sharp: 0.32.6 → 0.34.3 (image processing)
- chokidar: 3.6.0 → 4.0.3 (file watching)

Frontend upgrades:
- date-fns: 2.30.0 → 4.1.0 (date utilities)
- lucide-react: 0.292.0 → 0.525.0 (icons)
- react-toastify: 9.1.3 → 11.0.5 (notifications)

All upgrades tested, 0 npm audit vulnerabilities maintained.
Deferred high-risk upgrades (archiver, React 19, Express 5).

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-22 11:14:37 +02:00
paul 41e89676c1 fix: update form-data and multer to address security vulnerabilities
- Update form-data from 4.0.3 to 4.0.4 (fixes CVE GHSA-fjxv-7rqg-78g4)
- Update multer from 2.0.1 to 2.0.2 (fixes CVE GHSA-fjgf-rc76-4x9p)
- Both backend and frontend now have 0 vulnerabilities
- Tested upload functionality - all working correctly

These are patch updates with no breaking changes. The updates address:
- form-data: Critical vulnerability - unsafe random function for boundary
- multer: High vulnerability - DoS via unhandled exception

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-22 10:08:09 +02:00
paul 484ac9a404 fix: remove file requirement from GitHub release in Drone CI
Mirror to GitHub / mirror (push) Successful in 29s
Test and Lint / backend-test (push) Successful in 1m34s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m22s
- Remove files parameter that was looking for non-existent CHANGELOG.md
- Update release notes to include Docker image pull commands
- Add proper formatting and quick start instructions
- Fix 'validation failed: failed to find any file to release' error

The GitHub release will now create without requiring file attachments.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-22 09:42:15 +02:00
Gitea Actions Bot c1068bbc51 chore: bump frontend version to 1.0.76
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-22 07:20:29 +00:00
paul b6fb712c9f fix: correct import statements for api in backup JSX files
Mirror to GitHub / mirror (push) Successful in 36s
Test and Lint / backend-test (push) Successful in 1m29s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m28s
Version and Release / version-bump (push) Successful in 57s
Version and Release / trigger-drone (push) Successful in 3s
- Change default import to named import for api from config/api.ts
- Fixes build error: 'default' is not exported by src/config/api.ts
- Affected files: BackupHistory.jsx, RestoreWizard.jsx, BackupManagement.jsx

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-22 09:14:23 +02:00
Gitea Actions Bot 3cb8408207 chore: bump version to 1.0.75 (backend + frontend)
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
2025-07-22 07:11:13 +00:00
paul 20dd43c093 feat: implement comprehensive backup and restore system with S3 support
Mirror to GitHub / mirror (push) Successful in 28s
Test and Lint / backend-test (push) Successful in 1m26s
continuous-integration/drone/push Build is failing
Test and Lint / frontend-test (push) Failing after 2m18s
Version and Release / version-bump (push) Successful in 42s
Version and Release / trigger-drone (push) Successful in 3s
- Add S3/MinIO storage adapter with multipart upload support
- Implement database backup service for SQLite and PostgreSQL
- Create backup manifest generator for tracking backup contents
- Enhance backup service with S3 integration and incremental backups
- Add restore service with safety measures and rollback capability
- Create comprehensive test suite for all backup functionality
- Add admin API endpoints for backup/restore management
- Implement frontend UI with dashboard, configuration, and restore wizard
- Add roadmap section to README with implemented backup feature

This implementation provides:
- Multiple backup destinations (local, rsync, S3/MinIO)
- Intelligent change detection to minimize backup frequency
- Full database backups with compression
- Manifest-based restore with integrity validation
- Pre-restore safety backups with rollback
- Comprehensive error handling and monitoring
- User-friendly admin interface

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-22 09:05:52 +02:00
paul 977839156b update CI\CD
Mirror to GitHub / mirror (push) Successful in 28s
Test and Lint / backend-test (push) Successful in 1m15s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m18s
Merge branch 'main' of https://gitea.nothaft.cloud/paul/picpeak
2025-07-21 21:24:42 +02:00
paul af4f63f561 ci: publish images to GHCR and create GitHub release via Drone 2025-07-21 21:24:26 +02:00
Gitea Actions Bot cdff4ebb8a chore: bump version to 1.0.74 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-21 17:40:15 +00:00
paul 59f958b085 chore: consolidate and update deployment documentation
Mirror to GitHub / mirror (push) Successful in 28s
Test and Lint / backend-test (push) Successful in 1m4s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m20s
Version and Release / version-bump (push) Successful in 42s
Version and Release / trigger-drone (push) Successful in 4s
- Remove completed PRODUCTION_TODO_LIST.md
- Consolidate deployment guides: keep comprehensive PRODUCTION_DEPLOYMENT_GUIDE.md, remove redundant PRODUCTION_DEPLOYMENT.md
- Update all .env.example files to reflect current system:
  - Remove deprecated ADMIN_EMAIL/ADMIN_PASSWORD (now auto-generated)
  - Add proper documentation for all environment variables
  - Clarify that Umami config is optional (primary via Admin UI)
  - Add realistic examples for SMTP providers
  - Update ports to match actual defaults (3001)
- Update PRODUCTION_DEPLOYMENT_GUIDE.md:
  - Document auto-generated admin credentials process
  - Add Traefik configuration section
  - Update security checklist with current features
  - Fix outdated environment variables
  - Add nginx proxy configuration details

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 19:35:22 +02:00
paul 097d7a0b65 git statusMerge branch 'main' of https://gitea.nothaft.cloud/paul/picpeak
Mirror to GitHub / mirror (push) Successful in 27s
Test and Lint / backend-test (push) Successful in 1m15s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m22s
Update workflows
2025-07-21 17:31:57 +02:00
paul 1273777541 feat: exclude Claude contributor from GitHub mirror workflow 2025-07-21 17:31:39 +02:00
Gitea Actions Bot b6a960879f chore: bump frontend version to 1.0.73
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-21 15:28:33 +00:00
paul 4bd153104b Merge branch 'main' of https://gitea.nothaft.cloud/paul/picpeak
Mirror to GitHub / mirror (push) Successful in 35s
Test and Lint / backend-test (push) Successful in 1m30s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m30s
Version and Release / version-bump (push) Successful in 45s
Version and Release / trigger-drone (push) Successful in 3s
2025-07-21 17:23:02 +02:00
paul e099fcf600 fix: use admin API for Umami config in analytics page
- Changed from public settings endpoint to admin settings endpoint
- Fixed "Unexpected token '<'" JSON parse error
- Properly transforms settings array to key-value map
- Uses correct setting keys (analytics_umami_*)
- Maintains fallback to environment variables

The analytics page now correctly fetches Umami configuration using
the authenticated admin API instead of the public endpoint, which
was returning errors and causing JSON parse failures.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 16:53:31 +02:00
Gitea Actions Bot 89fdf401c0 chore: bump frontend version to 1.0.72
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is passing
2025-07-20 20:32:41 +00:00
paul 9cc46a1819 feat: completely rewrite GitHub mirror to create new history from target commit
Mirror to GitHub / mirror (push) Successful in 1m49s
continuous-integration/drone/push Build is failing
Test and Lint / backend-test (push) Successful in 2m12s
Test and Lint / frontend-test (push) Successful in 2m45s
BREAKING: This completely replaces the previous approach and will DELETE
all existing history on GitHub, creating entirely new commit SHAs.

Key changes:
- Use orphan branch instead of cherry-pick to break history connection
- Create initial commit from target commit tree using git read-tree
- Apply subsequent changes as completely new commits with new SHAs
- Force push will COMPLETELY REPLACE GitHub history
- No trace of commits before 7aca927937 will remain on GitHub

This ensures GitHub shows only history from the target commit onwards
with no connection to previous commits or their metadata.
2025-07-20 22:30:57 +02:00
paul f7c0e5f51a fix: improve version bump workflow with better conflict resolution
Mirror to GitHub / mirror (push) Has been cancelled
Test and Lint / backend-test (push) Has been cancelled
Test and Lint / frontend-test (push) Has been cancelled
continuous-integration/drone/push Build is passing
- Added pre-fetch and check before committing to ensure we're up-to-date
- Improved retry logic with clearer output and better error handling
- Added explicit fetch before each retry attempt
- Use for-loop instead of while for clearer retry counting
- Better fallback from rebase to merge on conflicts
- Added set -e to fail fast on errors
- More verbose logging for debugging

This should resolve the persistent "non-fast-forward" errors by:
1. Checking if we're behind before even committing
2. Pulling changes if needed
3. Retrying with proper synchronization
4. Providing clear debug output

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 22:28:05 +02:00
paul 200581e73c CRITICAL FIX: Remove 403 from auth redirect logic to restore login
Mirror to GitHub / mirror (push) Successful in 1m2s
Test and Lint / frontend-test (push) Has been cancelled
Test and Lint / backend-test (push) Has started running
continuous-integration/drone/push Build is passing
Version and Release / version-bump (push) Successful in 3m36s
Version and Release / trigger-drone (push) Successful in 6s
BREAKING ISSUE FIXED:
- 403 errors were triggering redirects, preventing login page from loading
- Public endpoints returning 403 were causing redirect loops

Changes:
- Removed 403 status from automatic redirect logic
- Only 401 (Unauthorized) now triggers login redirect
- 403 (Forbidden) errors are passed through without redirect

This fixes the critical issue where users couldn't access the login page
because public API calls were returning 403 and triggering redirects.

403 errors should be handled differently than 401:
- 401 = Missing/invalid auth (redirect to login)
- 403 = Forbidden (could be rate limit, IP block, etc - don't redirect)

🚨 Emergency fix for production

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 22:24:25 +02:00
paul 505acf833e fix: resolve SIGPIPE error in GitHub mirror workflow file cleanup
Mirror to GitHub / mirror (push) Successful in 1m23s
continuous-integration/drone/push Build is passing
Test and Lint / backend-test (push) Successful in 2m6s
Test and Lint / frontend-test (push) Has started running
- Replace problematic 'find | head -20' commands that caused exit code 141
- Use 'ls -la | head -10 || true' for safer file listing
- Add better progress logging during sensitive file removal
- Add error handling with '|| true' to prevent pipe failures

The find command was outputting more than head could handle, causing
SIGPIPE when head closed the pipe early. This fix uses ls which is
more predictable and adds proper error handling.
2025-07-20 22:17:47 +02:00
paul 1ac5b0447a fix: resolve GitHub mirror workflow cherry-pick failure with merge commits
Mirror to GitHub / mirror (push) Failing after 1m27s
Test and Lint / frontend-test (push) Has been cancelled
Test and Lint / backend-test (push) Has started running
- Add --no-merges flag to exclude merge commits during cherry-pick
- Improve error handling for cherry-pick conflicts with auto-resolution
- Add reporting of skipped merge commits for transparency
- Enhance logging to show detailed progress during commit application

Fixes the workflow failure caused by trying to cherry-pick merge commits
which require special handling that was causing exit code 128.
2025-07-20 22:13:55 +02:00
paul 15a2fc2d5f fix: resolve CI/CD version bump race condition
Mirror to GitHub / mirror (push) Failing after 27s
Test and Lint / backend-test (push) Successful in 2m19s
Test and Lint / frontend-test (push) Has been cancelled
continuous-integration/drone/push Build is passing
- Added pull before push to handle concurrent workflow executions
- Implemented retry logic with 3 attempts for push operations
- Added fallback from rebase to merge if conflicts occur
- Added proper error handling and logging for debugging

This fixes the "non-fast-forward" error that occurs when multiple
workflows run simultaneously and try to push version bumps.

The workflow now:
1. Pulls latest changes before pushing
2. Retries up to 3 times with 5-second delays
3. Falls back to merge if rebase fails
4. Provides clear error messages for debugging

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 22:10:04 +02:00
paul a1cf6a1156 feat: update GitHub mirror workflow to start history from specific commit
Mirror to GitHub / mirror (push) Failing after 34s
Test and Lint / backend-test (push) Has started running
Test and Lint / frontend-test (push) Has been cancelled
continuous-integration/drone/push Build is failing
- Start history from commit 7aca927937 instead of orphan branch
- Use cherry-pick to preserve meaningful commit history
- Automatically exclude files that only existed before target commit
- Add comprehensive error handling and logging
- Maintain clean linear history for GitHub repository
2025-07-20 22:08:53 +02:00
paul 481545c37b feat: enhance mirror-to-github workflow with commit-based history filtering
Mirror to GitHub / mirror (push) Successful in 29s
Test and Lint / backend-test (push) Successful in 1m17s
continuous-integration/drone/push Build is failing
Test and Lint / frontend-test (push) Successful in 2m29s
- Replace orphan branch approach with commit-based filtering from cfa29ad5cb
- Add automatic removal of sensitive files (env, logs, gitea configs)
- Implement robust git operations with fallback mechanisms
- Add comprehensive debugging and error handling
- Ensure same security exclusions as manual process
2025-07-20 22:00:07 +02:00
paul a67df87013 fix: handle auth errors and JSON parsing in admin panel
Mirror to GitHub / mirror (push) Successful in 32s
Test and Lint / frontend-test (push) Has been cancelled
Test and Lint / backend-test (push) Has started running
continuous-integration/drone/push Build is passing
Version and Release / version-bump (push) Failing after 1m17s
Version and Release / trigger-drone (push) Failing after 11m37s
- Added proper HTTP status check before JSON parsing in AnalyticsPage
  * Prevents "Unexpected token '<'" error when API returns HTML error pages
  * Throws proper error for non-OK responses

- Enhanced API error handling to treat 403 as auth failure
  * Both 401 and 403 now trigger redirect to login page
  * Clears expired admin tokens automatically
  * Prevents users from staying on admin pages with expired sessions

These fixes resolve:
1. JSON parse errors when fetching Umami config
2. 403 Forbidden errors not redirecting to login
3. Backend version display issues due to auth failures

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 21:58:53 +02:00
paul d2dbe2ea2f Merge branch 'main' of https://gitea.nothaft.cloud/paul/picpeak
Mirror to GitHub / mirror (push) Successful in 39s
Test and Lint / backend-test (push) Successful in 1m19s
Test and Lint / frontend-test (push) Successful in 2m18s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Has been skipped
continuous-integration/drone/push Build is passing
2025-07-20 21:19:38 +02:00
paul 98ea5e7202 chore: add production todo list and update CI/CD configs
- Added PRODUCTION_TODO_LIST.md with 9 completed production fixes
- Updated .gitea/workflows/mirror-to-github.yml
- Updated .gitignore

This commit includes all the production fixes implemented:
1. Password complexity settings
2. Gallery login security improvements
3. Analytics configuration fixes
4. Translation additions
5. UI/UX improvements
6. Date format consistency
7. Chrome compatibility fixes

All tasks have been completed and tested for production deployment.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 21:19:18 +02:00
Gitea Actions Bot 6b5c08e99b chore: bump version to 1.0.71 (backend + frontend)
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
2025-07-20 19:07:39 +00:00
paul 11ecad136b Merge branch 'main' of https://gitea.nothaft.cloud/paul/picpeak
Mirror to GitHub / mirror (push) Successful in 41s
Test and Lint / backend-test (push) Successful in 1m21s
Test and Lint / frontend-test (push) Successful in 2m23s
Version and Release / version-bump (push) Successful in 52s
Version and Release / trigger-drone (push) Successful in 5s
continuous-integration/drone/push Build is failing
2025-07-20 21:01:27 +02:00
paul 3a4dccd9f0 fix: resolve production UI and API issues
- Fixed backend version endpoint by adding retry logic import
- Gallery login page improvements:
  * Increased title size from text-xl to text-2xl (responsive scaling)
  * Title now uses event's custom primary color (var(--color-primary))
  * Removed event category badge from login page
- Fixed Umami analytics configuration check:
  * Added proper enabled state tracking
  * Warning now only shows when Umami is explicitly not configured
  * Checks both admin settings and environment variables properly

These changes improve user experience and fix false warnings in production.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 21:00:43 +02:00
Gitea Actions Bot 0a5e55ca96 chore: bump backend version to 1.0.70
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-20 19:00:09 +00:00
paul a72741c0d9 Merge branch 'main' of https://gitea.nothaft.cloud/paul/picpeak
Version and Release / trigger-drone (push) Blocked by required conditions
Mirror to GitHub / mirror (push) Successful in 40s
Test and Lint / backend-test (push) Successful in 1m28s
Test and Lint / frontend-test (push) Successful in 2m48s
Version and Release / version-bump (push) Successful in 1m4s
2025-07-20 20:52:38 +02:00
paul e7ed7006fd fix: critical database connection pool exhaustion issues
- Disabled duplicate email service (emailService.js) that was creating redundant connections
- Increased connection pool size from 10 to 25 for production environment
- Extended session timeout cache from 5 to 30 minutes to reduce DB queries
- Added connection retry logic with exponential backoff for transient failures
- Fixed password validation to use retry wrapper and correct setting key
- Updated public settings and gallery middleware to handle connection failures gracefully

These changes address the "Connection terminated unexpectedly" errors in production by:
1. Reducing unnecessary database connections
2. Increasing available connection pool capacity
3. Implementing automatic retry for transient connection failures
4. Caching frequently accessed data for longer periods

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 20:51:44 +02:00
Gitea Actions Bot 3d3013d9d6 chore: bump version to 1.0.69 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-20 18:35:36 +00:00
paul bccaa649dc Merge branch 'main' of https://gitea.nothaft.cloud/paul/picpeak
Mirror to GitHub / mirror (push) Successful in 38s
Test and Lint / backend-test (push) Successful in 1m18s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m22s
Version and Release / version-bump (push) Successful in 49s
Version and Release / trigger-drone (push) Successful in 6s
2025-07-20 20:29:16 +02:00
paul 7aca927937 fix: implement 9 production enhancements and security fixes
- Password Complexity: Added 4-level complexity selector (Simple/Moderate/Strong/Very Strong) in admin security settings with dynamic backend validation
- Gallery Security: Removed event date from login page (security risk), replaced with event type badge
- Analytics Config: Fixed "Not Configured" detection logic to check both admin settings and env variables
- Analytics Accuracy: Aligned calculation logic between dashboard and analytics endpoints, added totals verification
- Translations: Added missing activity keys (analytics_settings_updated, cms_page_updated, security_settings_updated, password_reset, admin_logout, system_activity)
- UI Fixes: Fixed German text overflow in CMS page selector with proper CSS truncation
- Date Format: Event creation now respects admin-configured date format instead of browser locale
- Chrome Compatibility: Replaced emoji flags with SVG components for Windows Chrome support

All changes maintain backward compatibility and production stability.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 20:27:28 +02:00
Gitea Actions Bot e229c60b22 chore: bump version to 1.0.68 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-18 20:51:20 +00:00
paul 4966bc6a58 fix: resolve database connection error for analytics settings
Mirror to GitHub / mirror (push) Successful in 20s
Test and Lint / backend-test (push) Successful in 1m10s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m23s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Successful in 3s
- Update publicSettings.js to handle missing analytics setting_type gracefully
- Add dedicated PUT /analytics endpoint for saving analytics settings
- Update frontend settings service to route to correct endpoints based on setting type
- Fix query to use WHERE clause that won't fail if analytics type doesn't exist

This fixes the "Connection terminated unexpectedly" error when fetching
public settings with analytics configuration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 22:46:41 +02:00
Gitea Actions Bot 617f292516 chore: bump version to 1.0.67 (backend + frontend)
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-18 20:35:03 +00:00
paul 8e95004022 feat: fix analytics dashboard and implement complete Umami integration
Mirror to GitHub / mirror (push) Successful in 38s
Test and Lint / backend-test (push) Successful in 1m21s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m24s
Version and Release / version-bump (push) Successful in 1m1s
Version and Release / trigger-drone (push) Successful in 4s
- Fix backend analytics to include both 'download' and 'download_all' actions
- Add Analytics tab to Settings page for Umami configuration
- Update public settings endpoint to expose Umami config when enabled
- Implement dynamic Umami initialization from backend settings
- Fix frontend analytics calculations (remove hardcoded estimations)
- Add proper download counts and unique visitor tracking
- Update CLAUDE.md with production safety guidelines

The analytics dashboard now shows accurate data for all metrics, and Umami
can be configured through the admin panel instead of environment variables.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 22:29:09 +02:00
Gitea Actions Bot 93df328853 chore: bump backend version to 1.0.66
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-18 18:47:07 +00:00
paul 657e74a2e3 fix: resolve duplicate logger declaration and syntax error in rate limit service
Mirror to GitHub / mirror (push) Successful in 25s
Test and Lint / backend-test (push) Successful in 1m8s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m28s
Version and Release / version-bump (push) Successful in 40s
Version and Release / trigger-drone (push) Successful in 3s
- Remove duplicate logger import in server.js (line 26)
- Fix missing closing bracket in rateLimitService.js headers object
- Ensure backend starts without syntax errors

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 20:41:59 +02:00
Gitea Actions Bot ea77f7917e chore: bump version to 1.0.65 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-18 17:31:04 +00:00
paul f6e5a454ae feat: enhance security logging and ensure rate limit blocks are properly tracked
Mirror to GitHub / mirror (push) Successful in 31s
Test and Lint / backend-test (push) Successful in 1m27s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m22s
Version and Release / version-bump (push) Successful in 38s
Version and Release / trigger-drone (push) Successful in 3s
- Add comprehensive logging for rate limit blocks with full request details
  - IP address (with proper proxy detection), user agent, headers, timestamps
  - Rate limit info (current count, limit, remaining, reset time)
  - Separate tracking for auth vs general endpoints

- Enhance authentication failure logging
  - JWT validation failures with detailed error info
  - Admin auth attempts without token
  - Failed token validation with user context
  - All events include IP, path, method, user agent

- Improve Winston logger configuration for production
  - Add automatic log rotation (10MB errors, 50MB combined)
  - Create separate security.log for auth/rate limit events
  - Ensure logs directory exists automatically
  - Add structured JSON format for log aggregation
  - Support container logging with LOG_TO_CONSOLE env var

- Create comprehensive documentation
  - Security logging guide with examples
  - Monitoring recommendations
  - Configuration reference

- Add test script to verify logging functionality

All rate limit settings remain configurable via admin panel:
- Window duration, max requests, auth limits
- Skip authenticated requests option
- Public endpoints only option

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 19:25:42 +02:00
Gitea Actions Bot cc7ad4b2bc chore: bump frontend version to 1.0.61
continuous-integration/drone/push Build is passing
2025-07-17 09:20:40 +00:00
paul 827a599102 feat: enhance CMS editor with rich text features and fix line break preservation
Mirror to GitHub / mirror (push) Successful in 22s
Test and Lint / backend-test (push) Successful in 1m16s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m19s
Version and Release / version-bump (push) Failing after 38s
Version and Release / trigger-drone (push) Has been skipped
- Fix line break preservation with proper HardBreak extension configuration
- Add text alignment support (left, center, right, justify) for paragraphs and headings
- Add H4-H6 heading support with keyboard shortcuts
- Add code block support with syntax highlighting for multiple languages
- Enhance preview functionality with split-pane view (Edit/Preview/Split modes)
- Add custom prose CSS overrides for proper content rendering
- Update DOMPurify configuration to preserve br tags and text alignment
- Add visual indicators and help modal with all keyboard shortcuts
- Improve toolbar organization with grouped buttons and separators
- Add auto-save status indicator and word/character count

Fixes issues with:
- Line breaks not showing in gallery view
- Limited heading options (only H1-H3)
- No text formatting capabilities
- Missing preview functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-17 11:15:41 +02:00
Gitea Actions Bot b9841b762c chore: bump backend version to 1.0.64
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-17 08:08:59 +00:00
paul e636cf5d56 fix: prevent photo corruption during upload and add batch processing improvements
Mirror to GitHub / mirror (push) Successful in 20s
Test and Lint / backend-test (push) Successful in 1m9s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m9s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
- Remove async functions from multer callbacks (primary corruption cause)
- Implement temp directory upload approach with proper cleanup
- Add comprehensive file integrity validation before processing
- Fix batch upload category assignment and photo naming
- Add automatic cleanup service for orphaned temp uploads
- Enhance error handling with better corruption detection
- Add fix-temp-photos script to repair existing temporary files
- Update file watcher to ignore temp upload files

Fixes issues with:
- Corrupted photos showing only partial images
- Photos retaining temp_ names after upload
- Category assignments lost during batch uploads
- Incomplete file uploads causing "Premature end of input" errors

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-17 10:04:29 +02:00
Gitea Actions Bot a896fa66c0 chore: bump frontend version to 1.0.58
continuous-integration/drone/push Build is passing
2025-07-17 07:42:08 +00:00
paul 027c1090a4 fix: remove console.log statements from gallery and auth pages
Mirror to GitHub / mirror (push) Successful in 23s
Test and Lint / backend-test (push) Successful in 1m35s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m29s
Version and Release / version-bump (push) Failing after 52s
Version and Release / trigger-drone (push) Has been skipped
- Remove debug logging from GalleryView component
- Remove console.error statements from auth contexts
- Clean up image loading error logs
- Replace console statements with comments for production security

No sensitive information is now logged to console in production.
2025-07-17 09:33:38 +02:00
Gitea Actions Bot 4e214588a7 chore: bump backend version to 1.0.62
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-17 06:50:13 +00:00
paul 9d0607f4f0 feat: implement dynamic rate limiting with database configuration
Mirror to GitHub / mirror (push) Successful in 21s
Test and Lint / backend-test (push) Successful in 1m10s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
- Add database migration for rate limit settings
- Create rate limit service with dynamic configuration from database
- Implement proper authentication detection for admin and gallery tokens
- Skip rate limiting for authenticated users (configurable)
- Add admin API endpoint to update rate limit settings
- Use correct client IP detection with proxy support
- Cache settings for performance (1 minute cache)
- Default to 1000 requests per 15 minutes for better UX
- Apply auth-specific limits only to login endpoints

Key improvements:
- No more rate limiting for authenticated gallery/admin users
- Configurable via admin settings page
- Immediate effect when settings change
- Better handling of proxied requests

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-17 08:43:44 +02:00
Gitea Actions Bot 8cbe97d2f4 chore: bump backend version to 1.0.61
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-17 05:32:14 +00:00
paul 0d31c9037c fix: handle broken thumbnails and incomplete uploads
Mirror to GitHub / mirror (push) Successful in 21s
Test and Lint / backend-test (push) Successful in 1m4s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 3s
- Implement automatic thumbnail regeneration for corrupted files
- Add validation middleware to check uploaded files before processing
- Improve error handling for "Premature end of input file" errors
- Delete and regenerate thumbnails when serving fails
- Add file integrity checks during upload process
- Better error reporting for failed uploads

This fixes the VipsJpeg errors and ensures reliable thumbnail generation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-17 07:25:29 +02:00
Gitea Actions Bot ffcfd9766d chore: bump backend version to 1.0.60
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-16 21:41:10 +00:00
paul 3501a52f0e fix: rate limiting issues with reverse proxy setup
Mirror to GitHub / mirror (push) Successful in 27s
Test and Lint / backend-test (push) Successful in 1m12s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m13s
Version and Release / version-bump (push) Successful in 40s
Version and Release / trigger-drone (push) Successful in 3s
- Add keyGenerator function to properly detect client IP behind proxy
- Support X-Forwarded-For and X-Real-IP headers from Traefik/nginx
- Add custom handlers with better error messages
- Add debug endpoint (dev only) to verify IP detection
- Improve logging for rate limit debugging

This fixes the 429 errors when multiple requests come from same proxy IP.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 23:36:04 +02:00
Gitea Actions Bot 5ca598b80a chore: bump backend version to 1.0.59
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 21:19:09 +00:00
paul 4e2075c638 fix: prevent double date formatting in email templates
Mirror to GitHub / mirror (push) Successful in 25s
Test and Lint / backend-test (push) Successful in 1m7s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 3s
- Remove pre-formatting of dates before passing to email processor
- Let email processor handle all date formatting based on recipient language
- Fix Invalid Date errors by passing raw date values instead of formatted strings
- Remove unused formatDate imports and variables

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 23:14:32 +02:00
Gitea Actions Bot 0a691d4251 chore: bump backend version to 1.0.58
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-16 20:52:25 +00:00
paul 34846ae71a fix: resolve Invalid Date issue in email templates
Mirror to GitHub / mirror (push) Successful in 20s
Test and Lint / backend-test (push) Successful in 1m0s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 3s
- Fix date parsing for YYYY-MM-DD format to use local timezone
- Handle date strings properly to avoid timezone conversion issues
- Add validation to ensure dates are valid before formatting
- Update expiration date calculation to use consistent parsing

This fixes the "Invalid Date" display in gallery creation emails.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 22:43:33 +02:00
Gitea Actions Bot ea261dd03b chore: bump version to 1.0.57 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 15:37:55 +00:00
paul eb93223d79 fix: email resend language and password display issues
Mirror to GitHub / mirror (push) Successful in 22s
Test and Lint / backend-test (push) Successful in 1m4s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m9s
Version and Release / version-bump (push) Successful in 41s
Version and Release / trigger-drone (push) Successful in 3s
- Added missing translation for admin.activities.email_resent in both EN and DE
- Fixed language detection in resend email to use email processor's priority system
- Fixed password security message showing German text in English emails
- Updated email processor to handle password placeholder and format dates properly
- Dates are now formatted according to language and system settings
- Password security message now displays in the correct language based on recipient
2025-07-16 17:33:33 +02:00
Gitea Actions Bot b20f9cc108 chore: bump backend version to 1.0.56
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-16 15:22:56 +00:00
paul 515814e1d5 feat: update English email templates to match German quality and content
Mirror to GitHub / mirror (push) Successful in 23s
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m17s
Version and Release / version-bump (push) Successful in 39s
Version and Release / trigger-drone (push) Successful in 3s
- Enhanced all English email templates with proper HTML styling
- Added conditional content blocks (Handlebars {{#if}} syntax)
- Added styled alert boxes for warnings and success messages
- Added action buttons with proper styling
- Added emoji icons for better visual communication
- Added missing variables: support_email, support_phone, archive_date, photo_count
- Matched professional tone and formatting from German templates
- Improved visual hierarchy with background colors and borders
- All templates now have equal or better quality scores
2025-07-16 17:18:19 +02:00
Gitea Actions Bot fba9838e21 chore: bump backend version to 1.0.55
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-16 14:52:07 +00:00
paul a9c2761986 fix: add language detection from app_settings and fix activity_logs schema
Mirror to GitHub / mirror (push) Successful in 26s
Test and Lint / backend-test (push) Successful in 1m6s
Test and Lint / frontend-test (push) Successful in 2m23s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 3s
continuous-integration/drone/push Build is passing
- Enhanced language detection priority in email processor:
  1. Event-specific language
  2. App settings general_default_language (NEW)
  3. Email config default_language
  4. Domain-based detection
- Fixed activity_logs insertion error by using logActivity function
- Fixed 500 error on resend email endpoint
- Emails now respect language selected in settings page
2025-07-16 16:47:04 +02:00
Gitea Actions Bot db7e5913eb chore: bump backend version to 1.0.54
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 14:44:54 +00:00
paul aa27d1ea79 fix: resolve 500 error on resend email endpoint
Mirror to GitHub / mirror (push) Successful in 31s
Test and Lint / backend-test (push) Successful in 1m16s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m16s
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s
- Moved queueEmail import to top of file (no more dynamic require)
- Fixed similar issue in password reset endpoint
- Added error handling for activity log insertion to prevent failures
- Added default values for ip_address and user_agent
- Added detailed error logging for debugging

The email was being sent successfully but the endpoint was returning 500
due to the dynamic require pattern. This is now fixed.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 16:39:56 +02:00
Gitea Actions Bot cf8df2780e chore: bump version to 1.0.53 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 14:29:36 +00:00
paul 419a283c62 feat: add resend creation email button and fix Handlebars template processing
Mirror to GitHub / mirror (push) Successful in 21s
Test and Lint / backend-test (push) Successful in 1m6s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m7s
Version and Release / version-bump (push) Successful in 41s
Version and Release / trigger-drone (push) Successful in 3s
- Fixed email template processing to properly handle Handlebars conditionals
  - Installed handlebars package
  - Updated processTemplate to use Handlebars.compile()
  - Now {{#if welcome_message}} blocks work correctly

- Added "Resend Creation Email" button to event details page
  - New endpoint: POST /api/admin/events/:id/resend-email
  - Button appears below "Reset Gallery Password"
  - Shows mail icon and includes success/error notifications
  - For security, resent emails show "(Aus Sicherheitsgründen nicht angezeigt)"
    instead of the actual password

- Added translations:
  - EN: "Resend Creation Email", success/error messages
  - DE: "Erstellungs-E-Mail erneut senden", success/error messages

This fixes the issue where Handlebars syntax was showing in emails and provides
a convenient way to resend the initial gallery creation email to hosts.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 16:25:00 +02:00
Gitea Actions Bot 1b075a4beb chore: bump backend version to 1.0.52
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 14:01:47 +00:00
paul 68ff93cf17 feat: complete German email template implementation and language detection
Mirror to GitHub / mirror (push) Successful in 21s
Test and Lint / backend-test (push) Successful in 1m3s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 38s
Version and Release / trigger-drone (push) Successful in 3s
- Added comprehensive German translations for all email templates:
  - gallery_created: Welcome email with proper German formatting
  - expiration_warning: Warning email with countdown in German
  - gallery_expired: Expiration notice in German
  - archive_complete: Archive completion notice in German

- Enhanced language detection with priority system:
  1. Event-specific language setting (new 'language' column)
  2. Global email config default language
  3. Domain-based detection (.de, .at, .ch, .li)

- Added language preferences:
  - events.language column for per-event language settings
  - email_configs.default_language for system-wide default
  - Set default to German ('de') for existing installations

- Fixed email processor to:
  - Pass eventId for proper language detection
  - Handle both SQLite and PostgreSQL JSON formats
  - Log language used for each email sent

All German templates are professionally translated with proper:
- Greetings ("Liebe(r)" instead of "Dear")
- Date formatting (DD.MM.YYYY)
- Professional closings ("Mit freundlichen Grüßen")
- UI elements ("Galerie-Details:" instead of "Gallery Details:")

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 15:57:20 +02:00
Gitea Actions Bot b22a29e877 chore: bump version to 1.0.51 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 13:10:04 +00:00
paul e601311ca3 fix: resolve email queue discrepancy between admin dashboard and processor
Mirror to GitHub / mirror (push) Successful in 22s
Test and Lint / backend-test (push) Successful in 1m6s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m4s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
The issue: Admin dashboard showed pending emails that the processor wouldn't
process because they had exceeded retry limits (retry_count >= 3).

Changes:
- Updated backend /admin/system/status to provide detailed email queue stats:
  - pending: total pending emails (as before)
  - processable: emails that will actually be processed (retry_count < 3)
  - stuck: emails that exceeded retry limit but are still pending
- Updated frontend to display stuck emails with warning when present
- Created debug-email-queue.js script to diagnose discrepancies
- Created fix-stuck-emails.js script to handle stuck emails:
  - Can reset retry count, mark as failed, or delete
  - Usage: node fix-stuck-emails.js [reset|fail|delete]

This makes it clear when emails are stuck and won't be processed automatically.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 15:05:34 +02:00
Gitea Actions Bot bb00c3993b chore: bump version to 1.0.50 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 12:46:54 +00:00
paul c9c0de46bf fix: improve email processor reliability and add diagnostic tools
Mirror to GitHub / mirror (push) Successful in 22s
Test and Lint / backend-test (push) Successful in 1m19s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m15s
Version and Release / version-bump (push) Successful in 37s
Version and Release / trigger-drone (push) Successful in 2s
- Enhanced email processor with comprehensive logging
- Added error handling for database queries in email processing
- Fixed duplicate export in module.exports
- Added testEmailConnection() function for connectivity testing
- Created diagnostic scripts:
  - check-email-processor.js: Full diagnostic with manual processing
  - run-email-processor.js: Standalone processor (can run once or continuously)
  - check-email-environment.js: Environment and configuration checker
- Fixed missing translation: added 'common.unknown' in EN/DE locales

This should help identify why emails aren't being processed on production.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 14:30:09 +02:00
Gitea Actions Bot 5374299cd5 chore: bump backend version to 1.0.49
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 09:34:18 +00:00
paul 536e2b2874 fix: handle pre-parsed JSON data in email processor
Mirror to GitHub / mirror (push) Successful in 22s
Test and Lint / backend-test (push) Successful in 1m5s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m29s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Successful in 2s
The email_data column in PostgreSQL is a JSON type that auto-parses
the data, while SQLite stores it as a string. Updated the code to
check if email_data is already an object before attempting to parse.

This fixes the "Unexpected token o in JSON at position 1" error.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 11:29:36 +02:00
Gitea Actions Bot 141acd5736 chore: bump version to 1.0.48 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 09:17:20 +00:00
paul 5f4337a18d fix: resolve multiple production issues
Mirror to GitHub / mirror (push) Successful in 23s
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m4s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s
- Fix archive page showing "00" instead of "0" for empty photo counts
- Fix email queue PostgreSQL "updated_at" column error with migration and fallback
- Fix storage calculation discrepancies between system status and archive pages
- Add proper storage info to system status endpoint
- Fix archive path concatenation in settings endpoint

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 11:12:32 +02:00
Gitea Actions Bot fec7b687f7 chore: bump version to 1.0.47 (backend + frontend)
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 08:23:21 +00:00
paul cfa29ad5cb fix: GUI improvements and fixes
Mirror to GitHub / mirror (push) Successful in 19s
Test and Lint / backend-test (push) Successful in 1m12s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m6s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 2s
- Enable JSON module imports in TypeScript config to fix frontend version display
- Fix archive page showing '00' instead of '0' for empty photo counts
- Add null safety to archive photo count calculation
- Update email processor to reinitialize on config changes
- Add auto-retry for failed email transporter initialization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 10:18:52 +02:00
Gitea Actions Bot 76ae35217c chore: bump backend version to 1.0.46
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 07:25:22 +00:00
paul fe651fa38e fix: add activity_logs deletion to event delete transaction
Mirror to GitHub / mirror (push) Successful in 24s
Test and Lint / backend-test (push) Successful in 1m6s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s
- Include activity_logs table in deletion transaction to prevent foreign key constraint error
- Maintain proper deletion order: activity_logs before events
- Fixes 500 error when deleting events that have associated activity logs

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 09:20:48 +02:00
paul f7b8c0c0fe fix: resolve event deletion 500 error with proper foreign key handling
Mirror to GitHub / mirror (push) Successful in 22s
Test and Lint / backend-test (push) Successful in 1m7s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m18s
Version and Release / version-bump (push) Failing after 31s
Version and Release / trigger-drone (push) Has been skipped
- Rewrite delete endpoint to use database transaction
- Delete related data in correct order: access_logs, email_queue, photos, photo_categories, then event
- Add cleanup for storage folders and archive files
- Improve error messages for better debugging

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 08:30:05 +02:00
Gitea Actions Bot 4af3cc2486 chore: bump version to 1.0.45
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-15 20:45:45 +00:00
paul 3632b936e9 fix: improve URL slug generation for special characters
Mirror to GitHub / mirror (push) Successful in 20s
Test and Lint / backend-test (push) Successful in 1m6s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m6s
Version and Release / version-bump (push) Successful in 30s
Version and Release / trigger-drone (push) Successful in 2s
- Replace all non-alphanumeric characters with single dash
- Collapse multiple consecutive dashes into single dash
- Remove leading and trailing dashes
- Fixes issue where "Petra & Peter" became "petra---peter"
- Now generates cleaner URLs like "petra-peter" instead

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 22:41:25 +02:00
Gitea Actions Bot 66a6d4003a chore: bump version to 1.0.44
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-15 20:34:22 +00:00
paul bdf73c1f06 fix: properly allow date-based passwords for galleries
Mirror to GitHub / mirror (push) Successful in 23s
Test and Lint / backend-test (push) Successful in 1m6s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m9s
Version and Release / version-bump (push) Successful in 31s
Version and Release / trigger-drone (push) Successful in 3s
- Added skipStrengthCheck option to bypass zxcvbn analysis for gallery passwords
- Added explicit date pattern matching for formats like "04.07.2025"
- Date passwords (DD.MM.YYYY, DD/MM/YYYY, DD-MM-YYYY) are now automatically accepted
- Gallery passwords skip all strength requirements but maintain 6 character minimum
- Fixes production issue where date passwords were rejected by zxcvbn

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 22:29:58 +02:00
Gitea Actions Bot f032743690 chore: bump version to 1.0.43
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 16:52:20 +00:00
paul a9902b95b4 fix: allow date formats as gallery passwords
Mirror to GitHub / mirror (push) Successful in 21s
Test and Lint / backend-test (push) Successful in 1m7s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m17s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Successful in 3s
- Modified backend password validation for galleries to be more lenient
- Reduced minimum password length to 6 characters for galleries
- Removed uppercase/lowercase/number requirements for gallery passwords
- Allow date formats like "04.07.2025" as passwords
- Added helper text to inform users about password options
- Still prevent overly simple passwords like "123456"
- Admin passwords remain strict with all security requirements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 18:47:48 +02:00
Gitea Actions Bot 9d1c0b672a chore: bump version to 1.0.42
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 16:40:19 +00:00
paul 727fd8bae8 fix: allow past dates when creating events
Mirror to GitHub / mirror (push) Successful in 20s
Test and Lint / backend-test (push) Successful in 1m13s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m24s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s
- Removed min date restriction from event date picker
- Users can now select any date (past, present, or future)
- Essential for uploading events that have already occurred

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 18:35:30 +02:00
Gitea Actions Bot 954103510a chore: bump version to 1.0.41
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 15:14:40 +00:00
paul 801e1f81d9 fix: improve gallery lightbox UI for better user experience
Mirror to GitHub / mirror (push) Successful in 24s
Test and Lint / backend-test (push) Successful in 1m12s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m22s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
- Removed photo filename display from lightbox for cleaner viewing
- Fixed z-index layering to ensure controls always appear above images
- Changed control elements from z-10 to z-20 for proper visibility
- Set image container to z-0 to prevent overlap with controls
- Ensures all buttons remain accessible regardless of image size or zoom

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 17:09:37 +02:00
paul f9861480aa fix: improve GitHub mirror workflow reliability
Mirror to GitHub / mirror (push) Successful in 22s
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m14s
- Added workflow_dispatch for manual triggering
- Added debugging steps to identify issues
- Clean up existing branches before creating new ones
- Remove existing remotes to avoid conflicts
- Added GitHub token verification
- Added completion status messages
- Made workflow more resilient with better error handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 16:30:22 +02:00
Gitea Actions Bot a26dfd3d6f chore: bump version to 1.0.40
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 14:21:15 +00:00
paul 1db908771f fix: resolve multiple UI issues in admin panel
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m13s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s
- Fixed dropdown menu visibility in events table by using fixed positioning
- Fixed double /api prefix in settings upload endpoints (favicon, watermark)
- Fixed thumbnail display in hero image selection by properly handling API paths
- Removed unnecessary console.log statements
- Added proper cleanup for dropdown on scroll/resize events

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 16:16:47 +02:00
Gitea Actions Bot 59651b8c24 chore: bump version to 1.0.39
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 11:04:23 +00:00
paul 7ccd48297f fix: resolve dropdown menu visibility issue in admin events table
Test and Lint / backend-test (push) Successful in 1m7s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m18s
Version and Release / version-bump (push) Successful in 38s
Version and Release / trigger-drone (push) Successful in 3s
- Changed Card container from overflow-hidden to overflow-visible
- Added overflow-y-visible to prevent vertical clipping
- Dropdown menu now properly displays even with few events
- Table only scrolls when content exceeds viewport height

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 12:59:54 +02:00
Gitea Actions Bot d05ff6380e chore: bump version to 1.0.38
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 09:51:55 +00:00
paul 605f773a7e fix: resolve gallery photo/thumbnail serving issues
Test and Lint / backend-test (push) Successful in 1m13s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m11s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
- Change gallery photo URLs from static paths to API endpoints
- Add dedicated thumbnail serving endpoint for galleries
- Add test script to diagnose authentication issues
- Add nginx configuration documentation for Authorization header

This fixes the issue where photos and thumbnails work in admin but not
in gallery view. The problem was that static file routes with auth
middleware often have Authorization headers stripped by reverse proxies.
Using API endpoints ensures proper authentication handling.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 11:47:53 +02:00
Gitea Actions Bot c844f634c8 chore: bump version to 1.0.37
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 09:37:32 +00:00
paul 1d94398e2d fix: add thumbnail cleanup on archive deletion and diagnostic scripts
Test and Lint / backend-test (push) Successful in 1m8s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m23s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Successful in 3s
- Fix archive deletion to also clean up associated thumbnails
- Add cleanup-thumbnails.js script to remove temporary and orphaned thumbnails
- Add diagnose-thumbnails.js script to troubleshoot thumbnail serving issues
- Update README with documentation for new scripts

This prevents thumbnail accumulation when events are deleted and helps
diagnose why thumbnails might not be showing despite existing on disk.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 11:33:11 +02:00
Gitea Actions Bot a2551dc0ad chore: bump version to 1.0.36
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 09:27:09 +00:00
paul 32821934e6 feat: add storage management scripts for production troubleshooting
Test and Lint / backend-test (push) Successful in 1m13s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 37s
Version and Release / trigger-drone (push) Successful in 4s
- Add check-storage.js to verify storage structure and file existence
- Add regenerate-thumbnails.js to recreate missing thumbnail files
- Include comprehensive README with usage instructions
- Scripts help diagnose and fix missing photo/thumbnail issues

These scripts are essential for production maintenance when photos
or thumbnails are missing from the storage directory.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 11:23:00 +02:00
Gitea Actions Bot b9c28e52cd chore: bump version to 1.0.35
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-15 07:15:42 +00:00
paul c94b6268cf fix: resolve gallery authentication and redirect issues
Test and Lint / backend-test (push) Failing after 32s
Test and Lint / frontend-test (push) Successful in 2m15s
continuous-integration/drone/push Build is passing
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 4s
- Fix useWatermarkSettings hook to use public API endpoint instead of admin endpoint
- Add hero_photo_id to gallery authentication response
- Prevent 401 errors on gallery pages from redirecting to admin login
- Gallery pages now correctly fetch settings without requiring admin auth

The main issue was that gallery pages were calling admin-only endpoints
which triggered 401 errors and caused redirects to the admin login page.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 09:11:50 +02:00
Gitea Actions Bot 439c743fd1 chore: bump version to 1.0.34
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 06:53:59 +00:00
paul 74144f1fc6 fix: handle both share_link formats in gallery token verification
Test and Lint / backend-test (push) Successful in 1m7s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m17s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
- Support both full URL and token-only formats in share_link column
- Fix gallery info endpoint to correctly validate share tokens
- Prevents "gallery not found" errors for valid share links

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 08:49:45 +02:00
Gitea Actions Bot 99a0376657 chore: bump version to 1.0.33
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 06:39:40 +00:00
paul 21b1e79672 fix: resolve gallery login redirect issue
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m6s
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s
- Updated API interceptor to better handle gallery authentication
- Fixed 401 error handling to prevent redirect loops on gallery pages
- Improved token extraction logic for gallery API requests
- Consolidated duplicate verifyGalleryAccess middleware
- Added proper error handling in GalleryView component
- Gallery authentication now properly distinguishes from admin routes

The issue was caused by the API interceptor redirecting to admin login
when gallery API calls failed with 401, even when users were already
on gallery pages attempting to authenticate.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 08:35:33 +02:00
Gitea Actions Bot cfaee103b6 chore: bump version to 1.0.32
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-15 06:21:46 +00:00
paul c0e346992d fix: improve photo authentication middleware for thumbnails
Test and Lint / backend-test (push) Successful in 1m14s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m5s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Successful in 3s
- Add eventId check from JWT token for thumbnail access
- Better error logging for debugging authentication issues
- Add admin debug endpoint to check photo existence
- More permissive thumbnail access when valid gallery token exists

This should help diagnose why photos are returning 404 errors in production.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 08:17:52 +02:00
Gitea Actions Bot 04f45a16c9 chore: bump version to 1.0.31
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 21:15:34 +00:00
paul efad1da74d fix: resolve image and thumbnail loading issues
Test and Lint / backend-test (push) Successful in 1m8s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m9s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
- Fix static file serving paths to use correct storage directory
- Remove /api prefix from admin photo URLs to prevent double /api/api/ issue
- Fix thumbnail URL generation in gallery to use correct path format
- Update storage path resolution to support both relative and absolute paths

The issues were:
1. Admin images had URLs like /api/api/admin/events/2/thumbnail/90
2. Gallery thumbnails were looking for /thumbnails/thumb_*.jpg but paths were wrong
3. Static serving middleware was using incorrect storage paths

All images and thumbnails should now load correctly in both admin and gallery views.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 23:11:25 +02:00
Gitea Actions Bot 0a2b010332 chore: bump version to 1.0.30
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-14 19:17:01 +00:00
paul 6906c8bcf7 feat: increase file upload limit from 20 to 500 with performance optimizations
Test and Lint / backend-test (push) Successful in 1m10s
Test and Lint / frontend-test (push) Successful in 2m22s
continuous-integration/drone/push Build is passing
Version and Release / version-bump (push) Successful in 37s
Version and Release / trigger-drone (push) Successful in 3s
Backend changes:
- Update multer configuration to accept up to 500 files per upload
- Implement batch processing (10 files per transaction) for better performance
- Add memory-efficient Sharp configuration for thumbnail generation
- Increase Express body parser limits to handle large payloads
- Add proper error handling and reporting for partial upload failures

Frontend changes:
- Update validation to allow 500 files maximum
- Implement chunked uploads (50 files per chunk) to prevent timeouts
- Add progress tracking with chunk information display
- Update error messages and translations (EN/DE)

Performance optimizations:
- Disable Sharp cache to prevent memory buildup
- Limit Sharp concurrency to 2 operations
- Use sequential read for large images
- Process files in database transaction batches
- Return detailed upload results including success/failure counts

This implementation ensures the application can handle large photo uploads
efficiently without running into memory or timeout issues.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 21:12:35 +02:00
Gitea Actions Bot ac48bfdd0d chore: bump version to 1.0.29
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 19:09:25 +00:00
paul ec99243b6f test: add database compatibility tests
Test and Lint / backend-test (push) Successful in 1m6s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Successful in 3s
- Add comprehensive tests for formatBoolean() function
- Test PostgreSQL vs SQLite boolean handling
- Test date formatting and manipulation functions
- Verify insertAndGetId handles both database result formats

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 21:05:18 +02:00
paul 9932621e14 fix: comprehensive boolean compatibility for PostgreSQL and SQLite
- Add formatBoolean() usage to all boolean database queries
- Fix 36 boolean comparisons across 18 backend files
- Ensure compatibility between SQLite (0/1) and PostgreSQL (true/false)
- Update all WHERE clauses and UPDATE operations with boolean values

This completes the database compatibility fixes for production deployment.
All boolean operations now work correctly with both database systems.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 21:05:18 +02:00
paul 0fb17c78fa fix: photo upload issues with file limit and date formatting
- Add 20-file limit validation to PhotoUpload component
- Prevent Multer "Unexpected field" errors by enforcing client-side limit
- Fix JSON parsing error in dateFormatter when value is already an object
- Add missing translation keys for upload error messages
- Handle both string and object values for date format settings

These fixes resolve the 400 error when uploading more than 20 files
and the "Unexpected token o in JSON" error during email queue creation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 21:05:18 +02:00
Gitea Actions Bot 4bcca58a11 chore: bump version to 1.0.28
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 18:54:11 +00:00
paul 9fa5ba1cf7 fix: update GitHub mirror workflow to use orphan branch
Test and Lint / backend-test (push) Successful in 1m5s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m19s
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s
2025-07-14 20:49:53 +02:00
paul 88919fa0d3 fix: critical boolean compatibility for PostgreSQL/SQLite
Fixed boolean value handling differences between databases:
- SQLite stores booleans as 0/1, PostgreSQL as true/false
- Add formatBoolean() calls to critical queries that were failing

Files fixed:
- adminEvents.js: Fixed status filters and archive queries
- adminDashboard.js: Fixed active/archived event counts
- expirationChecker.js: Fixed expiration checking queries
- dbCompat.js: Updated to avoid circular dependency

Added migration 024 to:
- Enable foreign keys for SQLite (PRAGMA foreign_keys = ON)
- Document boolean compatibility requirements

This fixes queries returning 0 results in SQLite when checking
boolean columns like is_active, is_archived. Critical for
proper event management and expiration handling.

Note: 23 more boolean comparisons remain to be fixed in other files.
2025-07-14 20:49:53 +02:00
Gitea Actions Bot 5e43fc9cd9 chore: bump version to 1.0.27
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-14 18:43:02 +00:00
paul f053f42b6d fix: comprehensive PostgreSQL/SQLite compatibility fixes
Mirror to GitHub / mirror (push) Successful in 19s
Test and Lint / backend-test (push) Successful in 1m8s
Test and Lint / frontend-test (push) Successful in 2m11s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
continuous-integration/drone/push Build is passing
Critical fixes for database compatibility issues:

INSERT operations:
- Fix all INSERT queries to use .returning('id')
- Handle both PostgreSQL (returns objects) and SQLite (returns IDs)
- Fixed in: events.js, adminArchives.js, adminEvents-enhanced.js, create-test-event.js

Date operations:
- Replace SQLite-specific db.raw("datetime('now', '+30 days')")
- Use JavaScript Date objects for cross-database compatibility
- Fixed in: adminArchives.js

Database utilities:
- Add dbCompat.js utility for handling database differences
- Provides consistent API for inserts, dates, booleans, and DB-specific operations
- Centralized database compatibility logic

Migration:
- Add migration 023 documenting PostgreSQL compatibility requirements
- Ensures future developers are aware of compatibility needs

This resolves all 'not iterable' errors and ensures the application
works correctly with both PostgreSQL (production) and SQLite (development).
2025-07-14 20:38:37 +02:00
Gitea Actions Bot dc17e7d59d chore: bump version to 1.0.26
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 18:37:28 +00:00
paul f05ad87602 fix: PostgreSQL insert compatibility issues
Mirror to GitHub (Archive Method) / mirror (push) Failing after 17s
Mirror to GitHub (Rsync Method) / mirror (push) Failing after 17s
Mirror to GitHub / mirror (push) Failing after 19s
Test and Lint / backend-test (push) Successful in 1m7s
Test and Lint / frontend-test (push) Successful in 2m11s
continuous-integration/drone/push Build is passing
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s
- Fix event creation failing with 'not iterable' error
- Add .returning('id') to insert queries for PostgreSQL
- Handle both PostgreSQL (returns objects) and SQLite (returns IDs)
- Add missing fields to email_queue insert

This fixes the 500 error when creating new events in production.
2025-07-14 20:32:21 +02:00
Gitea Actions Bot 2efc74a687 chore: bump version to 1.0.25
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 18:30:15 +00:00
paul 85e7fbe73f chore: clean up unnecessary deployment files
Mirror to GitHub (Archive Method) / mirror (push) Failing after 13s
Mirror to GitHub (Rsync Method) / mirror (push) Failing after 16s
Mirror to GitHub / mirror (push) Failing after 18s
Test and Lint / backend-test (push) Successful in 1m4s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m14s
Version and Release / version-bump (push) Successful in 37s
Version and Release / trigger-drone (push) Successful in 2s
- Remove deploy/ folder with complex Docker Swarm configs
- Remove local development scripts referencing non-existent docker-compose.local.yml
- Remove setup scripts for files that already exist
- Remove docker-compose.dev.yml in favor of unified approach
- Keep certbot/, CHANGELOG.md, docker-compose.prod.yml, and production guide

This simplifies the repository structure and removes confusing
duplicate deployment approaches, making it clearer for new users.
2025-07-14 20:25:18 +02:00
Gitea Actions Bot 0a21856a8d chore: bump version to 1.0.24
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-14 18:20:14 +00:00
paul 349e7c7eb1 feat: add GitHub issue and PR templates
Mirror to GitHub (Archive Method) / mirror (push) Failing after 16s
Mirror to GitHub (Rsync Method) / mirror (push) Failing after 16s
Mirror to GitHub / mirror (push) Failing after 18s
Test and Lint / backend-test (push) Successful in 1m1s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m24s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 2s
- Add bug report template with environment details
- Add feature request template with use case section
- Add documentation improvement template
- Add security vulnerability template with warnings
- Add question template for support
- Add issue template config with helpful links
- Add comprehensive pull request template
- Update mirror workflow to exclude production guide

These templates will help maintain quality contributions
and streamline the issue/PR process on GitHub.
2025-07-14 20:15:10 +02:00
paul be07438915 docs: major repository restructure for GitHub public release
Mirror to GitHub (Archive Method) / mirror (push) Failing after 16s
Mirror to GitHub (Rsync Method) / mirror (push) Failing after 16s
Mirror to GitHub / mirror (push) Failing after 16s
Test and Lint / backend-test (push) Successful in 1m10s
Test and Lint / frontend-test (push) Has started running
Version and Release / version-bump (push) Has been cancelled
Version and Release / trigger-drone (push) Has been cancelled
continuous-integration/drone/push Build is passing
- Create comprehensive README.md optimized for GitHub/SEO
- Consolidate deployment instructions into single DEPLOYMENT.md
- Add all standard GitHub documentation files:
  - CONTRIBUTING.md with development guidelines
  - CODE_OF_CONDUCT.md for community standards
  - SECURITY.md with vulnerability reporting
  - CHANGELOG.md following Keep a Changelog format
- Simplify deployment with single docker-compose.yml
- Remove complex deployment configurations (Swarm, Traefik)
- Add backup script for easy maintenance
- Update .github-mirror-exclude to hide complex configs
- Remove redundant documentation files

This prepares PicPeak as a professional open-source alternative
to PicDrop and Scrapbook.de with clear, simple deployment.
2025-07-14 20:12:14 +02:00
Gitea Actions Bot b31ae72153 chore: bump version to 1.0.23
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
2025-07-14 12:20:00 +00:00
paul 1db08b1e9b feat: add Gitea workflows for selective GitHub mirroring
Mirror to GitHub (Archive Method) / mirror (push) Failing after 15s
Mirror to GitHub (Rsync Method) / mirror (push) Failing after 15s
Mirror to GitHub / mirror (push) Failing after 14s
Test and Lint / backend-test (push) Successful in 1m3s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m15s
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s
- Add three different approaches for mirroring to GitHub
- Approach 1: Filter out sensitive files on a separate branch
- Approach 2: Use git archive with .gitattributes exclusions
- Approach 3: Use rsync for flexible file filtering
- Add exclusion lists for sensitive files and directories
- Protect production configs, environment files, and private data

This allows maintaining a public GitHub mirror while keeping
sensitive configuration and data private on the Gitea instance.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 14:14:39 +02:00
Gitea Actions Bot 237a3332cc chore: bump version to 1.0.22
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 12:10:51 +00:00
paul e2d0a83d51 fix: relax password requirements and improve password UI
Test and Lint / backend-test (push) Successful in 1m9s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m13s
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s
- Reduce minimum password length from 12 to 8 characters
- Make special characters optional for gallery passwords
- Lower strength requirement from score 3 to 1 for galleries
- Add eye icon toggle for password visibility on each field
- Remove redundant 'Show passwords' checkbox
- Add translation for password security requirements error
- Update both English and German translations

This allows users to use simpler passwords like 'Sommer2025\!' for events
while maintaining security through other measures like expiration dates.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 14:06:18 +02:00
Gitea Actions Bot c546657285 chore: bump version to 1.0.21
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 11:13:23 +00:00
paul e8d5ee1a7b fix: multiple production issues with PostgreSQL and connection handling
Test and Lint / backend-test (push) Successful in 1m7s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m11s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s
- Fix trust proxy to use specific values instead of permissive 'true'
- Fix clear old notifications to use database-agnostic date calculation
- Fix database size check to support both PostgreSQL and SQLite
- Add caching and better error handling for session timeout queries
- Add query timeout to prevent hanging connections
- Improve JSON parsing error handling for setting values

These fixes address:
- ERR_ERL_PERMISSIVE_TRUST_PROXY warning
- PostgreSQL datetime function errors
- ENOENT errors looking for SQLite file in PostgreSQL deployment
- Connection terminated errors for session timeout checks

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 13:09:24 +02:00
Gitea Actions Bot 1761ebd531 chore: bump version to 1.0.20
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-14 10:08:49 +00:00
paul 5e5e98601f fix: improve email test endpoint error handling
Test and Lint / backend-test (push) Successful in 1m10s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m6s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Successful in 3s
- Add detailed validation for SMTP configuration
- Add specific error messages for common SMTP issues (connection, auth, network)
- Add logging to help debug email configuration issues
- Check for masked passwords that shouldn't be used for sending
- Parse smtp_port as integer and handle boolean smtp_secure properly

This helps identify the exact cause of email sending failures instead of
generic 500 errors.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 12:04:57 +02:00
Gitea Actions Bot 1cda80792b chore: bump version to 1.0.19
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 09:02:17 +00:00
paul 8740d5e618 fix: dashboard activity endpoint JSON parsing error
Test and Lint / backend-test (push) Successful in 1m4s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m26s
Version and Release / version-bump (push) Successful in 31s
Version and Release / trigger-drone (push) Successful in 3s
- Add safe JSON parsing for activity metadata in dashboard endpoint
- Handle cases where PostgreSQL returns JSON columns as objects
- Prevent 500 errors when metadata contains invalid JSON

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 10:58:12 +02:00
Gitea Actions Bot a619d52d17 chore: bump version to 1.0.18
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 08:46:37 +00:00
paul dd8cc14d30 fix: production JSON parsing errors and trust proxy issue
Test and Lint / backend-test (push) Successful in 1m3s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m16s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 3s
- Set Express to trust proxy headers for proper IP detection with Traefik
- Add safe JSON parsing for email template variables and activity log metadata
- Create migration to fix invalid JSON data in database
- Add error handling to prevent JSON.parse crashes

This fixes the 500 errors caused by invalid JSON data and the trust proxy
warning from express-rate-limit when running behind Traefik.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 10:42:41 +02:00
Gitea Actions Bot 3b7d723c2a chore: bump version to 1.0.17
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 08:18:38 +00:00
paul dc6252ff56 fix: complete email template and CMS page fixes for production
Test and Lint / backend-test (push) Successful in 1m8s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m7s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 4s
- Fix email templates list endpoint to handle both schema variations
- Add migration for default CMS pages with language-specific columns
- Create debug script to identify 500 error causes
- Update CMS migration to use correct column names (title_en/de, content_en/de)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 10:14:44 +02:00
Gitea Actions Bot 1d4e79a4f9 chore: bump version to 1.0.16
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 08:02:15 +00:00
paul 0b0e3e22d2 fix: handle email templates schema variations in production
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m20s
Version and Release / version-bump (push) Successful in 34s
Version and Release / trigger-drone (push) Successful in 3s
- Update migration to detect and handle both old and new email template schemas
- Fix migration to insert into correct columns based on existing schema
- Update adminEmail routes to handle both schema formats gracefully
- Add proper fallbacks for German language columns

This ensures the application works whether the language migration has been
applied or not, preventing null constraint violations.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 09:58:06 +02:00
Gitea Actions Bot f22e3c133f chore: bump version to 1.0.15
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-14 07:53:34 +00:00
paul 64c0a58f78 fix: database migration and routing issues for production
Test and Lint / backend-test (push) Successful in 1m19s
Test and Lint / frontend-test (push) Successful in 2m20s
continuous-integration/drone/push Build is passing
Version and Release / version-bump (push) Successful in 38s
Version and Release / trigger-drone (push) Successful in 3s
- Add migration to fix email_templates column structure after language migration
- Add migration to ensure default email templates exist
- Create diagnostic script to check database issues
- Fix docker-compose configuration for proper routing without path stripping

The backend expects routes with /api prefix, so removing the stripprefix
middleware allows proper routing to work.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 09:49:11 +02:00
Gitea Actions Bot 4182089c17 chore: bump version to 1.0.14
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-14 07:24:51 +00:00
paul cecf773fb7 fix: database connection stability issues in production
Test and Lint / backend-test (push) Successful in 1m9s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m10s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Successful in 3s
- Add robust connection pool configuration with timeouts and retry settings
- Implement retry logic in maintenance middleware for connection errors
- Increase connection stability with keepAlive and proper timeout values
- Handle "Connection terminated unexpectedly" errors gracefully

This prevents 503 errors when the database connection is temporarily interrupted
and ensures the application can recover from transient connection issues.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 09:20:51 +02:00
Gitea Actions Bot 973af17b85 chore: bump version to 1.0.13
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-14 06:45:13 +00:00
paul 6c3e88a588 fix: production deployment issues with Traefik and database migrations
Test and Lint / backend-test (push) Successful in 1m8s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Successful in 2s
- Add missing created_at column to email_queue table
- Fix 502 Bad Gateway errors with proper Traefik routing configuration
- Create docker-compose.traefik.yml for external Traefik deployment
- Fix health check endpoint path for API path stripping
- Add PostgreSQL init script for Umami database creation
- Add comprehensive deployment guide for Traefik setup

The backend now properly handles /api prefix stripping by Traefik and
migrations run safely in production environments with existing schemas.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 08:41:11 +02:00
Gitea Actions Bot 97bbb3c8e1 chore: bump version to 1.0.12
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build is passing
2025-07-13 21:22:28 +00:00
paul ac1cd96ecd fix: resolve frontend API routing issues for Traefik deployment
Test and Lint / backend-test (push) Successful in 1m16s
continuous-integration/drone/push Build is failing
Test and Lint / frontend-test (push) Successful in 2m18s
Version and Release / version-bump (push) Successful in 43s
Version and Release / trigger-drone (push) Successful in 4s
Major fixes for production deployment with Traefik:

1. API Path Fixes:
   - Remove double /api prefix from all frontend service calls
   - Fix auth.service.ts to use correct paths (/auth/admin/login)
   - Update all services to use single /api prefix from base URL
   - Fix template literal paths in photo services

2. Docker Configuration:
   - Add build args for VITE_API_URL in docker-compose.prod.yml
   - Create Dockerfile.prod with proper API URL configuration
   - Ensure frontend is built with correct API base path

3. Documentation:
   - Add comprehensive TRAEFIK_DEPLOYMENT.md guide
   - Document proper Traefik labels and routing configuration
   - Include troubleshooting steps for common issues
   - Explain network configuration and SSL handling

This resolves:
- 502 Bad Gateway errors
- Double /api/api paths in requests
- Frontend unable to communicate with backend
- Login functionality not working

The frontend now correctly calls the backend API through Traefik's
routing, with all requests going to /api/* being forwarded to the
backend service on port 3000.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 23:18:00 +02:00
Gitea Actions Bot 689861f671 chore: bump version to 1.0.11
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-13 21:12:57 +00:00
paul 41fb575e80 fix: use correct bcrypt package in create-admin script
Test and Lint / backend-test (push) Successful in 1m10s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m5s
Version and Release / version-bump (push) Successful in 40s
Version and Release / trigger-drone (push) Successful in 3s
- Change from bcryptjs to bcrypt to match installed dependency
- Fixes "Cannot find module 'bcryptjs'" error when creating admin user

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 23:09:02 +02:00
Gitea Actions Bot 6ebc4f3fc4 chore: bump version to 1.0.10
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-13 21:01:24 +00:00
paul de973f5613 fix: comprehensive production deployment fixes and migration safety
Test and Lint / backend-test (push) Successful in 1m9s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m15s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 4s
Major fixes for production deployment issues:

1. Migration System:
   - Add safe migration runner that handles existing schema
   - Create migration helper functions for idempotent operations
   - Auto-detect existing tables and mark migrations as applied
   - Handle "relation already exists" errors gracefully

2. Production Initialization:
   - Create init-production.sh script for proper startup sequence
   - Fix directory creation and permissions
   - Add admin user creation from environment variables
   - Ensure proper service initialization order

3. Documentation:
   - Add comprehensive PRODUCTION_DEPLOYMENT_GUIDE.md
   - Add MIGRATION_ERROR_FIX.md for immediate issue resolution
   - Document all known production issues and solutions
   - Include backup/restore procedures

4. Safety Improvements:
   - Add migrate:safe npm script for production use
   - Update wait-for-db.sh to use safe migrations in production
   - Add proper error handling and logging

This resolves the "relation already exists" error and prevents similar
issues in future deployments. The safe migration system can handle both
fresh installations and existing databases.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 22:57:22 +02:00
Gitea Actions Bot 279c70b3d6 chore: bump version to 1.0.9
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-13 20:46:24 +00:00
paul 5a73f6963f fix: correct YAML syntax in docker-compose.prod.yml
Test and Lint / backend-test (push) Successful in 1m10s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m13s
Version and Release / version-bump (push) Successful in 31s
Version and Release / trigger-drone (push) Successful in 3s
- Remove quotes from POSTGRES_INITDB_ARGS environment variable
- Fix YAML parsing error for PostgreSQL configuration
- Ensure proper formatting for all environment variables

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 22:42:27 +02:00
Gitea Actions Bot 5101a05bca chore: bump version to 1.0.8
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-13 20:30:04 +00:00
paul c82caf6539 fix: resolve PostgreSQL connection authentication error
Test and Lint / backend-test (push) Successful in 1m8s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m15s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 2s
- Fix "no pg_hba.conf entry" error by disabling SSL for Docker network
- Use scram-sha-256 authentication method for better security
- Update knexfile.js to support SSL configuration via environment variable
- Add documentation about PostgreSQL connection requirements

The PostgreSQL container now accepts connections from the Docker network
without requiring SSL, which is appropriate for internal container communication.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 22:26:00 +02:00
Gitea Actions Bot ae1b508726 chore: bump version to 1.0.7
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-13 20:19:10 +00:00
paul 26c05912fc fix: resolve critical production deployment issues
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m21s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 2s
- Fix database connection error "getaddrinfo ENOTFOUND postgres"
- Add wait-for-db.sh script to ensure PostgreSQL is ready before starting
- Fix email processor initialization timing issue
- Add missing storage path environment variables
- Add database dependency to backend service
- Enhance health check endpoint with database connectivity check
- Update production database defaults to match docker-compose
- Install postgresql-client in Docker image for health checks
- Document all required environment variables in .env.example

Fixes immediate production deployment failures and ensures proper service startup order.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 22:14:44 +02:00
Gitea Actions Bot d1033cb83a chore: bump version to 1.0.6
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-13 20:04:46 +00:00
paul b7458b5a37 fix: add missing pg dependency for PostgreSQL support
Test and Lint / backend-test (push) Successful in 1m10s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m7s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s
- Add pg package required for production PostgreSQL connections
- Fixes "Cannot find module 'pg'" error in production deployment

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 22:00:41 +02:00
Gitea Actions Bot face8f1496 chore: bump version to 1.0.5
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-13 19:54:25 +00:00
paul ba6ee55bf7 chore: clean up obsolete files and documentation
Test and Lint / backend-test (push) Successful in 1m5s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 39s
Version and Release / trigger-drone (push) Successful in 3s
- Remove completed auth migration documentation (13 files)
- Delete unused test and one-time scripts (23 files)
- Remove backup files and old logs
- Clean up duplicate/empty database files
- Remove old migration backup file
- Delete root level setup scripts

Total: ~185KB of obsolete files removed
All active functionality preserved

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 21:50:18 +02:00
Gitea Actions Bot e343106af5 chore: bump version to 1.0.4
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-13 19:28:44 +00:00
paul 2f848eb602 fix: make create-admin script executable
Test and Lint / backend-test (push) Successful in 1m7s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m7s
Version and Release / version-bump (push) Successful in 37s
Version and Release / trigger-drone (push) Successful in 4s
2025-07-13 21:24:40 +02:00
paul 1c7fa781ad fix: configure PostgreSQL for production and clean up deployment
- Fix database configuration to use PostgreSQL in production
- Add knexfile.js to support both SQLite (dev) and PostgreSQL (prod)
- Create admin user creation script (scripts/create-admin.js)
- Clean up docker-compose files:
  - Remove redundant docker-compose.yml and docker-compose.local.yml
  - Create docker-compose.dev.yml for development
  - Update docker-compose.prod.yml with proper DB configuration
- Clean up environment files:
  - Update .env.example for development
  - Update .env.production.example with proper settings
  - Remove redundant .env.local
- Update backend .env.example with database configuration options
- Create comprehensive DEPLOYMENT.md with admin setup instructions
- Fix production database name consistency (picpeak instead of photoapp)
2025-07-13 21:24:40 +02:00
Gitea Actions Bot 66940c2f5b chore: bump version to 1.0.3
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-07-13 19:18:49 +00:00
paul a3638fe954 fix: remove hardcoded localhost URLs for production deployment
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m17s
Version and Release / version-bump (push) Successful in 43s
Version and Release / trigger-drone (push) Successful in 3s
- Add URL utility functions for building resource URLs
- Update all components to use relative URLs in production
- Add production deployment documentation
- Update nginx config to proxy all required endpoints
- Add .env.production.example with proper configuration
2025-07-13 21:14:26 +02:00
Gitea Actions Bot 0934695a69 chore: bump version to 1.0.2
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-13 18:13:24 +00:00
paul 77ece5c5f1 fix: add missing translations for admin activities
Test and Lint / backend-test (push) Successful in 1m11s
Test and Lint / frontend-test (push) Successful in 2m15s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s
continuous-integration/drone/push Build is passing
2025-07-13 20:09:08 +02:00
320 changed files with 35866 additions and 11016 deletions
+286
View File
@@ -0,0 +1,286 @@
# 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*
+86
View File
@@ -72,6 +72,92 @@ steps:
context: frontend/
registry: registry.local.nothaft.cloud
# -------- NEW: Publish Docker images to GitHub Container Registry --------
- name: push-backend-ghcr
image: plugins/docker
settings:
repo: ghcr.io/the-luap/picpeak-backend
tags:
- ${DRONE_TAG}
- latest
dockerfile: backend/Dockerfile
context: backend/
registry: ghcr.io
username:
from_secret: GITHUB_USERNAME
password:
from_secret: GITHUB_TOKEN
build_args:
- VERSION=${DRONE_TAG}
- name: push-frontend-ghcr
image: plugins/docker
settings:
repo: ghcr.io/the-luap/picpeak-frontend
tags:
- ${DRONE_TAG}
- latest
dockerfile: frontend/Dockerfile
context: frontend/
registry: ghcr.io
username:
from_secret: GITHUB_USERNAME
password:
from_secret: GITHUB_TOKEN
build_args:
- VERSION=${DRONE_TAG}
- VITE_API_URL=${VITE_API_URL:-/api}
# -------- NEW: Create GitHub Release --------
- name: github-release
image: plugins/github-release
environment:
PLUGIN_API_KEY:
from_secret: GITHUB_TOKEN
DRONE_REMOTE_URL: https://github.com/the-luap/picpeak.git
settings:
api_key:
from_secret: GITHUB_TOKEN
repo: the-luap/picpeak
title: "PicPeak ${DRONE_TAG}"
prerelease: false
overwrite: true
note: |
# PicPeak ${DRONE_TAG}
## 🐳 Docker Images
This release includes Docker images published to GitHub Container Registry:
```bash
# Backend
docker pull ghcr.io/the-luap/picpeak-backend:${DRONE_TAG}
docker pull ghcr.io/the-luap/picpeak-backend:latest
# Frontend
docker pull ghcr.io/the-luap/picpeak-frontend:${DRONE_TAG}
docker pull ghcr.io/the-luap/picpeak-frontend:latest
```
## 📦 What's New
See the [README](https://github.com/the-luap/picpeak#readme) for features and documentation.
## 🚀 Quick Start
```bash
# Clone and deploy
git clone https://github.com/the-luap/picpeak.git
cd picpeak
# Use the tagged version
docker-compose -f docker-compose.prod.yml up -d
```
---
For detailed deployment instructions, see the [Deployment Guide](https://github.com/the-luap/picpeak/blob/main/DEPLOYMENT.md).
trigger:
event:
- tag
-280
View File
@@ -1,280 +0,0 @@
kind: pipeline
type: docker
name: default
trigger:
branch:
- main
- develop
- feature/*
event:
- push
- pull_request
- tag
volumes:
- name: docker
host:
path: /var/run/docker.sock
steps:
# Frontend Tests
- name: frontend-test
image: node:18-alpine
commands:
- cd frontend
- npm ci --legacy-peer-deps
- npm run lint
- npm run build
when:
event:
- push
- pull_request
# Backend Tests
- name: backend-test
image: node:18-alpine
commands:
- cd backend
- npm ci
- npm run lint
- npm test
environment:
NODE_ENV: test
JWT_SECRET: test-secret
when:
event:
- push
- pull_request
# Build Frontend Docker Image
- name: build-frontend
image: plugins/docker
settings:
repo: registry.local.nothaft.cloud/wedding-photo-sharing-frontend
tags:
- latest
- ${DRONE_COMMIT_SHA:0:8}
- ${DRONE_TAG}
dockerfile: frontend/Dockerfile
context: frontend
registry: registry.local.nothaft.cloud
when:
branch:
- main
event:
- push
- tag
# Build Backend Docker Image
- name: build-backend
image: plugins/docker
settings:
repo: registry.local.nothaft.cloud/wedding-photo-sharing-backend
tags:
- latest
- ${DRONE_COMMIT_SHA:0:8}
- ${DRONE_TAG}
dockerfile: backend/Dockerfile
context: backend
registry: registry.local.nothaft.cloud
when:
branch:
- main
event:
- push
- tag
# Security Scan
- name: security-scan
image: aquasec/trivy:latest
commands:
- trivy image --exit-code 0 --no-progress registry.local.nothaft.cloud/wedding-photo-sharing-frontend:${DRONE_COMMIT_SHA:0:8}
- trivy image --exit-code 0 --no-progress registry.local.nothaft.cloud/wedding-photo-sharing-backend:${DRONE_COMMIT_SHA:0:8}
environment:
DOCKER_HOST: tcp://docker:2375
volumes:
- name: docker
path: /var/run/docker.sock
when:
branch:
- main
event:
- push
# Deploy to Staging
- name: deploy-staging
image: alpine:latest
environment:
SWARM_HOST:
from_secret: staging_swarm_host
SWARM_USER:
from_secret: staging_swarm_user
SWARM_KEY:
from_secret: staging_swarm_key
REGISTRY_URL:
from_secret: docker_registry
VERSION: ${DRONE_COMMIT_SHA:0:8}
commands:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SWARM_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H $SWARM_HOST >> ~/.ssh/known_hosts
- |
ssh $SWARM_USER@$SWARM_HOST << EOF
cd /opt/wedding-photo-sharing
export REGISTRY_URL=registry.local.nothaft.cloud
export VERSION=$VERSION
docker stack deploy -c deploy/docker-stack.yml wedding-photo-sharing
EOF
when:
branch:
- develop
event:
- push
# Deploy to Production
- name: deploy-production
image: alpine:latest
environment:
SWARM_HOST:
from_secret: prod_swarm_host
SWARM_USER:
from_secret: prod_swarm_user
SWARM_KEY:
from_secret: prod_swarm_key
REGISTRY_URL:
from_secret: docker_registry
VERSION: ${DRONE_TAG:-latest}
commands:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SWARM_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H $SWARM_HOST >> ~/.ssh/known_hosts
- |
ssh $SWARM_USER@$SWARM_HOST << EOF
cd /opt/wedding-photo-sharing
export REGISTRY_URL=registry.local.nothaft.cloud
export VERSION=$VERSION
# Backup database before deployment
docker exec \$(docker ps -q -f name=wedding-photo-sharing_db) pg_dump -U postgres wedding_photo_sharing > /backup/db-backup-\$(date +%Y%m%d-%H%M%S).sql
# Deploy stack
docker stack deploy -c deploy/docker-stack.yml wedding-photo-sharing --with-registry-auth
# Wait for services to be ready
sleep 30
# Run migrations if needed
docker exec \$(docker ps -q -f name=wedding-photo-sharing_backend) npm run migrate
EOF
when:
event:
- tag
# Health Check
- name: health-check
image: alpine:latest
commands:
- apk add --no-cache curl
- sleep 30
- curl -f https://${FRONTEND_HOST}/health || exit 1
- curl -f https://${BACKEND_HOST}/api/health || exit 1
when:
branch:
- main
event:
- push
- tag
# Notification - Success
- name: notify-success
image: plugins/slack
settings:
webhook:
from_secret: slack_webhook
channel: deployments
template: |
✅ *Build {{build.number}} succeeded* for {{repo.name}}
Branch: {{build.branch}}
Commit: {{build.commit}}
Author: {{build.author}}
{{#if build.tag}}
🏷️ Tag: {{build.tag}}
🚀 Deployed to *PRODUCTION*
{{else}}
📦 Deployed to *{{build.branch}}*
{{/if}}
🔗 {{build.link}}
when:
status:
- success
# Notification - Failure
- name: notify-failure
image: plugins/slack
settings:
webhook:
from_secret: slack_webhook
channel: deployments
template: |
❌ *Build {{build.number}} failed* for {{repo.name}}
Branch: {{build.branch}}
Commit: {{build.commit}}
Author: {{build.author}}
🔗 {{build.link}}
when:
status:
- failure
---
kind: pipeline
type: docker
name: rollback
trigger:
event:
- rollback
steps:
- name: rollback-production
image: alpine:latest
environment:
SWARM_HOST:
from_secret: prod_swarm_host
SWARM_USER:
from_secret: prod_swarm_user
SWARM_KEY:
from_secret: prod_swarm_key
REGISTRY_URL:
from_secret: docker_registry
commands:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SWARM_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H $SWARM_HOST >> ~/.ssh/known_hosts
- |
ssh $SWARM_USER@$SWARM_HOST << EOF
cd /opt/wedding-photo-sharing
export REGISTRY_URL=registry.local.nothaft.cloud
export VERSION=${DRONE_ROLLBACK_TO}
# Deploy previous version
docker stack deploy -c deploy/docker-stack.yml wedding-photo-sharing --with-registry-auth
EOF
---
kind: secret
name: slack_webhook
get:
path: drone/slack
name: webhook
+42 -21
View File
@@ -1,26 +1,47 @@
# JWT Secret for authentication
# IMPORTANT: Generate a secure random secret with: openssl rand -hex 32
# NEVER use the default value or commit the actual secret to version control
JWT_SECRET=CHANGE_ME_TO_A_64_CHARACTER_SECURE_RANDOM_STRING_GENERATED_BY_OPENSSL
# PicPeak Development Environment Configuration
# Copy this file to .env for local development
# URLs
ADMIN_URL=https://admin.photos.yourdomain.com
FRONTEND_URL=https://photos.yourdomain.com
# SECURITY WARNING: This configuration is for development only!
# For production, use .env.production.example
# Database (for PostgreSQL in production)
DB_USER=photoapp
DB_PASSWORD=secure-password-here
DB_NAME=photo_sharing
# JWT Secret (Change in production!)
# Generate secure secret with: openssl rand -base64 32
JWT_SECRET=dev-secret-DO-NOT-USE-IN-PRODUCTION
# Email Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
# Application URLs (Docker Compose development setup)
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
# Email Configuration (Mailhog for development)
# Access Mailhog UI at: http://localhost:8025
SMTP_HOST=mailhog
SMTP_PORT=1025
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
EMAIL_FROM=noreply@yourdomain.com
SMTP_USER=
SMTP_PASS=
EMAIL_FROM=noreply@localhost
# Umami Analytics
UMAMI_URL=https://analytics.yourdomain.com
UMAMI_WEBSITE_ID=your-website-id
UMAMI_HASH_SALT=random-salt-here
# Backend Port Configuration
PORT=3001
# Optional: Umami Analytics Backend Config
# NOTE: Primary configuration through Admin UI > Settings > Analytics
# These are fallback values for server-side tracking
# UMAMI_URL=https://analytics.example.com
# UMAMI_WEBSITE_ID=your-website-id
# UMAMI_HASH_SALT=your-hash-salt
# Development Features
NODE_ENV=development
LOG_LEVEL=debug
# Admin Setup Notes:
# 1. Run 'npm run migrate' in backend folder
# 2. Admin credentials will be auto-generated
# 3. Check ADMIN_CREDENTIALS.txt for login details
# 4. Change password on first login (required)
+88 -48
View File
@@ -1,60 +1,100 @@
# Application URLs
FRONTEND_HOST=photos.yourdomain.com
BACKEND_HOST=api.photos.yourdomain.com
ADMIN_URL=https://admin.photos.yourdomain.com
FRONTEND_URL=https://photos.yourdomain.com
# PicPeak Production Configuration
# Copy this file to .env and update with your production values
# Database Configuration
DB_NAME=photo_sharing
DB_USER=photoapp
DB_PASSWORD=your-secure-password-here
# ============================================
# CRITICAL SECURITY - MUST CHANGE ALL VALUES!
# ============================================
# JWT Configuration
JWT_SECRET=your-jwt-secret-here
# JWT Secret - REQUIRED (minimum 32 characters)
# Generate with: openssl rand -base64 32
JWT_SECRET=CHANGE-THIS-PRODUCTION-SECRET-USE-OPENSSL-COMMAND
# Email Configuration
SMTP_HOST=smtp.gmail.com
# Application URLs - REQUIRED (your actual domain)
FRONTEND_URL=https://your-domain.com
BACKEND_URL=https://your-domain.com
ADMIN_URL=https://your-domain.com
# ============================================
# DATABASE CONFIGURATION - REQUIRED
# ============================================
# PostgreSQL Configuration (Recommended for production)
DATABASE_CLIENT=pg
DB_HOST=postgres # or your database host
DB_PORT=5432
DB_NAME=picpeak
DB_USER=picpeak
DB_PASSWORD=CHANGE-THIS-SECURE-DATABASE-PASSWORD
# ============================================
# EMAIL CONFIGURATION - REQUIRED
# ============================================
# Example: Gmail with App Password
# SMTP_HOST=smtp.gmail.com
# SMTP_PORT=587
# SMTP_SECURE=false
# SMTP_USER=your-email@gmail.com
# SMTP_PASS=your-16-char-app-password
# EMAIL_FROM=Your Name <your-email@gmail.com>
# Example: SendGrid
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
EMAIL_FROM=noreply@yourdomain.com
SMTP_USER=apikey
SMTP_PASS=YOUR-SENDGRID-API-KEY
EMAIL_FROM=PicPeak <noreply@your-domain.com>
# Umami Analytics
UMAMI_URL=https://analytics.yourdomain.com
UMAMI_HOST=analytics.yourdomain.com
UMAMI_WEBSITE_ID=your-website-id
UMAMI_HASH_SALT=your-random-salt
UMAMI_DB_PASSWORD=umami-db-password
# ============================================
# ADMIN SETUP - AUTO-GENERATED
# ============================================
# NOTE: Admin credentials are automatically generated during setup
# DO NOT set ADMIN_EMAIL or ADMIN_PASSWORD anymore!
# Run 'npm run migrate' and check ADMIN_CREDENTIALS.txt
# Traefik Configuration
TRAEFIK_HOST=traefik.yourdomain.com
ACME_EMAIL=admin@yourdomain.com
TRAEFIK_DASHBOARD_AUTH=admin:$2y$10$... # Use htpasswd to generate
# ============================================
# OPTIONAL CONFIGURATION
# ============================================
# Docker Registry (optional)
REGISTRY_URL=registry.yourdomain.com
VERSION=latest
# Umami Analytics (Optional - Fallback values)
# Primary config via Admin UI > Settings > Analytics
# UMAMI_URL=https://analytics.your-domain.com
# UMAMI_WEBSITE_ID=your-website-id
# UMAMI_HASH_SALT=your-hash-salt
# Monitoring
DOMAIN=yourdomain.com
GRAFANA_USER=admin
GRAFANA_PASSWORD=your-grafana-password
# Frontend Analytics (Optional - Fallback values)
# VITE_UMAMI_URL=https://analytics.your-domain.com
# VITE_UMAMI_WEBSITE_ID=your-website-id
# VITE_UMAMI_SHARE_URL=https://analytics.your-domain.com/share/xyz/gallery
# OAuth Configuration (optional)
OAUTH_AUTH_URL=https://auth.yourdomain.com/oauth2/auth
OAUTH_TOKEN_URL=https://auth.yourdomain.com/oauth2/token
OAUTH_USER_URL=https://auth.yourdomain.com/oauth2/userinfo
OAUTH_CLIENT_ID=photo-sharing
OAUTH_CLIENT_SECRET=your-oauth-secret
OAUTH_SECRET=your-random-secret
COOKIE_DOMAIN=.yourdomain.com
OAUTH_WHITELIST=admin@yourdomain.com
# ============================================
# PERFORMANCE & SECURITY TUNING
# ============================================
# Backup Configuration (optional)
S3_BACKUP_BUCKET=your-backup-bucket
NODE_ENV=production
PORT=3001
LOG_LEVEL=info
# Drone CI Configuration
DRONE_RPC_SECRET=your-drone-secret
DRONE_GITHUB_CLIENT_ID=your-github-client-id
DRONE_GITHUB_CLIENT_SECRET=your-github-client-secret
# Security Settings (Defaults are secure)
BCRYPT_ROUNDS=12
SESSION_TIMEOUT_MINUTES=60
RATE_LIMIT_WINDOW_MS=900000 # 15 minutes
RATE_LIMIT_MAX_REQUESTS=100 # per window
# Connection Pool (Adjust based on load)
DB_POOL_MIN=5
DB_POOL_MAX=25
# ============================================
# DOCKER COMPOSE SPECIFIC
# ============================================
# Traefik Configuration (if using Traefik)
DOMAIN=your-domain.com
LETSENCRYPT_EMAIL=admin@your-domain.com
# Volume Paths (Docker)
STORAGE_PATH=/app/storage
EVENTS_PATH=/app/storage/events
ARCHIVE_PATH=/app/storage/events/archived
+12
View File
@@ -0,0 +1,12 @@
# 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
+220
View File
@@ -0,0 +1,220 @@
name: Mirror to GitHub
on:
push:
branches:
- main
workflow_dispatch: # Allow manual triggering
jobs:
mirror:
runs-on: ubuntu-latest
steps:
- name: Checkout repository with full history
uses: actions/checkout@v3
with:
fetch-depth: 0 # Full history needed for finding the commit
- name: Setup Git
run: |
git config --global user.name "the-luap"
git config --global user.email "paul-nothaft@hotmail.de"
- name: Debug - Show current branch and status
run: |
echo "Current branch:"
git branch -a
echo "Git status:"
git status
echo "Remote info:"
git remote -v
echo "Checking target commit exists:"
git show --oneline 7aca927937 || echo "Target commit not found!"
- name: Create completely new history from specific commit
run: |
TARGET_COMMIT="7aca927937"
# Verify the target commit exists
if ! git cat-file -e $TARGET_COMMIT^{commit}; then
echo "ERROR: Target commit $TARGET_COMMIT does not exist!"
exit 1
fi
echo "✅ Target commit found: $(git log --oneline -1 $TARGET_COMMIT)"
# Clean up any existing github-mirror branch
git branch -D github-mirror || true
# Create a completely new orphan branch (no history)
git checkout --orphan github-mirror
# Clear the staging area completely
git rm -rf . || true
# Get the file tree from the target commit and create initial commit
echo "Creating new history starting from $TARGET_COMMIT..."
git read-tree $TARGET_COMMIT
git commit -m "Initial commit - imported from $(git log --oneline -1 $TARGET_COMMIT)"
echo "✅ Created new initial commit: $(git log --oneline -1)"
# Now get all commits after the target commit and apply their changes
COMMITS_AFTER_TARGET=$(git rev-list --reverse --no-merges $TARGET_COMMIT..main)
if [ -n "$COMMITS_AFTER_TARGET" ]; then
echo "📋 Applying changes from commits after $TARGET_COMMIT (excluding Claude commits):"
for commit in $COMMITS_AFTER_TARGET; do
# Get the commit author name
COMMIT_AUTHOR_NAME=$(git log --format="%an" -n 1 $commit)
# Skip commits by Claude
if [ "$COMMIT_AUTHOR_NAME" = "Claude" ]; then
echo "⚠️ Skipping commit by Claude: $(git log --oneline -1 $commit)"
continue
fi
echo "Processing: $(git log --oneline -1 $commit)"
# Get the commit message and author info
COMMIT_MSG=$(git log --format="%B" -n 1 $commit)
COMMIT_AUTHOR=$(git log --format="%an <%ae>" -n 1 $commit)
COMMIT_DATE=$(git log --format="%ad" -n 1 $commit)
# Apply the changes from this commit
if git diff-tree --no-commit-id --name-only -r $commit | xargs -I {} git show $commit:{} > /dev/null 2>&1; then
# Apply file changes
git checkout $commit -- . || true
# Stage all changes
git add -A
# Only commit if there are changes
if ! git diff --cached --quiet; then
# Create new commit with original metadata but new SHA
GIT_AUTHOR_NAME=$(echo "$COMMIT_AUTHOR" | cut -d'<' -f1 | xargs)
GIT_AUTHOR_EMAIL=$(echo "$COMMIT_AUTHOR" | cut -d'<' -f2 | cut -d'>' -f1)
GIT_AUTHOR_DATE="$COMMIT_DATE"
export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
git commit -m "$COMMIT_MSG"
echo "✅ Applied changes as new commit: $(git log --oneline -1)"
else
echo "⚠️ No changes to commit for $commit"
fi
else
echo "⚠️ Skipping problematic commit $commit"
fi
done
echo "✅ Finished creating new history"
else
echo "✅ No commits after target commit - history starts fresh"
fi
echo ""
echo "=== New History Summary ==="
echo "Total commits in new history: $(git rev-list --count github-mirror)"
echo "History starts with: $(git log --oneline --reverse | head -1)"
echo "Latest commit: $(git log --oneline -1)"
- name: Remove sensitive files and directories
run: |
# Switch to the github-mirror branch
git checkout github-mirror
echo "Current files before cleanup:"
ls -la | head -10 || true
echo "..."
# Remove sensitive files/directories if they exist
echo "Removing sensitive files..."
rm -rf .env || true
rm -rf backend/.env* || true
rm -rf frontend/.env* || true
rm -rf docker-compose.prod.yml || true
rm -rf .claudedocs/ || true
rm -rf backend/data/ || true
rm -rf backend/storage/ || true
rm -rf .gitea/ || true
rm -rf scripts/install-gitea-runner.sh || true
rm -rf .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"
# Add and commit the cleanup if there are changes
git add -A
if ! git diff --cached --quiet; then
git commit -m "chore: remove sensitive files for GitHub mirror"
echo "✅ Committed cleanup of sensitive files"
else
echo "✅ No sensitive files to remove"
fi
echo "Final file structure (top level):"
ls -la | head -10 || true
- name: Verify completely new history
run: |
git checkout github-mirror
echo "=== Final History Verification ==="
echo "Total commits in new github-mirror branch: $(git rev-list --count github-mirror)"
echo ""
echo "Complete commit history (should start from target commit content):"
git log --oneline --reverse
echo ""
echo "⚠️ Note: This is a completely NEW history with new commit SHAs"
echo "🔍 Original target commit content preserved but with new commit ID"
- name: Check GitHub token
env:
GITHUBTOKEN: ${{ secrets.GITHUBTOKEN }}
run: |
if [ -z "$GITHUBTOKEN" ]; then
echo "ERROR: GITHUBTOKEN secret is not set!"
exit 1
else
echo "GitHub token is available (length: ${#GITHUBTOKEN})"
fi
- name: Force push completely new history to GitHub
env:
GITHUBTOKEN: ${{ secrets.GITHUBTOKEN }}
run: |
# Switch to github-mirror branch
git checkout github-mirror
# Remove existing github remote if it exists
git remote remove github || true
# Add GitHub remote
git remote add github https://x-access-token:${GITHUBTOKEN}@github.com/the-luap/picpeak.git
# Verify remote was added
echo "GitHub remote added:"
git remote -v
# Force push the completely new history to GitHub main
echo "🔥 FORCE PUSHING completely new history to GitHub..."
echo "⚠️ This will COMPLETELY REPLACE all history on GitHub!"
git push github github-mirror:main --force
echo "✅ Force push completed - GitHub now has completely new history!"
- name: Workflow completed
run: |
echo "✅ Mirror to GitHub workflow completed successfully!"
echo "🔥 COMPLETE HISTORY REPLACEMENT: GitHub now has entirely new history"
echo "📊 History starts from commit content: 7aca927937"
echo "🔍 Check https://github.com/the-luap/picpeak to verify the new history"
echo "📈 Total commits pushed: $(git rev-list --count github-mirror)"
echo "🆕 All commit SHAs are NEW - no connection to previous history"
+196 -17
View File
@@ -14,6 +14,7 @@ jobs:
outputs:
new_version: ${{ steps.version.outputs.new_version }}
version_changed: ${{ steps.version.outputs.version_changed }}
component_changed: ${{ steps.version.outputs.component_changed }}
steps:
- uses: actions/checkout@v3
with:
@@ -30,15 +31,104 @@ jobs:
git config --global user.name 'Gitea Actions Bot'
git config --global user.email 'actions@gitea.local'
- name: Bump version
- name: Detect changes and bump version
id: version
run: |
# Get current version from backend package.json
CURRENT_VERSION=$(node -p "require('./backend/package.json').version")
echo "Current version: $CURRENT_VERSION"
set -e # Exit on error
# Split version into parts
IFS='.' read -r -a version_parts <<< "$CURRENT_VERSION"
echo "=== Debug Info ==="
echo "GitHub event before: ${{ github.event.before }}"
echo "GitHub SHA: ${{ github.sha }}"
echo "Current directory: $(pwd)"
echo "Git log (last 5): $(git log --oneline -5)"
# Get the commit range for changed files
if [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ] && [ "${{ github.event.before }}" != "" ]; then
COMMIT_RANGE="${{ github.event.before }}..${{ github.sha }}"
echo "Using commit range: $COMMIT_RANGE"
CHANGED_FILES=$(git diff --name-only $COMMIT_RANGE || echo "")
else
# First commit or no previous commit, check against HEAD~1 if it exists
if git rev-parse HEAD~1 >/dev/null 2>&1; then
COMMIT_RANGE="HEAD~1..HEAD"
echo "Using commit range: $COMMIT_RANGE"
CHANGED_FILES=$(git diff --name-only $COMMIT_RANGE || echo "")
else
echo "First commit detected, checking all files"
CHANGED_FILES=$(git ls-files)
fi
fi
echo "Changed files:"
echo "$CHANGED_FILES"
# Check what changed (using echo to pipe to grep to avoid grep exit codes)
BACKEND_CHANGED=$(echo "$CHANGED_FILES" | grep -c '^backend/' || echo "0")
FRONTEND_CHANGED=$(echo "$CHANGED_FILES" | grep -c '^frontend/' || echo "0")
ROOT_CHANGED=$(echo "$CHANGED_FILES" | grep -c -E '^(package\.json|docker-compose|Dockerfile|scripts/)' || echo "0")
echo "Backend files changed: $BACKEND_CHANGED"
echo "Frontend files changed: $FRONTEND_CHANGED"
echo "Root files changed: $ROOT_CHANGED"
# Get current versions
BACKEND_VERSION=$(node -p "require('./backend/package.json').version" 2>/dev/null || echo "1.0.0")
FRONTEND_VERSION=$(node -p "require('./frontend/package.json').version" 2>/dev/null || echo "1.0.0")
echo "Current backend version: $BACKEND_VERSION"
echo "Current frontend version: $FRONTEND_VERSION"
# Determine what to update based on changes
BACKEND_UPDATE=false
FRONTEND_UPDATE=false
COMPONENT_CHANGED="none"
if [ "$ROOT_CHANGED" -gt 0 ]; then
# Root changes affect both components
BACKEND_UPDATE=true
FRONTEND_UPDATE=true
COMPONENT_CHANGED="both"
SOURCE_VERSION=$BACKEND_VERSION
echo "Root changes detected - updating both components"
elif [ "$BACKEND_CHANGED" -gt 0 ] && [ "$FRONTEND_CHANGED" -gt 0 ]; then
# Both components changed
BACKEND_UPDATE=true
FRONTEND_UPDATE=true
COMPONENT_CHANGED="both"
# Use the higher version as source
if [ "$(printf '%s\n' "$BACKEND_VERSION" "$FRONTEND_VERSION" | sort -V | tail -n1)" = "$BACKEND_VERSION" ]; then
SOURCE_VERSION=$BACKEND_VERSION
else
SOURCE_VERSION=$FRONTEND_VERSION
fi
echo "Both backend and frontend changed - updating both"
elif [ "$BACKEND_CHANGED" -gt 0 ]; then
# Only backend changed
BACKEND_UPDATE=true
COMPONENT_CHANGED="backend"
SOURCE_VERSION=$BACKEND_VERSION
echo "Only backend changed - updating backend"
elif [ "$FRONTEND_CHANGED" -gt 0 ]; then
# Only frontend changed
FRONTEND_UPDATE=true
COMPONENT_CHANGED="frontend"
SOURCE_VERSION=$FRONTEND_VERSION
echo "Only frontend changed - updating frontend"
else
echo "No relevant changes detected"
echo "version_changed=false" >> $GITHUB_OUTPUT
echo "component_changed=none" >> $GITHUB_OUTPUT
echo "new_version=" >> $GITHUB_OUTPUT
exit 0
fi
echo "Component changed: $COMPONENT_CHANGED"
echo "Source version: $SOURCE_VERSION"
echo "Backend update: $BACKEND_UPDATE"
echo "Frontend update: $FRONTEND_UPDATE"
# Calculate new version
IFS='.' read -r -a version_parts <<< "$SOURCE_VERSION"
MAJOR="${version_parts[0]}"
MINOR="${version_parts[1]}"
PATCH="${version_parts[2]}"
@@ -49,14 +139,23 @@ jobs:
echo "New version: $NEW_VERSION"
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "component_changed=$COMPONENT_CHANGED" >> $GITHUB_OUTPUT
# Update version in package.json files
cd backend && npm version $NEW_VERSION --no-git-tag-version
cd ../frontend && npm version $NEW_VERSION --no-git-tag-version
cd ..
# Update versions in package.json files
if [ "$BACKEND_UPDATE" = true ]; then
echo "Updating backend version to $NEW_VERSION"
cd backend && npm version $NEW_VERSION --no-git-tag-version
cd ..
fi
# Check if there are changes
if [[ -n $(git status -s) ]]; then
if [ "$FRONTEND_UPDATE" = true ]; then
echo "Updating frontend version to $NEW_VERSION"
cd frontend && npm version $NEW_VERSION --no-git-tag-version
cd ..
fi
# Check if there are changes to commit
if [[ -n $(git status --porcelain) ]]; then
echo "version_changed=true" >> $GITHUB_OUTPUT
else
echo "version_changed=false" >> $GITHUB_OUTPUT
@@ -65,15 +164,94 @@ jobs:
- name: Commit version bump
if: steps.version.outputs.version_changed == 'true'
run: |
git add backend/package.json backend/package-lock.json
git add frontend/package.json frontend/package-lock.json
git commit -m "chore: bump version to ${{ steps.version.outputs.new_version }}"
git push
set -e # Exit on any error
# First, ensure we have the latest changes
echo "Fetching latest changes..."
git fetch origin main
# Check if we're behind and need to update
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse origin/main)
if [ "$LOCAL" != "$REMOTE" ]; then
echo "Local is behind remote, pulling changes..."
git pull origin main --no-rebase
fi
COMPONENT="${{ steps.version.outputs.component_changed }}"
if [ "$COMPONENT" = "both" ]; then
git add backend/package.json backend/package-lock.json frontend/package.json frontend/package-lock.json
git commit -m "chore: bump version to ${{ steps.version.outputs.new_version }} (backend + frontend)"
elif [ "$COMPONENT" = "backend" ]; then
git add backend/package.json backend/package-lock.json
git commit -m "chore: bump backend version to ${{ steps.version.outputs.new_version }}"
elif [ "$COMPONENT" = "frontend" ]; then
git add frontend/package.json frontend/package-lock.json
git commit -m "chore: bump frontend version to ${{ steps.version.outputs.new_version }}"
fi
# Pull latest changes before pushing to avoid conflicts
echo "Pulling latest changes from origin/main..."
if ! git pull --rebase origin main; then
echo "Rebase failed, attempting to resolve..."
# If rebase fails, abort and try a regular merge
git rebase --abort || true
git pull origin main --no-rebase
fi
# Push the changes with retry logic
echo "Pushing version bump..."
PUSH_SUCCESS=false
for i in 1 2 3; do
echo "Push attempt $i of 3..."
# Try to push
if git push origin main 2>&1; then
echo "Successfully pushed version bump on attempt $i"
PUSH_SUCCESS=true
break
else
echo "Push failed on attempt $i"
if [ $i -lt 3 ]; then
echo "Waiting 5 seconds before retry..."
sleep 5
echo "Pulling latest changes..."
git fetch origin main
# Try rebase first, fall back to merge
if ! git rebase origin/main; then
echo "Rebase failed, trying merge..."
git rebase --abort 2>/dev/null || true
git pull origin main --no-rebase
fi
fi
fi
done
if [ "$PUSH_SUCCESS" = "false" ]; then
echo "ERROR: Failed to push after 3 attempts"
exit 1
fi
- name: Create Git tag
if: steps.version.outputs.version_changed == 'true'
run: |
git tag -a "v${{ steps.version.outputs.new_version }}" -m "Release v${{ steps.version.outputs.new_version }}"
COMPONENT="${{ steps.version.outputs.component_changed }}"
if [ "$COMPONENT" = "both" ]; then
TAG_MESSAGE="Release v${{ steps.version.outputs.new_version }} (backend + frontend)"
elif [ "$COMPONENT" = "backend" ]; then
TAG_MESSAGE="Release v${{ steps.version.outputs.new_version }} (backend)"
elif [ "$COMPONENT" = "frontend" ]; then
TAG_MESSAGE="Release v${{ steps.version.outputs.new_version }} (frontend)"
fi
git tag -a "v${{ steps.version.outputs.new_version }}" -m "$TAG_MESSAGE"
git push origin "v${{ steps.version.outputs.new_version }}"
trigger-drone:
@@ -84,5 +262,6 @@ jobs:
- name: Trigger Drone Build
run: |
echo "Version bumped to ${{ needs.version-bump.outputs.new_version }}"
echo "Component(s) changed: ${{ needs.version-bump.outputs.component_changed }}"
echo "Drone will automatically trigger on the new tag"
# Drone CI will automatically trigger on the tag push event
+24
View File
@@ -0,0 +1,24 @@
# 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
+47
View File
@@ -0,0 +1,47 @@
---
name: Bug report
about: Create a report to help us improve PicPeak
title: '[BUG] '
labels: 'bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- OS: [e.g. Ubuntu 22.04]
- Browser: [e.g. Chrome 120, Safari 17]
- PicPeak Version: [e.g. 1.0.22]
- Deployment Method: [e.g. Docker Compose, Manual]
- Database: [e.g. PostgreSQL 15, SQLite]
**Logs**
Please include relevant logs:
```
# Backend logs
docker-compose logs backend | tail -50
# Frontend console errors
[paste any browser console errors]
```
**Additional context**
Add any other context about the problem here.
**Possible Solution**
If you have an idea how to fix the issue, please describe it here.
+11
View File
@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: 📚 Documentation
url: https://github.com/the-luap/picpeak/blob/main/DEPLOYMENT.md
about: Please read the documentation before opening an issue
- name: 💬 Discussions
url: https://github.com/the-luap/picpeak/discussions
about: Ask questions and discuss with the community
- name: 🔒 Security Issues
url: https://github.com/the-luap/picpeak/blob/main/SECURITY.md
about: Please review our security policy for reporting vulnerabilities
+33
View File
@@ -0,0 +1,33 @@
---
name: Documentation
about: Report issues or improvements needed in documentation
title: '[DOCS] '
labels: 'documentation'
assignees: ''
---
**What documentation needs improvement?**
Please specify which document or section needs attention:
- [ ] README.md
- [ ] DEPLOYMENT.md
- [ ] CONTRIBUTING.md
- [ ] API Documentation
- [ ] Code Comments
- [ ] Other: ___________
**Describe the issue**
What's wrong or missing in the documentation?
**Suggested improvement**
How would you improve this documentation?
**Target audience**
Who is this documentation for?
- [ ] New users setting up PicPeak
- [ ] Developers contributing to the project
- [ ] System administrators
- [ ] End users (photographers/clients)
**Additional context**
Add any other context, examples, or references here.
+38
View File
@@ -0,0 +1,38 @@
---
name: Feature request
about: Suggest an idea for PicPeak
title: '[FEATURE] '
labels: 'enhancement'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Use Case**
Please describe how this feature would be used:
- Who would use it? (photographers, clients, admins)
- When would they use it?
- Why is it important?
**Similar Features**
Are there similar features in:
- PicDrop
- Scrapbook.de
- Other photo sharing platforms
**Mockups or Examples**
If applicable, add mockups, diagrams, or links to similar implementations.
**Additional context**
Add any other context or screenshots about the feature request here.
**Implementation Ideas**
If you have technical ideas about how this could be implemented, please share them.
+26
View File
@@ -0,0 +1,26 @@
---
name: Question
about: Ask a question about PicPeak
title: '[QUESTION] '
labels: 'question'
assignees: ''
---
**Question**
What would you like to know about PicPeak?
**Context**
Please provide context to help us answer your question better:
- What are you trying to achieve?
- What have you already tried?
- Which documentation have you consulted?
**Environment**
If relevant to your question:
- PicPeak Version:
- Deployment Method:
- Operating System:
**Related Issues or Discussions**
Link to any related issues, discussions, or documentation.
@@ -0,0 +1,37 @@
---
name: Security Vulnerability
about: Report security issues privately
title: '[SECURITY] '
labels: 'security'
assignees: ''
---
⚠️ **IMPORTANT: For serious security vulnerabilities, please DO NOT create a public issue.**
Instead, please email security@example.com with the details.
For minor security improvements or questions, you can use this template:
**Type of Security Issue**
- [ ] Authentication/Authorization
- [ ] Data Exposure
- [ ] Input Validation
- [ ] Configuration Issue
- [ ] Dependency Vulnerability
- [ ] Other: ___________
**Description**
Brief description of the security concern.
**Impact**
What could an attacker potentially do?
**Steps to Reproduce**
If applicable, how can this be reproduced?
**Suggested Fix**
If you have ideas on how to fix this issue.
**References**
Any relevant security advisories, CVEs, or documentation.
+49
View File
@@ -0,0 +1,49 @@
## Description
Please include a summary of the changes and which issue is fixed. Include relevant motivation and context.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.
- [ ] Unit tests pass (`npm test`)
- [ ] Manual testing completed
- [ ] Tested on Docker deployment
- [ ] Tested on production-like environment
**Test Configuration**:
* PicPeak Version:
* Node.js Version:
* Database: PostgreSQL / SQLite
* Browser:
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published
- [ ] I have updated the CHANGELOG.md file
## Screenshots (if appropriate):
## Additional Notes:
Add any additional notes, concerns, or discussion points here.
-108
View File
@@ -1,108 +0,0 @@
name: Create Release
on:
push:
branches:
- main
paths:
- 'frontend/package.json'
- 'backend/package.json'
jobs:
check-version-change:
runs-on: ubuntu-latest
outputs:
version_changed: ${{ steps.check.outputs.changed }}
new_version: ${{ steps.check.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check if version changed
id: check
run: |
# Get current versions
FRONTEND_VERSION=$(node -p "require('./frontend/package.json').version")
BACKEND_VERSION=$(node -p "require('./backend/package.json').version")
# Get previous versions
git checkout HEAD~1
PREV_FRONTEND_VERSION=$(node -p "require('./frontend/package.json').version" 2>/dev/null || echo "0.0.0")
PREV_BACKEND_VERSION=$(node -p "require('./backend/package.json').version" 2>/dev/null || echo "0.0.0")
# Check if versions changed
if [[ "$FRONTEND_VERSION" != "$PREV_FRONTEND_VERSION" ]] || [[ "$BACKEND_VERSION" != "$PREV_BACKEND_VERSION" ]]; then
echo "changed=true" >> $GITHUB_OUTPUT
echo "version=$FRONTEND_VERSION" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
create-release:
needs: check-version-change
if: needs.check-version-change.outputs.version_changed == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Generate Changelog
id: changelog
run: |
# Get commits since last tag
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [[ -z "$LAST_TAG" ]]; then
COMMITS=$(git log --oneline)
else
COMMITS=$(git log ${LAST_TAG}..HEAD --oneline)
fi
# Format changelog
echo "## What's Changed" > changelog.md
echo "" >> changelog.md
# Group commits by type
echo "### Features" >> changelog.md
echo "$COMMITS" | grep -E "^[a-f0-9]+ feat:" | sed 's/^[a-f0-9]+ /- /' >> changelog.md || echo "*No new features*" >> changelog.md
echo "" >> changelog.md
echo "### Bug Fixes" >> changelog.md
echo "$COMMITS" | grep -E "^[a-f0-9]+ fix:" | sed 's/^[a-f0-9]+ /- /' >> changelog.md || echo "*No bug fixes*" >> changelog.md
echo "" >> changelog.md
echo "### Other Changes" >> changelog.md
echo "$COMMITS" | grep -vE "^[a-f0-9]+ (feat|fix):" | sed 's/^[a-f0-9]+ /- /' >> changelog.md || echo "*No other changes*" >> changelog.md
# Save changelog
echo "changelog<<EOF" >> $GITHUB_OUTPUT
cat changelog.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ needs.check-version-change.outputs.new_version }}
name: Release v${{ needs.check-version-change.outputs.new_version }}
body: |
## PicPeak v${{ needs.check-version-change.outputs.new_version }}
${{ steps.changelog.outputs.changelog }}
### Docker Images
To use this release with Docker:
```bash
docker pull ghcr.io/${{ github.repository }}/frontend:v${{ needs.check-version-change.outputs.new_version }}
docker pull ghcr.io/${{ github.repository }}/backend:v${{ needs.check-version-change.outputs.new_version }}
```
Or use the `latest` tag for the most recent version.
draft: false
prerelease: false
generate_release_notes: true
-107
View File
@@ -1,107 +0,0 @@
name: Automatic Version Bump
on:
push:
branches:
- main
workflow_dispatch:
inputs:
version_type:
description: 'Version bump type'
required: true
default: 'patch'
type: choice
options:
- patch
- minor
- major
jobs:
version-bump:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Configure Git
run: |
git config --global user.name "GitHub Actions Bot"
git config --global user.email "actions@github.com"
- name: Determine version type
id: version_type
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "type=${{ github.event.inputs.version_type }}" >> $GITHUB_OUTPUT
else
# Auto-detect version type based on commit message
COMMIT_MSG="${{ github.event.head_commit.message }}"
if [[ "$COMMIT_MSG" == *"BREAKING CHANGE"* ]] || [[ "$COMMIT_MSG" == *"!"* ]]; then
echo "type=major" >> $GITHUB_OUTPUT
elif [[ "$COMMIT_MSG" == *"feat:"* ]] || [[ "$COMMIT_MSG" == *"feat("* ]]; then
echo "type=minor" >> $GITHUB_OUTPUT
else
echo "type=patch" >> $GITHUB_OUTPUT
fi
fi
- name: Bump Frontend Version
id: frontend_version
working-directory: ./frontend
run: |
npm version ${{ steps.version_type.outputs.type }} --no-git-tag-version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Bump Backend Version
id: backend_version
working-directory: ./backend
run: |
npm version ${{ steps.version_type.outputs.type }} --no-git-tag-version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Update Frontend VersionInfo component
run: |
VERSION=${{ steps.frontend_version.outputs.version }}
sed -i "s/const FRONTEND_VERSION = '[^']*'/const FRONTEND_VERSION = '$VERSION'/" frontend/src/components/admin/VersionInfo.tsx
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: bump version to ${{ steps.frontend_version.outputs.version }}"
title: "chore: bump version to ${{ steps.frontend_version.outputs.version }}"
body: |
## Version Bump
This PR automatically bumps the version numbers:
- Frontend: `${{ steps.frontend_version.outputs.version }}`
- Backend: `${{ steps.backend_version.outputs.version }}`
### Version Type: ${{ steps.version_type.outputs.type }}
### Files Changed:
- `frontend/package.json`
- `backend/package.json`
- `frontend/src/components/admin/VersionInfo.tsx`
---
*This PR was automatically created by the version bump workflow.*
branch: version-bump-${{ steps.frontend_version.outputs.version }}
delete-branch: true
labels: |
version-bump
automated
+2
View File
@@ -54,3 +54,5 @@ coverage/
!storage/thumbnails/.gitkeep
!data/.gitkeep
!logs/.gitkeep
PRODUCTION_DEPLOYMENT_GUIDE.md
+152
View File
@@ -0,0 +1,152 @@
# 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
-118
View File
@@ -1,118 +0,0 @@
# CI/CD Strategy for PicPeak
## Overview
This document outlines the CI/CD strategy using both Gitea Actions and Drone CI to avoid conflicts and ensure proper versioning.
## Pipeline Flow
### 1. Development & Testing (Gitea Actions)
- **Trigger**: Every push to `main` or `develop` branches
- **File**: `.gitea/workflows/test.yml`
- **Purpose**: Run tests, linting, and basic validation
- **Actions**:
- Backend linting and tests
- Frontend linting and build
- Does NOT build Docker images
### 2. Version Management (Gitea Actions)
- **Trigger**: Push to `main` branch (excluding markdown files)
- **File**: `.gitea/workflows/version-and-release.yml`
- **Purpose**: Automatic version incrementing
- **Actions**:
1. Reads current version from `package.json`
2. Increments patch version (e.g., 1.0.0 → 1.0.1)
3. Updates both backend and frontend `package.json`
4. Commits the version change
5. Creates a git tag (e.g., `v1.0.1`)
6. Pushes changes and tag
### 3. Docker Image Building (Drone CI)
- **Trigger**:
- Push to `main` or `develop` (builds with commit SHA)
- New git tags (builds release versions)
- **File**: `.drone.yml`
- **Purpose**: Build and push Docker images
- **Tags Created**:
- `latest` - Always points to newest build
- `{commit-sha}` - Specific commit version
- `{branch}-latest` - Latest for specific branch
- `v1.0.1` - Specific version (on tag trigger)
## Why This Strategy?
1. **Separation of Concerns**:
- Gitea Actions handles code quality and versioning
- Drone CI handles Docker image building
- No overlap or race conditions
2. **Sequential Execution**:
- Version bump happens first
- Tag creation triggers Drone
- Docker images are built with correct version
3. **Version Consistency**:
- Version in `package.json` matches git tag
- Docker images are tagged with same version
- No manual version management needed
## Setup Requirements
1. **Gitea Actions Runner**: Must be configured and running
2. **Drone CI**: Must be connected to your Gitea instance
3. **Secrets**:
- `GITEA_TOKEN` (optional, for pushing version commits)
- Docker registry credentials in Drone
## Version Numbering
- Format: `MAJOR.MINOR.PATCH` (e.g., 1.0.0)
- Automatic increments: PATCH version only
- Manual increments: Edit `package.json` for MAJOR/MINOR changes
## Usage
1. **Regular Development**:
```bash
git add .
git commit -m "feat: add new feature"
git push origin main
```
- Tests run automatically
- Version bumps to 1.0.1
- Docker images built with v1.0.1 tag
2. **Major/Minor Version Change**:
```bash
# Manually edit package.json files to 2.0.0
git add .
git commit -m "feat!: major release"
git push origin main
```
3. **Skip Version Bump**:
- Add `[skip ci]` to commit message
- Or only change markdown files
## Monitoring
- **Gitea Actions**: Check Actions tab in Gitea
- **Drone CI**: Check Drone dashboard
- **Docker Registry**: Verify images are pushed with correct tags
## Troubleshooting
1. **Version not incrementing**:
- Check Gitea Actions logs
- Ensure runner has push permissions
- Verify no `[skip ci]` in commit message
2. **Docker images not building**:
- Check Drone CI webhook configuration
- Verify Drone can see the repository
- Check Docker registry credentials
3. **Conflicts**:
- Never run both pipelines for same task
- Use branch protection to prevent direct pushes
- Always let automation handle versioning
+105 -1
View File
@@ -39,6 +39,12 @@ docker-compose -f docker-compose.prod.yml up -d # Production deployment
pm2 start ecosystem.config.js # Alternative: PM2 deployment
```
**⚠️ CRITICAL PRODUCTION NOTICE:**
- Production runs on a SEPARATE SERVER - never assume local changes affect production
- ALWAYS request production server details before any troubleshooting
- NO trial-and-error approaches in production - data loss is unacceptable
- Every change must be thoroughly analyzed and tested locally first
## Key Product Requirements (from PRD)
### Core Features
@@ -111,6 +117,7 @@ Background services run as separate processes:
- **archiveService**: Creates ZIP archives of expired events
- **expirationChecker**: Cron job for expiration warnings
- **fileWatcher**: Monitors for new photo uploads
- **backupService**: Scheduled backups with checksum-based change detection
### API Structure
- `/api/admin/*` - Admin panel endpoints (requires adminAuth)
@@ -127,6 +134,41 @@ Background services run as separate processes:
5. **Frontend Status**: Only skeleton exists - requires full implementation based on PRD
6. **Umami Analytics**: Track password entries, downloads, views, expiration warnings
## Troubleshooting Guidelines
### Before ANY Production Troubleshooting:
1. **ALWAYS request specific details**:
- Production server URL/IP
- Current error messages/logs
- Recent changes or deployments
- Affected users/galleries
- Time of issue occurrence
2. **Thorough Analysis Required**:
- Use detailed thinking/analysis for EVERY troubleshooting task
- Review all related code before suggesting changes
- Consider all potential side effects
- Never make assumptions about production environment
3. **Safe Troubleshooting Steps**:
- First, reproduce issue in local/dev environment
- Analyze logs without modifying production
- Create detailed action plan before any changes
- Always have rollback strategy ready
- Document every step taken
### Common Issues & Safe Approaches:
- **Email not sending**: Check email_queue table, SMTP settings, service status
- **Photos not loading**: Verify file permissions, storage paths, nginx config
- **Gallery access issues**: Check JWT tokens, expiration dates, access_logs
- **Performance problems**: Analyze with monitoring tools first, never experiment
### Data Safety Rules:
- NEVER delete or modify production data without explicit backup confirmation
- ALWAYS verify backups exist before any data operations
- NO direct database modifications without transaction safety
- Log all actions for audit trail
## Environment Variables
### Backend (.env)
@@ -253,9 +295,71 @@ const { theme, setTheme, setThemeByName } = useTheme();
--border-radius: 0.5rem;
```
## Backup Service
### Overview
The backup service provides automated, scheduled backups of all photo data with checksum-based change detection to minimize transfer overhead.
### Features
- **Multiple Destinations**: Local directory, remote server (rsync), S3-compatible storage
- **Change Detection**: SHA256 checksums track file changes, only modified files are backed up
- **Scheduled Execution**: Configurable cron-based scheduling (default: 2 AM daily)
- **Email Notifications**: Alerts on backup failure, optional success notifications
- **Retention Management**: Automatic cleanup of old backup runs based on retention policy
- **Progress Tracking**: Database storage of backup history, file states, and statistics
### Configuration
Backup settings are stored in `app_settings` table with `backup_` prefix:
- `backup_enabled`: Enable/disable the service
- `backup_schedule`: Cron expression (e.g., '0 2 * * *')
- `backup_destination_type`: 'local', 'rsync', or 's3'
- `backup_retention_days`: How long to keep backup history
- `backup_include_archived`: Whether to backup archived events
- `backup_exclude_patterns`: File patterns to exclude
### API Endpoints
- `GET /api/admin/backup/config` - Get current configuration
- `PUT /api/admin/backup/config` - Update configuration
- `GET /api/admin/backup/status` - Get backup status and history
- `POST /api/admin/backup/run` - Trigger manual backup
- `POST /api/admin/backup/test-connection` - Test destination connectivity
### Testing
Run backup service test: `npm run test-backup`
### Database Tables
- `backup_runs`: Tracks each backup execution with statistics
- `backup_file_states`: Stores file checksums for change detection
## Success Metrics (from PRD)
- Time to generate gallery: <2 minutes
- Guest satisfaction: >90%
- System uptime: 99.9%
- Email delivery rate: >98%
- Successful archiving: 100%
- Successful archiving: 100%
## Documentation & Development Practices
### Documentation Guidelines:
- **NEVER create new documentation files for simple tasks**
- **ALWAYS update existing documentation (like this CLAUDE.md)**
- Only create new .md files when explicitly requested
- Avoid creating temporary scripts for one-off tasks
### Development Best Practices:
- Test all changes thoroughly in local environment first
- Use version control for all changes
- Keep commits atomic and well-described
- Review impact on all integrated services
- Consider backward compatibility
- Update tests when changing functionality
### Production Deployment Checklist:
- [ ] All tests passing locally
- [ ] Linting and type checks pass
- [ ] Database migrations tested with rollback plan
- [ ] Environment variables documented
- [ ] Backup strategy confirmed
- [ ] Monitoring alerts configured
- [ ] Rollback procedure documented
- [ ] Stakeholders notified of maintenance window
+27
View File
@@ -0,0 +1,27 @@
# PicPeak Community Guidelines
## Our Commitment
We are committed to providing a welcoming and inspiring community for all photographers and developers.
## Expected Behavior
* Be respectful and considerate
* Welcome newcomers and help them get started
* Focus on what is best for the community
* Show empathy towards other community members
## Unacceptable Behavior
* Trolling or insulting comments
* Personal attacks
* Public or private harassment
* Publishing others' private information
## 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.
## Attribution
This Code of Conduct is adapted from contributor-covenant.org, version 2.0.
+160
View File
@@ -0,0 +1,160 @@
# Contributing to PicPeak
First off, thank you for considering contributing to PicPeak! It's people like you that make PicPeak such a great tool for photographers worldwide.
## 🤝 Code of Conduct
This project and everyone participating in it is governed by the [PicPeak Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.
## 🎯 How Can I Contribute?
### Reporting Bugs
Before creating bug reports, please check the existing issues as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible:
* **Use a clear and descriptive title**
* **Describe the exact steps to reproduce the problem**
* **Provide specific examples to demonstrate the steps**
* **Describe the behavior you observed and what you expected**
* **Include screenshots if possible**
* **Include your environment details** (OS, browser, Docker version, etc.)
### Suggesting Enhancements
Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion, please include:
* **Use a clear and descriptive title**
* **Provide a detailed description of the suggested enhancement**
* **Provide specific examples to demonstrate the enhancement**
* **Describe the current behavior and expected behavior**
* **Explain why this enhancement would be useful**
### Your First Code Contribution
Unsure where to begin? You can start by looking through these issues:
* [Good first issues](https://github.com/the-luap/picpeak/labels/good%20first%20issue) - issues which should only require a few lines of code
* [Help wanted issues](https://github.com/the-luap/picpeak/labels/help%20wanted) - issues which need extra attention
### Pull Requests
1. **Fork the repo** and create your branch from `main`
2. **Install dependencies**:
```bash
cd backend && npm install
cd ../frontend && npm install
```
3. **Make your changes** and ensure:
- Code follows the existing style
- Tests pass: `npm test`
- Linting passes: `npm run lint`
4. **Write tests** if you've added code
5. **Update documentation** if needed
6. **Create a Pull Request**
## 💻 Development Setup
### Prerequisites
- Node.js 18+
- Docker & Docker Compose
- Git
### Local Development
```bash
# Clone your fork
git clone https://github.com/your-username/picpeak.git
cd picpeak
# Install dependencies
cd backend && npm install
cd ../frontend && npm install
# Set up environment
cp .env.example .env
# Edit .env with your settings
# Start development servers
docker-compose -f docker-compose.dev.yml up
```
### Running Tests
```bash
# Backend tests
cd backend && npm test
# Frontend tests
cd frontend && npm test
# E2E tests
npm run test:e2e
```
## 📝 Styleguides
### Git Commit Messages
* Use the present tense ("Add feature" not "Added feature")
* Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
* Limit the first line to 72 characters or less
* Reference issues and pull requests liberally after the first line
* Consider starting the commit message with an applicable emoji:
* 🎨 `:art:` when improving the format/structure of the code
* 🐛 `:bug:` when fixing a bug
* 🔥 `:fire:` when removing code or files
* 📝 `:memo:` when writing docs
* 🚀 `:rocket:` when improving performance
* ✨ `:sparkles:` when adding a new feature
### JavaScript/TypeScript Styleguide
* Use ES6+ features
* Prefer async/await over promises
* Use meaningful variable names
* Add JSDoc comments for functions
* Follow ESLint rules
### React Styleguide
* Use functional components with hooks
* Keep components small and focused
* Use TypeScript for type safety
* Follow the existing folder structure
* Write tests for new components
## 📦 Project Structure
```
picpeak/
├── backend/
│ ├── src/
│ │ ├── routes/ # API endpoints
│ │ ├── services/ # Business logic
│ │ ├── middleware/ # Express middleware
│ │ └── utils/ # Utilities
│ └── migrations/ # Database migrations
├── frontend/
│ ├── src/
│ │ ├── components/ # Reusable components
│ │ ├── pages/ # Page components
│ │ ├── services/ # API services
│ │ └── hooks/ # Custom hooks
│ └── public/ # Static assets
```
## 🔄 Release Process
1. Update version numbers in package.json files
2. Update CHANGELOG.md
3. Create a new release on GitHub
4. Docker images are automatically built and published
## 📮 Contact
- Create an issue for bugs or features
- Join discussions for questions
- Email: picpeak@example.com for security issues
Thank you for contributing! 🎉
+169 -432
View File
@@ -1,483 +1,220 @@
# Photo Sharing Platform - Production Deployment Guide
# 🚀 PicPeak Deployment Guide
This guide covers deploying the photo sharing platform using Docker Swarm, Traefik, and Drone CI/CD.
This guide will help you deploy PicPeak in production. The entire process takes about 10-15 minutes.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Infrastructure Setup](#infrastructure-setup)
- [Docker Swarm Setup](#docker-swarm-setup)
- [Traefik Setup](#traefik-setup)
- [Application Deployment](#application-deployment)
- [CI/CD with Drone](#cicd-with-drone)
- [Monitoring](#monitoring)
- [Backup and Recovery](#backup-and-recovery)
- [Troubleshooting](#troubleshooting)
## 📋 Prerequisites
## Prerequisites
- A server with Docker and Docker Compose installed
- A domain name (for SSL certificates)
- SMTP credentials for sending emails
- Basic command line knowledge
### Hardware Requirements
- **Manager Node**: 2 CPU cores, 4GB RAM, 50GB storage
- **Worker Nodes**: 2 CPU cores, 2GB RAM, 20GB storage
- **Storage**: SSD recommended for database and photo storage
## 🏃 Quick Deploy (Recommended)
### Software Requirements
- Ubuntu 20.04+ or similar Linux distribution
- Docker Engine 20.10+
- Docker Compose 2.0+
- Git
- SSL certificates (automated with Let's Encrypt)
### Network Requirements
- Ports 80, 443 open for web traffic
- Port 2377 for Swarm management
- Ports 7946, 4789 for Swarm networking
- Static IP or reliable dynamic DNS
## Infrastructure Setup
### 1. Install Docker
### 1. Clone and Configure
```bash
# Install Docker
curl -fsSL https://get.docker.com | sh
# Clone the repository
git clone https://github.com/the-luap/picpeak.git
cd picpeak
# Add user to docker group
sudo usermod -aG docker $USER
# Enable Docker service
sudo systemctl enable docker
sudo systemctl start docker
```
### 2. Configure Firewall
```bash
# Allow Docker Swarm ports
sudo ufw allow 2377/tcp
sudo ufw allow 7946/tcp
sudo ufw allow 7946/udp
sudo ufw allow 4789/udp
# Allow web traffic
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
```
## Docker Swarm Setup
### 1. Initialize Swarm
On the manager node:
```bash
cd deploy/scripts
sudo ./init-swarm.sh
```
This script will:
- Initialize Docker Swarm
- Create overlay networks
- Label nodes for service placement
- Create required directories
### 2. Join Worker Nodes
On each worker node, run the join command displayed by the init script:
```bash
docker swarm join --token SWMTKN-1-xxx... manager-ip:2377
```
### 3. Verify Swarm
```bash
docker node ls
```
## Application Configuration
### 1. Environment Setup
```bash
# Copy environment template
cp .env.production.example .env.production
cp .env.production.example .env
# Edit with your values
nano .env.production
# Generate a secure JWT secret
echo "JWT_SECRET=$(openssl rand -base64 32)" >> .env
# Edit configuration
nano .env
```
Required configurations:
- Domain names for frontend, backend, and services
- SMTP credentials for email
- Database passwords
- JWT secrets
### 2. Required Environment Variables
### 2. Create Docker Secrets
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
cd deploy/scripts
./create-secrets.sh
# 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
```
This will create all required secrets in Docker Swarm. Save the generated passwords!
## 🔧 Configuration Options
## Traefik Setup
### Storage Settings
### 1. Deploy Traefik
```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
cd deploy/traefik
# Backup database and photos
./scripts/backup.sh
# Create traefik network
docker network create --driver overlay traefik-public
# Deploy Traefik stack
docker stack deploy -c docker-compose.traefik.yml traefik
# Backups are stored in ./backups/
```
### 2. Verify Traefik
### Update
```bash
# Check service status
docker service ls | grep traefik
# Pull latest changes
git pull
# View logs
docker service logs traefik_traefik
# Rebuild and restart
docker-compose -f docker-compose.prod.yml up -d --build
```
Access Traefik dashboard at: `https://traefik.yourdomain.com/dashboard/`
## Application Deployment
### 1. Build Images (if using local registry)
### Logs
```bash
# Build frontend
cd frontend
docker build -t photo-sharing-frontend:latest .
# View all logs
docker-compose logs
# Build backend
cd ../backend
docker build -t photo-sharing-backend:latest .
# View specific service
docker-compose logs backend
docker-compose logs frontend
```
### 2. Deploy Application Stack
```bash
cd deploy/scripts
./deploy.sh
```
Options:
- `--env FILE`: Specify environment file
- `--registry URL`: Docker registry URL
- `--version VERSION`: Image version to deploy
### 3. Verify Deployment
```bash
# Check all services
docker service ls
# Check specific service
docker service ps photo-sharing_backend
# View logs
docker service logs photo-sharing_backend -f
```
### 4. Run Database Migrations
The deploy script automatically runs migrations, but you can run manually:
```bash
docker exec $(docker ps -q -f name=photo-sharing_backend) npm run migrate
```
## CI/CD with Drone
### 1. Drone Server Setup
Deploy Drone server on your CI infrastructure:
```bash
docker run \
--volume=/var/lib/drone:/data \
--env=DRONE_GITHUB_CLIENT_ID=your-id \
--env=DRONE_GITHUB_CLIENT_SECRET=your-secret \
--env=DRONE_RPC_SECRET=your-rpc-secret \
--env=DRONE_SERVER_HOST=drone.yourdomain.com \
--env=DRONE_SERVER_PROTO=https \
--publish=80:80 \
--publish=443:443 \
--restart=always \
--detach=true \
--name=drone \
drone/drone:2
```
### 2. Drone Runner Setup
On build servers:
```bash
docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock \
-e DRONE_RPC_PROTO=https \
-e DRONE_RPC_HOST=drone.yourdomain.com \
-e DRONE_RPC_SECRET=your-rpc-secret \
-e DRONE_RUNNER_CAPACITY=2 \
-e DRONE_RUNNER_NAME=runner-1 \
-p 3000:3000 \
--restart always \
--name runner \
drone/drone-runner-docker:1
```
### 3. Repository Setup
1. Enable repository in Drone UI
2. Add secrets in Drone:
- `docker_username`
- `docker_password`
- `docker_registry`
- `staging_swarm_host`
- `staging_swarm_user`
- `staging_swarm_key`
- `prod_swarm_host`
- `prod_swarm_user`
- `prod_swarm_key`
- `slack_webhook`
### 4. Deployment Workflow
- Push to `develop` → Deploy to staging
- Create tag → Deploy to production
- Automatic rollback on failure
## Monitoring
### 1. Deploy Monitoring Stack
```bash
cd deploy/monitoring
# Deploy monitoring services
docker stack deploy -c docker-compose.monitoring.yml monitoring
```
### 2. Access Services
- Grafana: `https://grafana.yourdomain.com`
- Prometheus: `https://prometheus.yourdomain.com`
- Alertmanager: `https://alerts.yourdomain.com`
### 3. Configure Alerts
Create alert rules in `deploy/monitoring/alerts/`:
```yaml
groups:
- name: photo-sharing
rules:
- alert: ServiceDown
expr: up{job="photo-sharing-backend"} == 0
for: 5m
annotations:
summary: "Photo sharing backend is down"
```
## Backup and Recovery
### 1. Automated Backups
Set up cron job for automated backups:
```bash
# Edit crontab
crontab -e
# Add daily backup at 2 AM
0 2 * * * /opt/photo-sharing/deploy/scripts/backup.sh
```
### 2. Manual Backup
```bash
cd deploy/scripts
./backup.sh
```
### 3. Restore from Backup
```bash
# Extract backup
tar -xzf backup-20240615-020000.tar.gz
# Restore database
docker exec -i $(docker ps -q -f name=photo-sharing_db) \
psql -U postgres photo_sharing < backup-20240615-020000/database.sql
# Restore photos
tar -xzf backup-20240615-020000/photos.tar.gz -C /opt/photo-sharing/
# Restore volumes
docker run --rm \
-v photo-sharing_app-data:/data \
-v $(pwd)/backup-20240615-020000:/backup \
alpine tar -xzf /backup/volume-photo-sharing_app-data.tar.gz -C /data
```
## Maintenance
### 1. Scaling Services
```bash
# Scale backend to 5 replicas
docker service scale photo-sharing_backend=5
# Scale frontend to 3 replicas
docker service scale photo-sharing_frontend=3
```
### 2. Rolling Updates
```bash
# Update backend image
docker service update \
--image registry.yourdomain.com/photo-sharing-backend:v2.0 \
photo-sharing_backend
```
### 3. Drain Node for Maintenance
```bash
# Drain node
docker node update --availability drain worker-1
# Perform maintenance...
# Activate node
docker node update --availability active worker-1
```
## Troubleshooting
## 🚨 Troubleshooting
### Common Issues
#### 1. Service Won't Start
**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 service ps photo-sharing_backend --no-trunc
docker-compose ps
# View detailed logs
docker service logs photo-sharing_backend --details
# Test backend API
curl https://your-domain.com/api/health
# Check disk space
df -h storage/
```
#### 2. Database Connection Issues
```bash
# Check database logs
docker service logs photo-sharing_db
## 🐳 Alternative Deployment Methods
# Test connection
docker exec $(docker ps -q -f name=photo-sharing_db) \
pg_isready -U postgres
```
### Using Docker Swarm
#### 3. Traefik Certificate Issues
```bash
# Check Traefik logs
docker service logs traefik_traefik | grep acme
For high availability deployments, see [Docker Swarm Setup](deploy/README.md).
# Remove and regenerate certificates
rm -rf /opt/traefik/letsencrypt/acme.json
docker service update --force traefik_traefik
```
### Manual Installation
#### 4. Storage Issues
```bash
# Check disk usage
df -h
If you prefer not to use Docker:
# Clean up Docker
docker system prune -a
```
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
### Debug Mode
## 📞 Support
Enable debug logging:
- 📘 [Documentation](https://github.com/the-luap/picpeak)
- 🐛 [Report Issues](https://github.com/the-luap/picpeak/issues)
- 💬 [Discussions](https://github.com/the-luap/picpeak/discussions)
```bash
# Update service with debug logging
docker service update \
--env-add LOG_LEVEL=debug \
photo-sharing_backend
```
---
### Health Checks
```bash
# Check all endpoints
curl -f https://photos.yourdomain.com/health
curl -f https://api.photos.yourdomain.com/api/health
curl -f https://traefik.yourdomain.com/ping
```
## Security Best Practices
1. **Regular Updates**
- Keep Docker and system packages updated
- Update application dependencies regularly
- Monitor security advisories
2. **Access Control**
- Use strong passwords for all services
- Enable 2FA where possible
- Restrict SSH access to specific IPs
- Use Docker secrets for sensitive data
3. **Network Security**
- Use internal networks for service communication
- Enable firewall rules
- Use TLS for all external communication
- Regular security scans with Trivy
4. **Backup Security**
- Encrypt backups at rest
- Test restore procedures regularly
- Store backups in multiple locations
- Rotate old backups
## Performance Tuning
1. **Database Optimization**
```sql
-- Add indexes for common queries
CREATE INDEX idx_photos_event_id ON photos(event_id);
CREATE INDEX idx_access_logs_event_id ON access_logs(event_id);
```
2. **Image Optimization**
- Use CDN for static assets
- Enable aggressive caching
- Optimize image sizes before upload
3. **Service Limits**
```yaml
deploy:
resources:
limits:
cpus: '2'
memory: 1G
reservations:
cpus: '0.5'
memory: 256M
```
## Support
For issues and questions:
- Check logs: `docker service logs <service_name>`
- Review documentation: [README.md](README.md)
- Check monitoring dashboards
- Contact: admin@yourdomain.com
**Need help?** Open an issue on GitHub and we'll assist you!
+378
View File
@@ -0,0 +1,378 @@
# 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
-130
View File
@@ -1,130 +0,0 @@
# 🚀 Quick Local Development Setup
Get the photo sharing platform running locally in under 2 minutes!
## Prerequisites
- Docker Desktop installed and running
- Git
- 4GB RAM available
## Quick Start
```bash
# 1. Clone the repository
git clone <your-repo-url>
cd picpeak
# 2. Start everything
./start-local.sh
```
That's it! 🎉
## What You Get
| Service | URL | Description |
|---------|-----|-------------|
| Frontend (Dev) | http://localhost:3002 | React app with hot reload |
| Frontend (Prod) | http://localhost:3000 | Production build |
| Backend API | http://localhost:3001 | Express API |
| Mailhog | http://localhost:8025 | Email testing UI |
## Default Credentials
- **Admin Login**: Check `ADMIN_CREDENTIALS.txt` after first setup
- **Test Gallery**:
- Create via Admin Panel
- Set your own secure password
## Common Tasks
### View Logs
```bash
docker-compose -f docker-compose.local.yml logs -f
```
### Stop Everything
```bash
./stop-local.sh
```
### Reset Database
```bash
docker-compose -f docker-compose.local.yml exec backend npm run migrate
```
### Add Test Photos
1. Create a gallery in the admin panel
2. Get the gallery slug (e.g., `wedding-smith-2024`)
3. Add photos to: `./storage/events/active/wedding-smith-2024/`
4. Photos appear automatically!
### Access Backend Shell
```bash
docker-compose -f docker-compose.local.yml exec backend sh
```
## Development Workflow
1. **Frontend Development** (Port 3002)
- Hot reload enabled
- Edit files in `./frontend/src`
- Changes appear instantly
2. **Backend Development** (Port 3001)
- Nodemon watches for changes
- Edit files in `./backend/src`
- Server restarts automatically
3. **Email Testing**
- All emails go to Mailhog
- View at http://localhost:8025
- No real emails sent!
## Troubleshooting
### Backend won't start
```bash
# Check logs
docker-compose -f docker-compose.local.yml logs backend
# Rebuild
docker-compose -f docker-compose.local.yml build backend
```
### Frontend build issues
```bash
# Clear cache and rebuild
docker-compose -f docker-compose.local.yml exec frontend-dev npm run build
```
### Port conflicts
Edit `docker-compose.local.yml` and change the port mappings:
- Backend: Change `3001:3000` to `XXXX:3000`
- Frontend: Change `3002:5173` to `YYYY:5173`
### Reset everything
```bash
# Stop and remove all data
docker-compose -f docker-compose.local.yml down -v
rm -rf data storage logs
./start-local.sh
```
## Tips
- 📧 Check Mailhog for all emails
- 🔄 Frontend auto-refreshes on save
- 📁 SQLite DB at `./data/photo_sharing.db`
- 🖼️ Photos in `./storage/events/active/`
- 📝 Logs in `./logs/`
## Next Steps
1. Create your first gallery via Admin Panel
2. Upload some test photos
3. Test the gallery with password
4. Check expiration warnings
5. View emails in Mailhog
Happy coding! 🎨
+189 -22
View File
@@ -1,32 +1,199 @@
# Photo Sharing Platform
# 📸 PicPeak - Open Source Photo Sharing for Events
A secure, self-hosted photo sharing platform designed for weddings and events. Features automatic expiration, email notifications, and simple file-based management.
<div align="center">
<img src="docs/picpeak-logo.png" alt="PicPeak Logo" width="300" />
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=flat&logo=docker&logoColor=white)](https://www.docker.com/)
[![Node.js](https://img.shields.io/badge/node.js-6DA55F?style=flat&logo=node.js&logoColor=white)](https://nodejs.org/)
[![React](https://img.shields.io/badge/react-%2320232a.svg?style=flat&logo=react&logoColor=%2361DAFB)](https://reactjs.org/)
</div>
## Features
**PicPeak** is a powerful, self-hosted open-source alternative to commercial photo-sharing platforms like PicDrop.com and Scrapbook.de. Designed specifically for photographers and event organizers, PicPeak makes it simple to share beautiful, time-limited photo galleries with clients while maintaining full control over your data and branding.
- 🔒 Password Protected Galleries
- ⏰ Automatic Expiration
- 📧 Email Notifications
- 📁 Simple File Management
- 📊 Analytics Integration
- 🎨 Customizable Themes
- 📱 Mobile Responsive
- ⚡ Docker Ready
![PicPeak Gallery Preview](docs/screenshot-gallery.png)
## Quick Start
## 🌟 Why Choose PicPeak?
1. Clone the repository
2. Run `./scripts/install.sh`
3. Configure `.env` file
4. Setup SSL: `./scripts/setup-ssl.sh`
5. Start: `docker-compose -f docker-compose.prod.yml up -d`
Unlike expensive SaaS solutions, PicPeak gives you:
Default credentials: Check ADMIN_CREDENTIALS.txt after first setup
- **💰 No Monthly Fees** - One-time setup, unlimited galleries
- **🔒 Complete Data Control** - Your photos stay on your server
- **🎨 White-Label Ready** - Full branding customization
- **📱 Mobile-First Design** - Beautiful on all devices
- **🚀 Lightning Fast** - Optimized performance and caching
- **🌍 Multi-Language** - Built-in i18n support (EN, DE)
## Documentation
## ✨ Key Features
See DEPLOYMENT.md for detailed deployment instructions.
### For Photographers
- 📁 **Drag & Drop Upload** - Simply drop photos into folders
-**Auto-Expiring Galleries** - Set expiration dates (default: 30 days)
- 🔐 **Password Protection** - Secure client galleries
- 📧 **Automated Emails** - Creation confirmations and expiration warnings
- 📊 **Analytics Dashboard** - Track views, downloads, and engagement
- 🎨 **Custom Themes** - Match your brand perfectly
## License
### For Clients
- 🖼️ **Beautiful Galleries** - Clean, modern interface
- 📱 **Mobile Optimized** - Swipe through photos on any device
- ⬇️ **Bulk Downloads** - Download all photos with one click
- 🔍 **Smart Search** - Find photos quickly
- 📤 **Guest Uploads** - Optional client photo uploads
MIT License
### Technical Excellence
- 🐳 **Docker Ready** - Deploy in minutes
- 🔄 **Auto-Processing** - Automatic thumbnail generation
- 💾 **Smart Storage** - Automatic archiving of expired galleries
- 🛡️ **Security First** - JWT auth, rate limiting, CORS protection
- 📈 **Scalable** - From small studios to large agencies
## 🚀 Quick Start
Get PicPeak running in under 5 minutes:
```bash
# Clone the repository
git clone https://github.com/the-luap/picpeak.git
cd picpeak
# Copy environment template
cp .env.example .env
# Edit configuration (required: JWT_SECRET)
nano .env
# Start with Docker Compose
docker-compose up -d
# Access at http://localhost:3005
```
## 📖 Documentation
- 📘 [**Deployment Guide**](DEPLOYMENT.md) - Detailed installation instructions
- 🤝 [**Contributing**](CONTRIBUTING.md) - How to contribute
- 📜 [**License**](LICENSE) - MIT License
- 🔒 [**Security**](SECURITY.md) - Security policies
- 📋 [**Code of Conduct**](CODE_OF_CONDUCT.md) - Community guidelines
## 🎯 Use Cases
Perfect for:
- 💒 **Wedding Photographers** - Share ceremony photos securely
- 🎂 **Event Photography** - Birthday parties, corporate events
- 📸 **Portrait Studios** - Client galleries with download limits
- 🏢 **Corporate Events** - Internal photo sharing with branding
- 🎓 **School Photography** - Secure parent access with expiration
## 🏗️ Tech Stack
- **Backend**: Node.js, Express, SQLite/PostgreSQL
- **Frontend**: React, Tailwind CSS, Framer Motion
- **Storage**: File-based with automatic archiving
- **Email**: SMTP with customizable templates
- **Analytics**: Privacy-focused with Umami integration
## 🤝 Contributing
We love contributions! PicPeak is built by photographers, for photographers. Whether you're fixing bugs, adding features, or improving documentation, your help is welcome.
See our [Contributing Guide](CONTRIBUTING.md) for details.
## 📊 Comparison with Alternatives
| Feature | PicPeak | PicDrop | Scrapbook.de |
|---------|---------|---------|--------------|
| Self-Hosted | ✅ | ❌ | ❌ |
| Custom Branding | ✅ Full | Limited | Limited |
| Monthly Cost | $0 | $29-199 | €19-99 |
| Storage Limit | Unlimited* | 50-500GB | 100-1000GB |
| Client Uploads | ✅ | ✅ | ✅ |
| API Access | ✅ | Paid | ❌ |
| Open Source | ✅ | ❌ | ❌ |
*Limited only by your server storage
## 🛡️ Security
PicPeak takes security seriously:
- 🔐 Password hashing with bcrypt
- 🎫 JWT-based authentication
- 🚦 Rate limiting on all endpoints
- 🛡️ CORS protection
- 📝 Activity logging
- 🔒 Secure file access
Found a security issue? Please email security@example.com
## 📸 Screenshots
### 🎛️ **Admin Dashboard**
Get a complete overview of your photo galleries, analytics, and system status.
<img src="docs/screenshot-dashboard.png" alt="PicPeak Admin Dashboard" width="800" />
### 📊 **Analytics & Insights**
Track gallery performance, view statistics, and monitor user engagement.
<img src="docs/screenshot-analytics.png" alt="PicPeak Analytics Dashboard" width="800" />
### 📁 **Event Management**
Organize and manage your photo galleries with intuitive event management tools.
<img src="docs/screenshots-events.png" alt="PicPeak Events Management" width="800" />
### ✨ **Key Interface Highlights**
<details>
<summary>👆 Click to see more interface details</summary>
#### What makes PicPeak's interface special:
- **🎨 Clean Design**: Modern, photographer-friendly interface
- **📱 Responsive**: Perfect on desktop, tablet, and mobile
- **⚡ Fast Loading**: Optimized for quick photo browsing
- **🔒 Secure Access**: Password-protected galleries with expiration
- **📤 Easy Uploads**: Drag & drop functionality for effortless photo management
- **🎯 Client-Focused**: Intuitive gallery experience for your clients
</details>
## 🗺️ Roadmap
We're constantly improving PicPeak and welcome contributions from our community! If you have ideas for new features or want to help implement existing ones, please open an issue or submit a pull request. Your contributions help make PicPeak better for everyone.
| Feature | Description | Priority | Status |
|---------|-------------|----------|---------|
| **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 |
| **Video Support** | Upload and display videos alongside photos in galleries with streaming support | Low | 🔄 Open |
**Status Legend:** ✅ Implemented | 🚧 In Progress | 🔄 Open | 📋 Planned
## 🙏 Acknowledgments
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.
## 📄 License
PicPeak is released under the [MIT License](LICENSE). Use it freely for personal or commercial projects.
## 🚀 Ready to Get Started?
1.**Star this repository** to show your support
2. 📖 Read the [Deployment Guide](DEPLOYMENT.md)
3. 🐛 Report issues or request features
4. 🤝 Join our community and contribute!
---
<p align="center">
Made with ❤️ by photographers, for photographers
<br>
<a href="https://github.com/the-luap/picpeak">GitHub</a> •
<a href="DEPLOYMENT.md">Documentation</a> •
<a href="https://github.com/the-luap/picpeak/issues">Support</a>
</p>
+85
View File
@@ -0,0 +1,85 @@
# Security Policy
## Supported Versions
We release patches for security vulnerabilities. Currently supported versions:
| Version | Supported |
| ------- | ------------------ |
| 1.x.x | :white_check_mark: |
| < 1.0 | :x: |
## Reporting a Vulnerability
We take the security of PicPeak seriously. If you have discovered a security vulnerability, please follow these steps:
### 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)
### 3. You can expect:
- Acknowledgment within 48 hours
- Regular updates on our progress
- Credit in the fix announcement (unless you prefer to remain anonymous)
## Security Measures
PicPeak implements several security measures:
### Authentication & Authorization
- JWT-based authentication with secure token storage
- bcrypt password hashing with configurable rounds
- Role-based access control for admin functions
- Session timeout management
### Input Validation
- All user inputs are validated and sanitized
- SQL injection prevention through parameterized queries
- XSS protection via Content Security Policy
- File upload restrictions and validation
### Rate Limiting
- API rate limiting to prevent abuse
- Brute force protection on authentication endpoints
- Configurable limits per endpoint
### Data Protection
- HTTPS enforcement in production
- Secure cookie settings
- CORS configuration
- Sensitive data encryption
### Infrastructure
- Regular dependency updates
- Security headers (HSTS, X-Frame-Options, etc.)
- Activity logging for audit trails
- Automated backups
## Best Practices for Deployment
1. **Always use HTTPS** in production
2. **Change default passwords** immediately
3. **Keep dependencies updated** regularly
4. **Configure firewall rules** appropriately
5. **Monitor logs** for suspicious activity
6. **Backup regularly** and test restoration
## Vulnerability Disclosure
We believe in responsible disclosure. Once a vulnerability is fixed:
1. We'll publish a security advisory
2. Credit researchers (with permission)
3. Detail the impact and mitigation steps
4. Release patches for all supported versions
## Contact
- Security issues: security@example.com
- General support: https://github.com/the-luap/picpeak/issues
Thank you for helping keep PicPeak and its users safe!
-252
View File
@@ -1,252 +0,0 @@
# PicPeak - Complete Setup Guide
## Repository Created Successfully! 🎉
Your PicPeak repository has been created at:
**https://gitea.nothaft.cloud/paul/picpeak**
## What's Been Created
I've uploaded the core files needed to run the application:
### ✅ Created Files:
- `.gitignore` - Git ignore rules
- `.dockerignore` - Docker ignore rules
- `.env.example` - Environment configuration template
- `docker-compose.yml` - Development Docker setup
- `docker-compose.prod.yml` - Production Docker setup
- `backend/` - Core backend files including:
- `package.json` - Dependencies
- `server.js` - Main server file
- `Dockerfile` - Backend container config
- Core routes and services
- `setup-remaining-files.sh` - Script to create remaining files
## Next Steps to Complete Setup
### 1. Clone the Repository
```bash
git clone https://gitea.local.nothaft.cloud/paul/picpeak.git
cd picpeak
```
### 2. Run the Setup Script
```bash
chmod +x setup-remaining-files.sh
./setup-remaining-files.sh
```
This will create all remaining directories and files needed.
### 3. Create Critical Service Files
Due to the large number of files, I've created the most important ones. You'll need to add these remaining backend services:
#### backend/src/services/expirationChecker.js
```javascript
const cron = require('node-cron');
const { db } = require('../database/db');
const { archiveEvent } = require('./archiveService');
const logger = require('../utils/logger');
function startExpirationChecker() {
// Check every hour for expired events
cron.schedule('0 * * * *', async () => {
await checkExpirations();
});
logger.info('Expiration checker started');
}
async function checkExpirations() {
try {
const now = new Date();
const warningDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
// Check for events needing warning emails
const eventsNeedingWarning = await db('events')
.where('is_active', true)
.where('is_archived', false)
.where('expires_at', '<=', warningDate)
.where('expires_at', '>', now);
for (const event of eventsNeedingWarning) {
const existingWarning = await db('email_queue')
.where('event_id', event.id)
.where('email_type', 'warning')
.first();
if (!existingWarning) {
await queueExpirationWarning(event);
}
}
// Check for expired events
const expiredEvents = await db('events')
.where('is_active', true)
.where('is_archived', false)
.where('expires_at', '<=', now);
for (const event of expiredEvents) {
await handleExpiredEvent(event);
}
} catch (error) {
logger.error('Error checking expirations:', error);
}
}
async function queueExpirationWarning(event) {
const daysRemaining = Math.ceil((new Date(event.expires_at) - new Date()) / (1000 * 60 * 60 * 24));
await db('email_queue').insert({
event_id: event.id,
recipient_email: event.host_email,
email_type: 'warning',
email_data: JSON.stringify({
event_name: event.event_name,
days_remaining: daysRemaining,
share_link: event.share_link
})
});
logger.info(`Queued expiration warning for event ${event.slug}`);
}
async function handleExpiredEvent(event) {
try {
await db('events').where('id', event.id).update({ is_active: false });
await db('email_queue').insert([
{
event_id: event.id,
recipient_email: event.host_email,
email_type: 'expiration',
email_data: JSON.stringify({
event_name: event.event_name
})
},
{
event_id: event.id,
recipient_email: event.admin_email,
email_type: 'expiration',
email_data: JSON.stringify({
event_name: event.event_name,
event_slug: event.slug
})
}
]);
await archiveEvent(event);
logger.info(`Handled expiration for event ${event.slug}`);
} catch (error) {
logger.error(`Error handling expired event ${event.slug}:`, error);
}
}
module.exports = { startExpirationChecker };
```
### 4. Create Frontend Files
The frontend needs these key files in `frontend/src/`:
#### App.js
```javascript
import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import ProtectedRoute from './components/ProtectedRoute';
// Pages
import Login from './pages/Login';
import Gallery from './pages/Gallery';
import AdminLogin from './pages/admin/Login';
import AdminDashboard from './pages/admin/Dashboard';
function App() {
return (
<AuthProvider>
<Routes>
<Route path="/" element={<Navigate to="/gallery" />} />
<Route path="/gallery/:slug/:token?" element={<Gallery />} />
<Route path="/login/:slug" element={<Login />} />
<Route path="/admin/login" element={<AdminLogin />} />
<Route path="/admin" element={
<ProtectedRoute>
<AdminDashboard />
</ProtectedRoute>
} />
</Routes>
</AuthProvider>
);
}
export default App;
```
### 5. Install Dependencies
```bash
# Backend
cd backend
npm install
# Frontend
cd ../frontend
npm install
```
### 6. Configure Environment
Copy `.env.example` to `.env` and update with your settings:
```bash
cp .env.example .env
nano .env
```
### 7. Start Development Environment
```bash
# From root directory
docker-compose up
```
- Backend: http://localhost:3000
- Frontend: http://localhost:3001
- MailHog: http://localhost:8025
## Key Features Implemented
- ✅ Password-protected galleries
- ✅ Automatic expiration with email warnings
- ✅ File-based photo management
- ✅ ZIP archiving on expiration
- ✅ Separate admin and public interfaces
- ✅ Email notifications at all stages
- ✅ Mobile-responsive design
- ✅ Docker deployment ready
## Production Deployment
1. Update `.env` with production values
2. Run `./scripts/install.sh` on your server
3. Configure SSL with `./scripts/setup-ssl.sh`
4. Start with `docker-compose -f docker-compose.prod.yml up -d`
## Need Help?
The complete implementation includes:
- Backend API with all routes
- React frontend with admin panel
- Email service with templates
- Automatic file watching
- Expiration checking
- Archive service
- Docker configuration
- Deployment scripts
All core functionality from your PRD has been implemented. You may need to create some additional UI components based on your specific design preferences.
Default admin credentials: **admin / admin123** (change immediately!)
-47
View File
@@ -1,47 +0,0 @@
# TODO - Open Items Before Release
## Priority Items
- [ ] **Gallery Mobile View**
- Logout button should only show logo icon (no text)
- If photo upload is enabled, move upload button inside menu (not on top bar)
- Top bar should show: logo (left), gallery title (center), event date + expiration date
- [ ] **Gallery Preview**
- Preview should correctly reflect the selected grid layout style
- Add grid style selector above current top bar
- Selector should match the style of event template settings grid selector
- [ ] **Hero Grid Layout**
- Top bar: only menu and logout buttons
- Title + logo displayed centered on hero photo
- Event date and expiration date also on hero photo
- No logo/title in top bar
- [ ] **Logo Testing** - Test new PicPeak logos across all grid styles
- [ ] **Welcome Message**
- Add welcome message to email template when creating new event
- Use as personal message in the email
- [ ] **Gallery Upload Function**
- Fix scrolling in upload popup when multiple images selected
- Save/Cancel buttons unreachable due to incorrect scroll formatting
- [ ] **Watermarks** - Test watermark functionality, styling, and image application
- [ ] **Dashboard Activities** - Remove "show all" link from latest activities widget
- [ ] **Security Audit** - Perform security review and code audit
- [ ] **Drone CI/CD** - Update drone.yaml configuration
- [ ] **Version Management** - Implement automatic version updates on commits/builds
## Completed Items
_(Move completed items here with date)_
---
Last updated: 2025-07-10
+50 -25
View File
@@ -1,33 +1,58 @@
NODE_ENV=development
PORT=3000
# Backend Environment Variables Example
# Copy this file to .env and update with your values
# URLs
ADMIN_URL=http://localhost:3000
FRONTEND_URL=http://localhost:3001
# Application
NODE_ENV=production
PORT=3001
# Security
JWT_SECRET=dev-secret-key
# Generate with: openssl rand -base64 32
JWT_SECRET=your-very-secure-jwt-secret-at-least-32-characters-long-example123456
# Email Configuration
SMTP_HOST=mailhog
SMTP_PORT=1025
# URLs (adjust for your domain)
ADMIN_URL=https://photos.example.com
FRONTEND_URL=https://photos.example.com
# Database Configuration
DATABASE_CLIENT=pg
DB_HOST=localhost
DB_PORT=5432
DB_USER=picpeak
DB_PASSWORD=your-secure-database-password-change-this
DB_NAME=picpeak
# Email Configuration (Examples for common providers)
# Gmail example:
# SMTP_HOST=smtp.gmail.com
# SMTP_PORT=587
# SMTP_SECURE=false
# SMTP_USER=your-email@gmail.com
# SMTP_PASS=your-app-specific-password
# SendGrid example:
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=
SMTP_PASS=
EMAIL_FROM=noreply@localhost
SMTP_USER=apikey
SMTP_PASS=your-sendgrid-api-key
EMAIL_FROM=noreply@example.com
# Storage Paths (relative to project root)
STORAGE_PATH=./storage
EVENTS_PATH=./storage/events
ARCHIVE_PATH=./storage/events/archived
# Storage Paths
# Docker deployment:
STORAGE_PATH=/app/storage
EVENTS_PATH=/app/storage/events
ARCHIVE_PATH=/app/storage/events/archived
# Local development:
# STORAGE_PATH=./storage
# EVENTS_PATH=./storage/events
# ARCHIVE_PATH=./storage/events/archived
# Analytics Backend Configuration (OPTIONAL)
# Used for server-side tracking only
# Primary configuration should be done through Admin UI > Settings > Analytics
# UMAMI_URL=https://analytics.example.com
# UMAMI_WEBSITE_ID=b4d3c2a1-5678-90ab-cdef-1234567890ab
# Logging
LOG_LEVEL=info
# Database (for production, consider PostgreSQL)
DATABASE_CLIENT=sqlite3
DATABASE_PATH=./data/photo_sharing.db
# Umami Analytics (optional)
UMAMI_URL=
UMAMI_WEBSITE_ID=
LOG_LEVEL=info
-137
View File
@@ -1,137 +0,0 @@
# Quick Guide: Activate Authentication V2 Fixes
## Step 1: Install Dependency
```bash
cd backend
npm install zxcvbn@4.4.2
```
## Step 2: Add to Docker & Run Migration
```bash
# Rebuild Docker with new dependency
docker-compose down
docker-compose up -d --build
# Run migration for token revocation
docker exec wedding-photo-sharing-backend-1 node /app/scripts/add-token-revocation-tables.js
```
## Step 3: Update server.js
### 3.1 Fix Rate Limiting (Line ~10)
```javascript
// Add after other requires
const { createSecureSkipFunction, logRateLimitHit } = require('./src/utils/rateLimitSecurity');
```
### 3.2 Update Rate Limiter (Line ~59)
```javascript
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: process.env.NODE_ENV === 'development' ? 1000 : 100,
skip: createSecureSkipFunction(), // CHANGE THIS LINE
handler: (req, res) => {
logRateLimitHit(req, res); // ADD THIS
res.status(429).json({
error: 'Too many requests from this IP, please try again later.'
});
}
});
```
### 3.3 Update Auth Limiter (Line ~81)
```javascript
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true, // ADD THIS
handler: (req, res) => {
logRateLimitHit(req, res); // ADD THIS
res.status(429).json({
error: 'Too many login attempts, please try again later.',
retryAfter: res.getHeader('Retry-After')
});
}
});
```
### 3.4 Change Auth Routes (Line ~22)
```javascript
// Change from:
const authRoutes = require('./src/routes/auth-enhanced');
// To:
const authRoutes = require('./src/routes/auth-enhanced-v2');
```
### 3.5 Add Token Revocation (After line ~147)
```javascript
// After initializeCleanupJob();
const { initializeRevocationCleanup } = require('./src/utils/tokenRevocation');
initializeRevocationCleanup();
```
## Step 4: Update Middleware Imports
In files that import adminAuth:
```javascript
// Change from:
const { adminAuth } = require('../middleware/auth-enhanced');
// To:
const { adminAuth } = require('../middleware/auth-enhanced-v2');
```
## Step 5: Update adminEvents.js
Add password validation to event creation:
```javascript
// At top of file
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
// In POST route, after extracting password, add:
const passwordValidation = validatePasswordInContext(password, 'gallery', {
eventName: event_name
});
if (!passwordValidation.valid) {
return res.status(400).json({
error: 'Password does not meet security requirements',
details: passwordValidation.errors,
score: passwordValidation.score,
feedback: passwordValidation.feedback
});
}
// Change password hashing to:
const password_hash = await bcrypt.hash(password, getBcryptRounds());
```
## Step 6: Add Environment Variable
```bash
# In .env file
BCRYPT_ROUNDS=12
```
## Step 7: Restart & Test
```bash
docker-compose restart backend
# Test rate limiting
curl -H "Authorization: Bearer invalid" http://localhost:3001/api/admin/events
# Test password validation
node scripts/test-auth-v2-fixes.js
```
## Verification Checklist
- [ ] zxcvbn installed
- [ ] Token revocation tables created
- [ ] Rate limiting can't be bypassed
- [ ] Weak passwords rejected
- [ ] Password change works
- [ ] No errors in logs
## Rollback
If issues occur:
1. Revert server.js changes
2. Restart backend
3. All new features are additive, so existing functionality remains
-64
View File
@@ -1,64 +0,0 @@
# Authentication & Authorization Flaws Analysis
## Already Fixed ✅
1. **Missing Token Type Validation**
- Fixed in `auth-enhanced.js` line 31
- Checks `decoded.type !== 'admin'`
- Prevents gallery tokens from accessing admin endpoints
2. **No Audit Logging**
- Added `login_attempts` table
- Tracks all login attempts with IP, user agent, timestamp
- Automatic cleanup of old records
3. **Account Lockout Protection**
- Lockout after 5 failed attempts
- 30-minute lockout duration
- Prevents brute force attacks
4. **Basic Session Management**
- Added session timeout middleware
- Tracks active sessions
- Can invalidate sessions
## Still Needs Fixing ❌
### 1. Weak Password Requirements 🔴
- **Current**: No minimum length validation
- **Required**: Minimum 12 characters + complexity
- **Risk**: Vulnerable to brute force
### 2. Rate Limiting Bypass 🔴
- **Current**: Invalid JWT bypasses rate limiting
- **Location**: `server.js:64-71`
- **Risk**: Attackers can spam with invalid tokens
### 3. No Password Complexity 🟡
- **Current**: Any 6+ character password accepted
- **Required**: Upper, lower, number, special char
- **Risk**: Weak passwords
### 4. No Token Revocation 🟡
- **Current**: Tokens valid until expiration
- **Required**: Blacklist/revocation mechanism
- **Risk**: Can't invalidate compromised tokens
### 5. Fixed Bcrypt Rounds 🟡
- **Current**: Hardcoded to 10 rounds
- **Required**: Configurable (12-14 recommended)
- **Risk**: May become insufficient over time
### 6. In-Memory Session Storage 🟡
- **Current**: Sessions stored in memory
- **Required**: Redis or database storage
- **Risk**: Lost on restart, not scalable
## Priority Fixes
1. **Rate Limiting Bypass** (Critical)
2. **Password Requirements** (High)
3. **Password Complexity** (High)
4. **Token Revocation** (Medium)
5. **Bcrypt Rounds** (Medium)
6. **Session Storage** (Low - for scalability)
-216
View File
@@ -1,216 +0,0 @@
# Authentication Security Integration Guide
## How The Enhanced Security Works
### 1. Login Flow with Protection
```
User Login Attempt
Rate Limiter (5 attempts/15 min)
Account Lockout Check
reCAPTCHA Verification
Credentials Validation
Track Login Attempt
Generate Enhanced JWT
```
### 2. Token Structure
**Before** (Basic JWT):
```json
{
"id": 1,
"type": "admin",
"exp": 1234567890
}
```
**After** (Enhanced JWT):
```json
{
"id": 1,
"username": "admin",
"type": "admin",
"ip": "192.168.1.100",
"loginTime": 1234567890,
"exp": 1234567890,
"iss": "picpeak-auth"
}
```
### 3. Security Layers
1. **Network Level**:
- Rate limiting (express-rate-limit)
- CORS restrictions
- Helmet security headers
2. **Application Level**:
- Account lockout (5 attempts)
- reCAPTCHA validation
- Login attempt tracking
3. **Session Level**:
- JWT with expiration
- Session timeout tracking
- IP validation
- Password change detection
4. **Database Level**:
- Bcrypt password hashing
- Audit trail (login_attempts)
- Secure token storage
## Integration Points
### Server.js Changes
```javascript
// Add after database initialization
const { initializeCleanupJob } = require('./src/utils/authSecurity');
initializeCleanupJob();
// Update route import (when ready)
const authRoutes = require('./src/routes/auth-enhanced');
```
### Middleware Updates
For routes requiring enhanced security:
```javascript
// Change from:
router.get('/sensitive', adminAuth, handler);
// To:
const { adminAuth } = require('../middleware/auth-enhanced');
router.get('/sensitive', adminAuth, handler);
```
### Frontend Integration
1. **Handle New Error Codes**:
```javascript
// Lockout error
if (error.response?.status === 423) {
const retryAfter = error.response.data.retryAfter;
showError(`Account locked. Try again in ${retryAfter} seconds`);
}
// Session expired
if (error.response?.data?.code === 'SESSION_TIMEOUT') {
redirectToLogin();
}
```
2. **Implement Logout**:
```javascript
async function logout() {
await api.post('/auth/logout');
clearToken();
redirectToLogin();
}
```
3. **Check Session Status**:
```javascript
async function checkSession() {
const response = await api.get('/auth/session');
if (!response.data.valid) {
redirectToLogin();
}
}
```
## Configuration
### Environment Variables
No new environment variables required. Uses existing:
- `JWT_SECRET` - For token signing
- `NODE_ENV` - For environment detection
### Security Settings
In `authSecurity.js`:
```javascript
const MAX_LOGIN_ATTEMPTS = 5; // Attempts before lockout
const LOCKOUT_DURATION = 30 * 60 * 1000; // 30 minutes
const ATTEMPT_WINDOW = 15 * 60 * 1000; // 15 minute window
```
## Monitoring & Maintenance
### Daily Monitoring
```sql
-- Check for brute force attempts
SELECT identifier, COUNT(*) as attempts,
MAX(attempt_time) as last_attempt
FROM login_attempts
WHERE success = 0
AND attempt_time > datetime('now', '-24 hours')
GROUP BY identifier
HAVING COUNT(*) > 10
ORDER BY attempts DESC;
```
### Weekly Review
```sql
-- Suspicious activity patterns
SELECT DATE(attempt_time) as date,
COUNT(DISTINCT identifier) as unique_users,
COUNT(DISTINCT ip_address) as unique_ips,
COUNT(*) as total_attempts,
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failed_attempts
FROM login_attempts
WHERE attempt_time > datetime('now', '-7 days')
GROUP BY DATE(attempt_time)
ORDER BY date DESC;
```
### Automated Cleanup
The system automatically cleans up login attempts older than 7 days to prevent database bloat.
## Troubleshooting
### User Locked Out
```sql
-- Check lockout status
SELECT * FROM login_attempts
WHERE identifier = 'user@example.com'
AND attempt_time > datetime('now', '-30 minutes')
ORDER BY attempt_time DESC;
-- Clear lockout
DELETE FROM login_attempts
WHERE identifier = 'user@example.com'
AND success = 0;
```
### Token Issues
```javascript
// Debug token in browser console
const token = localStorage.getItem('token');
const decoded = JSON.parse(atob(token.split('.')[1]));
console.log('Token expires:', new Date(decoded.exp * 1000));
console.log('Token IP:', decoded.ip);
```
## Security Best Practices
1. **Monitor Failed Attempts**: Set up alerts for excessive failures
2. **Review IP Patterns**: Look for geographic anomalies
3. **Rotate JWT Secret**: Periodically update in production
4. **Update Dependencies**: Keep auth libraries current
5. **Test Lockouts**: Regularly verify protection works
## Future Enhancements
1. **Two-Factor Authentication**: Database columns already added
2. **IP Whitelist**: For admin accounts
3. **Device Fingerprinting**: Enhanced session security
4. **OAuth Integration**: Social login options
5. **WebAuthn/Passkeys**: Passwordless authentication
-221
View File
@@ -1,221 +0,0 @@
# Authentication Security Enhancement Migration Guide
## Overview
This guide provides a safe migration path to enhance authentication security without disrupting the production system.
## Security Enhancements Implemented
### 1. Account Lockout Protection
- Locks accounts after 5 failed login attempts within 15 minutes
- 30-minute lockout duration
- Prevents brute force attacks
### 2. Login Attempt Tracking
- Records all login attempts (success/failure)
- Tracks IP addresses and user agents
- Enables security monitoring and alerting
### 3. Enhanced Token Security
- Added issuer validation
- IP address tracking in tokens
- Login time tracking
- Password change detection
### 4. Generic Error Messages
- Prevents user enumeration attacks
- Returns "Invalid credentials" for all auth failures
### 5. Logout Endpoint
- Properly invalidates sessions
- Clears server-side session tracking
## Migration Steps
### Step 1: Database Migrations (Low Risk)
First, run the new migrations to add required tables/columns:
```bash
cd backend
# Run new migrations
npx knex migrate:latest
# Verify migrations
npx knex migrate:status
```
This adds:
- `login_attempts` table
- `password_changed_at` column to `admin_users`
- `last_login_ip` column to `admin_users`
### Step 2: Deploy Enhanced Auth Utilities (Low Risk)
The new files don't affect existing functionality:
- `src/utils/authSecurity.js` - New security utilities
- `src/middleware/auth-enhanced.js` - Enhanced auth middleware
- `src/routes/auth-enhanced.js` - Enhanced auth routes
### Step 3: Gradual Rollout Plan
#### Phase 1: Testing (Day 1)
1. Deploy code but keep using existing auth routes
2. Test enhanced routes in parallel:
```bash
# Test existing endpoint
curl -X POST http://localhost:3001/api/auth/admin/login
# Test enhanced endpoint (if added to routes)
curl -X POST http://localhost:3001/api/auth-enhanced/admin/login
```
#### Phase 2: Monitoring (Days 2-3)
1. Add the auth security initialization to server.js:
```javascript
// In server.js, after database initialization
const { initializeCleanupJob } = require('./src/utils/authSecurity');
initializeCleanupJob();
```
2. Monitor logs for any issues
3. Check login_attempts table is populating
#### Phase 3: Switch Routes (Day 4)
1. Update route imports in server.js:
```javascript
// Change from:
const authRoutes = require('./src/routes/auth');
// To:
const authRoutes = require('./src/routes/auth-enhanced');
```
2. Update middleware imports where needed:
```javascript
// Change from:
const { adminAuth } = require('./src/middleware/auth');
// To:
const { adminAuth } = require('./src/middleware/auth-enhanced');
```
### Step 4: Rollback Plan
If issues occur at any phase:
```bash
# Quick rollback - revert route imports
# In server.js, change back to:
const authRoutes = require('./src/routes/auth');
const { adminAuth } = require('./src/middleware/auth');
# Restart application
docker-compose restart backend
# or
pm2 restart picpeak-backend
```
## Testing Checklist
### Before Production Deployment:
1. **Test Normal Login Flow**:
```bash
# Should work normally
curl -X POST http://localhost:3001/api/auth/admin/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"correct-password"}'
```
2. **Test Account Lockout**:
```bash
# Make 5 failed attempts
for i in {1..5}; do
curl -X POST http://localhost:3001/api/auth/admin/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"wrong-password"}'
done
# 6th attempt should return lockout error
```
3. **Test Logout**:
```bash
curl -X POST http://localhost:3001/api/auth/logout \
-H "Authorization: Bearer YOUR_TOKEN"
```
4. **Test Session Info**:
```bash
curl http://localhost:3001/api/auth/session \
-H "Authorization: Bearer YOUR_TOKEN"
```
## Configuration Options
### Adjusting Security Settings
In `src/utils/authSecurity.js`, you can adjust:
```javascript
const MAX_LOGIN_ATTEMPTS = 5; // Number of attempts before lockout
const LOCKOUT_DURATION = 30 * 60 * 1000; // Lockout time in ms
const ATTEMPT_WINDOW = 15 * 60 * 1000; // Time window for counting attempts
```
## Monitoring
### Check Login Attempts:
```sql
-- Recent failed attempts
SELECT * FROM login_attempts
WHERE success = false
ORDER BY attempt_time DESC
LIMIT 20;
-- Accounts with multiple failures
SELECT identifier, COUNT(*) as failed_attempts
FROM login_attempts
WHERE success = false
AND attempt_time > datetime('now', '-1 hour')
GROUP BY identifier
HAVING COUNT(*) > 3;
```
### Monitor Locked Accounts:
```sql
-- Check currently locked accounts
SELECT identifier, COUNT(*) as attempts,
MAX(attempt_time) as last_attempt
FROM login_attempts
WHERE success = false
AND attempt_time > datetime('now', '-15 minutes')
GROUP BY identifier
HAVING COUNT(*) >= 5;
```
## Security Benefits
1. **Prevents Brute Force**: Account lockout after failed attempts
2. **Audit Trail**: Complete login history for security analysis
3. **Session Security**: Tokens invalidated on password change
4. **IP Monitoring**: Detect suspicious login patterns
5. **User Privacy**: Generic errors prevent user enumeration
## Notes
- Old tokens remain valid until expiration
- No immediate user impact
- Gradual rollout minimizes risk
- Full rollback possible at any stage
## Support
Monitor logs after deployment:
```bash
# Docker
docker-compose logs -f backend | grep -E "(auth|login|security)"
# PM2
pm2 logs picpeak-backend | grep -E "(auth|login|security)"
```
-187
View File
@@ -1,187 +0,0 @@
# Authentication Security Enhancement Rollback Plan
## Quick Rollback Steps
### Immediate Rollback (< 2 minutes)
If auth issues occur after deployment, follow these steps:
```bash
# 1. SSH into production server
ssh your-server
# 2. Navigate to backend directory
cd /path/to/picpeak/backend
# 3. Revert route changes in server.js
# Change from:
# const authRoutes = require('./src/routes/auth-enhanced');
# Back to:
# const authRoutes = require('./src/routes/auth');
# 4. Revert middleware if changed
# Change from:
# const { adminAuth } = require('./src/middleware/auth-enhanced');
# Back to:
# const { adminAuth } = require('./src/middleware/auth');
# 5. Restart application
docker-compose restart backend
# OR
pm2 restart picpeak-backend
```
## Rollback Scenarios
### Scenario 1: Users Can't Login
**Symptoms**:
- All login attempts fail
- Generic "Invalid credentials" error
- Admin panel inaccessible
**Quick Fix**:
```bash
# Revert to original auth routes
cd backend
git checkout HEAD -- server.js
docker-compose restart backend
```
### Scenario 2: Account Lockout Issues
**Symptoms**:
- Legitimate users locked out
- "Account temporarily locked" errors
**Quick Fix**:
```sql
-- Clear all lockouts
DELETE FROM login_attempts WHERE success = false;
-- Or clear specific user
DELETE FROM login_attempts
WHERE identifier = 'username_or_email'
AND success = false;
```
### Scenario 3: Token Validation Errors
**Symptoms**:
- "Invalid token" errors
- Existing sessions broken
- API calls failing
**Quick Fix**:
```javascript
// In auth middleware, temporarily disable strict validation
// Comment out issuer validation:
// issuer: 'picpeak-auth'
// Just use basic verification:
const decoded = jwt.verify(token, process.env.JWT_SECRET);
```
### Scenario 4: Database Migration Issues
**Symptoms**:
- Application won't start
- Database errors in logs
**Rollback Migration**:
```bash
# Rollback last 2 migrations
npx knex migrate:rollback --all
npx knex migrate:up 014_add_default_welcome_message.js
# Or manually fix:
sqlite3 database.db
DROP TABLE IF EXISTS login_attempts;
ALTER TABLE admin_users DROP COLUMN password_changed_at;
ALTER TABLE admin_users DROP COLUMN last_login_ip;
```
## Verification After Rollback
1. **Test Admin Login**:
```bash
curl -X POST http://your-domain/api/auth/admin/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"your-password"}'
```
2. **Test Gallery Access**:
```bash
curl -X POST http://your-domain/api/auth/gallery/verify \
-H "Content-Type: application/json" \
-d '{"slug":"test-gallery","password":"gallery-password"}'
```
3. **Check Logs**:
```bash
# No auth errors should appear
docker-compose logs backend | tail -100 | grep -i error
```
## File Restoration
If files were modified, restore from backup:
```bash
# List of files that can be safely reverted
git checkout HEAD -- src/middleware/auth.js
git checkout HEAD -- src/routes/auth.js
git checkout HEAD -- server.js
# Remove new files (safe to delete)
rm -f src/utils/authSecurity.js
rm -f src/middleware/auth-enhanced.js
rm -f src/routes/auth-enhanced.js
rm -f migrations/015_add_login_attempts_table.js
rm -f migrations/016_add_auth_security_columns.js
```
## Emergency SQL Fixes
```sql
-- Clear all security restrictions
DELETE FROM login_attempts;
-- Reset admin password if locked out
UPDATE admin_users
SET password_hash = '$2b$10$YourKnownGoodHashHere'
WHERE username = 'admin';
-- Remove security columns if causing issues
-- (SQLite doesn't support DROP COLUMN easily, so ignore)
```
## Monitoring After Rollback
```bash
# Watch for stability
watch -n 5 'docker-compose logs backend | tail -20'
# Check active connections
netstat -an | grep :3001 | wc -l
# Monitor CPU/Memory
docker stats wedding-photo-sharing-backend-1
```
## Prevention for Next Attempt
Before re-attempting the security enhancement:
1. **Test in staging environment first**
2. **Implement gradual rollout with feature flags**
3. **Add backwards compatibility for tokens**
4. **Create admin bypass for lockouts**
5. **Set up monitoring alerts**
## Contact
If rollback fails:
1. Check `backend/logs/error.log`
2. Restore from last known good backup
3. Use original auth implementation as reference
-119
View File
@@ -1,119 +0,0 @@
# Authentication Security Enhancement Summary
## Security Issues Fixed
### 1. ✅ Account Lockout Protection
- **Issue**: No protection against brute force attacks
- **Fix**: Lock account after 5 failed attempts in 15 minutes
- **Files**: `authSecurity.js`, `login_attempts` table
### 2. ✅ Login Attempt Tracking
- **Issue**: No audit trail for security monitoring
- **Fix**: Track all login attempts with IP, user agent, timestamp
- **Database**: New `login_attempts` table
### 3. ✅ Generic Error Messages
- **Issue**: Different errors could reveal if username exists
- **Fix**: Always return "Invalid credentials"
- **Impact**: Prevents user enumeration attacks
### 4. ✅ Session Management
- **Issue**: No way to invalidate tokens/logout
- **Fix**: Added `/api/auth/logout` endpoint
- **Fix**: Session tracking with timeout
### 5. ✅ Enhanced Token Security
- **Issue**: Basic JWT with minimal claims
- **Fix**: Added issuer, IP, loginTime claims
- **Fix**: Token invalidation on password change
## Implementation Details
### New Files Created
```
backend/
├── src/
│ ├── utils/
│ │ └── authSecurity.js (122 lines)
│ ├── middleware/
│ │ └── auth-enhanced.js (169 lines)
│ └── routes/
│ └── auth-enhanced.js (244 lines)
├── migrations/
│ ├── 015_add_login_attempts_table.js
│ └── 016_add_auth_security_columns.js
└── scripts/
└── test-auth-security.js
```
### Database Changes
1. **login_attempts** table:
- Tracks all authentication attempts
- Enables lockout and monitoring
2. **admin_users** additions:
- `password_changed_at` - Invalidate old tokens
- `last_login_ip` - Security monitoring
- `two_factor_enabled` - Future 2FA support
## Security Improvements
### Before
- ❌ Unlimited login attempts
- ❌ No audit trail
- ❌ User enumeration possible
- ❌ No session invalidation
- ❌ Basic JWT validation
### After
- ✅ Brute force protection
- ✅ Complete audit trail
- ✅ Generic error messages
- ✅ Logout functionality
- ✅ Enhanced token validation
- ✅ IP tracking
- ✅ Password change detection
## Deployment Safety
### Gradual Rollout
1. **Phase 1**: Deploy code (no impact)
2. **Phase 2**: Run migrations (adds tables only)
3. **Phase 3**: Initialize tracking (monitoring only)
4. **Phase 4**: Switch routes (activates protection)
### Risk Mitigation
- ✅ Backward compatible
- ✅ No breaking changes
- ✅ Existing tokens remain valid
- ✅ Quick rollback possible
- ✅ Comprehensive testing
## Testing Results
```
✅ All 10 security tests passed
✅ Generic errors working
✅ Lockout logic verified
✅ Token enhancements tested
```
## Next Steps
1. **Deploy database migrations** (safe)
2. **Deploy new files** (no impact)
3. **Test in staging** if available
4. **Gradual production rollout**
5. **Monitor login_attempts table**
## Monitoring Commands
```bash
# Check failed login attempts
sqlite3 database.db "SELECT identifier, COUNT(*) as attempts FROM login_attempts WHERE success = 0 AND attempt_time > datetime('now', '-1 hour') GROUP BY identifier"
# View recent login activity
sqlite3 database.db "SELECT * FROM login_attempts ORDER BY attempt_time DESC LIMIT 10"
# Check locked accounts
sqlite3 database.db "SELECT identifier FROM login_attempts WHERE success = 0 GROUP BY identifier HAVING COUNT(*) >= 5"
```
-232
View File
@@ -1,232 +0,0 @@
# Authentication Security V2 Deployment Plan
## Overview
This deployment adds remaining authentication security fixes identified in the security scan.
## New Security Features
### 1. Rate Limiting Bypass Fix ✅
- **File**: `src/utils/rateLimitSecurity.js`
- **Fix**: Properly validates JWT before skipping rate limit
- **Impact**: Prevents attackers from bypassing with invalid tokens
### 2. Password Complexity Requirements ✅
- **File**: `src/utils/passwordValidation.js`
- **Features**:
- Minimum 12 characters (up from 6)
- Must contain: uppercase, lowercase, numbers, special chars
- Password strength scoring (zxcvbn)
- Context-aware validation (admin vs gallery)
- Configurable bcrypt rounds
### 3. Token Revocation System ✅
- **Files**: `src/utils/tokenRevocation.js`, migration
- **Features**:
- Revoke individual tokens
- Revoke all user tokens
- Automatic cleanup of expired revocations
- Check on every auth request
### 4. Enhanced Auth Routes ✅
- **File**: `src/routes/auth-enhanced-v2.js`
- **Features**:
- Password change endpoint with validation
- Real-time password strength checking
- Better error responses with feedback
## Dependencies to Install
```bash
npm install zxcvbn@4.4.2
```
## Database Migrations
```sql
-- Token revocation tables
CREATE TABLE revoked_tokens (
id INTEGER PRIMARY KEY,
token_id TEXT UNIQUE NOT NULL,
user_id INTEGER,
token_type TEXT,
revoked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
reason TEXT,
metadata TEXT
);
CREATE TABLE user_token_revocations (
user_id INTEGER PRIMARY KEY,
revoked_at TIMESTAMP NOT NULL,
reason TEXT
);
```
## Deployment Steps
### Phase 1: Preparation (Day 1)
1. **Install Dependencies**
```bash
cd backend
npm install zxcvbn@4.4.2
```
2. **Run Migrations**
```bash
docker exec wedding-photo-sharing-backend-1 node scripts/add-token-revocation-tables.js
```
3. **Deploy New Files** (No impact yet)
- `rateLimitSecurity.js`
- `passwordValidation.js`
- `tokenRevocation.js`
- `auth-enhanced-v2.js`
### Phase 2: Testing (Day 2)
1. **Test Rate Limiting Fix**
```bash
# Try with invalid token
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3001/api/admin/events
# Should apply rate limiting
```
2. **Test Password Validation**
```bash
node -e "
const {validatePassword} = require('./src/utils/passwordValidation');
console.log(validatePassword('weak'));
console.log(validatePassword('StrongP@ssw0rd123'));
"
```
### Phase 3: Gradual Activation (Day 3)
#### Step 1: Update Server.js for Rate Limiting
```javascript
// Replace in server.js
const { createSecureSkipFunction, logRateLimitHit } = require('./src/utils/rateLimitSecurity');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: process.env.NODE_ENV === 'development' ? 1000 : 100,
skip: createSecureSkipFunction(), // NEW: Secure skip function
handler: (req, res) => {
logRateLimitHit(req, res); // NEW: Logging
res.status(429).json({
error: 'Too many requests from this IP, please try again later.'
});
}
});
```
#### Step 2: Update Auth Routes
```javascript
// In server.js, change to v2
const authRoutes = require('./src/routes/auth-enhanced-v2');
```
#### Step 3: Update Middleware
```javascript
// Update imports to use v2
const { adminAuth } = require('./src/middleware/auth-enhanced-v2');
```
#### Step 4: Update Event Creation
```javascript
// In adminEvents.js, add password validation
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
// In the POST route, add validation before hashing
```
#### Step 5: Initialize Token Revocation
```javascript
// In server.js, after initializeCleanupJob()
const { initializeRevocationCleanup } = require('./src/utils/tokenRevocation');
initializeRevocationCleanup();
```
## Environment Variables
Add to `.env`:
```bash
# Bcrypt rounds (12-14 recommended)
BCRYPT_ROUNDS=12
```
## Testing Checklist
- [ ] Invalid tokens can't bypass rate limiting
- [ ] Weak passwords are rejected
- [ ] Password change requires strong password
- [ ] Tokens can be revoked
- [ ] Revoked tokens are rejected
- [ ] Admin passwords require higher strength
- [ ] Gallery passwords check for event name
## Rollback Plan
### Quick Rollback
```bash
# Revert server.js changes
git checkout HEAD -- server.js
# Restart
docker-compose restart backend
```
### Rollback Specific Features
1. **Rate Limiting**: Revert to old skip function
2. **Password Validation**: Remove validation calls
3. **Token Revocation**: Skip revocation checks
## Monitoring
### Check Password Validation Failures
```bash
docker-compose logs backend | grep "Password validation failed"
```
### Check Rate Limiting
```bash
docker-compose logs backend | grep "Rate limit"
```
### Check Token Revocations
```bash
docker exec wedding-photo-sharing-backend-1 node -e "
const {db} = require('./src/database/db');
db('revoked_tokens').count().first()
.then(r => console.log('Revoked tokens:', r['count(*)'] || 0))
.then(() => db.destroy());
"
```
## Security Improvements
| Feature | Before | After |
|---------|---------|--------|
| Rate Limiting | Can bypass with invalid token | Properly validated |
| Password Length | 6 chars | 12 chars minimum |
| Password Complexity | None | Upper+lower+number+special |
| Password Strength | Not checked | zxcvbn scoring |
| Token Revocation | Not possible | Full revocation system |
| Bcrypt Rounds | Fixed (10) | Configurable (12) |
## Performance Considerations
1. **Password Validation**: ~50ms per check (zxcvbn)
2. **Token Revocation**: Adds 1 DB query per request
3. **Bcrypt Rounds**: 12 rounds = ~250ms (vs 100ms for 10)
## Success Criteria
- ✅ No invalid tokens bypass rate limiting
- ✅ All new passwords meet complexity requirements
- ✅ Password change works with validation
- ✅ Tokens can be revoked on logout
- ✅ No performance degradation > 100ms
-114
View File
@@ -1,114 +0,0 @@
# Authentication V2 Security Fixes Summary
## What We Fixed
### 1. ✅ Rate Limiting Bypass (CRITICAL)
**Issue**: Invalid JWT tokens could bypass rate limiting
**Fix**: Created `rateLimitSecurity.js` that properly validates tokens
**Impact**: Attackers can no longer spam requests with invalid tokens
### 2. ✅ Weak Password Requirements (HIGH)
**Issue**: Only 6 character minimum, no complexity
**Fix**: Created `passwordValidation.js` with:
- 12 character minimum
- Must have: uppercase, lowercase, numbers, special chars
- Password strength scoring (zxcvbn)
- Context-aware validation (prevents username/event name in password)
- Configurable bcrypt rounds (default 12)
**Impact**: Much stronger passwords, resistant to brute force
### 3. ✅ Token Revocation (MEDIUM)
**Issue**: No way to invalidate tokens before expiration
**Fix**: Created `tokenRevocation.js` with full revocation system
- Individual token revocation
- User-level revocation (all tokens)
- Automatic cleanup
- Database tables for tracking
**Impact**: Can now invalidate compromised tokens
### 4. ✅ Enhanced Authentication Routes
**Fix**: Created `auth-enhanced-v2.js` with:
- Password change endpoint with validation
- Real-time password strength API
- Better error messages with feedback
**Impact**: Users get helpful password feedback
## Files Created
```
backend/
├── src/
│ ├── utils/
│ │ ├── rateLimitSecurity.js (118 lines)
│ │ ├── passwordValidation.js (267 lines)
│ │ └── tokenRevocation.js (127 lines)
│ ├── routes/
│ │ ├── auth-enhanced-v2.js (332 lines)
│ │ └── adminEvents-enhanced.js (partial)
│ └── middleware/
│ └── auth-enhanced-v2.js (updated)
├── migrations/
│ └── 017_add_token_revocation_tables.js
├── scripts/
│ ├── add-token-revocation-tables.js
│ └── test-auth-v2-fixes.js
└── server-enhanced.js (partial)
```
## Deployment Status
### Ready to Deploy ✅
- All code written and tested
- Migration scripts ready
- Test scripts available
- Rollback plan documented
### Required Actions
1. Install `zxcvbn` dependency
2. Run token revocation migration
3. Update server.js with new imports
4. Update auth routes to v2
5. Test thoroughly before production
## Security Improvements Summary
| Vulnerability | Severity | Status | Fix |
|--------------|----------|---------|-----|
| Rate Limiting Bypass | 🔴 Critical | ✅ Fixed | Proper token validation |
| Weak Passwords | 🔴 High | ✅ Fixed | 12 chars + complexity |
| No Token Revocation | 🟡 Medium | ✅ Fixed | Full revocation system |
| Fixed Bcrypt Rounds | 🟡 Medium | ✅ Fixed | Configurable (env var) |
| No Password Feedback | 🟡 Low | ✅ Fixed | Strength API endpoint |
## What's Still Pending
From the original auth flaws, these remain lower priority:
1. **In-memory session storage** - Works fine for single instance
2. **No refresh tokens** - 24h tokens are reasonable for this use case
3. **Fixed token expiration** - Could make configurable later
## Testing Commands
```bash
# Test rate limiting fix
node scripts/test-auth-v2-fixes.js
# Test password validation
node -e "
const {validatePassword} = require('./src/utils/passwordValidation');
console.log(validatePassword('Test123!Pass'));
"
# Check if tables exist
docker exec wedding-photo-sharing-backend-1 node scripts/add-token-revocation-tables.js
```
## Next Steps
1. Review `AUTH_V2_DEPLOYMENT_PLAN.md`
2. Install zxcvbn: `npm install zxcvbn@4.4.2`
3. Run migrations
4. Deploy incrementally
5. Monitor for issues
All critical authentication vulnerabilities have been addressed with production-ready fixes!
+9 -3
View File
@@ -1,5 +1,8 @@
FROM node:18-alpine AS builder
# Add build argument for cache busting
ARG CACHEBUST=1
WORKDIR /app
# Copy package files
@@ -16,8 +19,8 @@ FROM node:18-alpine
WORKDIR /app
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Install dumb-init for proper signal handling and postgresql-client for database checks
RUN apk add --no-cache dumb-init postgresql-client
# Create non-root user
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
@@ -26,6 +29,9 @@ RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs . .
# Make wait script executable
RUN chmod +x wait-for-db.sh
# Create necessary directories
RUN mkdir -p storage/events/active storage/events/archived storage/thumbnails data logs && \
chown -R nodejs:nodejs storage data logs
@@ -35,4 +41,4 @@ USER nodejs
EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]
CMD ["./wait-for-db.sh", "node", "server.js"]
-215
View File
@@ -1,215 +0,0 @@
# Safe Authentication Security Activation Plan
## Current Situation Analysis
### ✅ What's Already Protected:
- **SQL Injection**: Fully protected with parameterized queries
- **Rate Limiting**: Basic rate limiting active (5 attempts/15 min on /auth)
- **Password Hashing**: Bcrypt in use
- **CORS**: Properly configured
### ❌ What's NOT Protected:
- **No Account Lockout**: After rate limit, users can keep trying
- **No Audit Trail**: Can't track attack patterns
- **No Session Invalidation**: Can't force logout
- **Limited Token Security**: Basic JWT validation only
## Potential Problems & Solutions
### Problem 1: Existing User Sessions
**Risk**: Users might get logged out unexpectedly
**Solution**:
- Enhanced auth accepts old tokens (backward compatible)
- Tokens remain valid until natural expiration
- Only new features (IP check, password change detection) are additions
### Problem 2: Accidental Lockouts
**Risk**: Legitimate users locked out due to typos
**Solution**:
- 5 attempts is reasonable (not too strict)
- 30-minute lockout (not permanent)
- Clear lockout message with retry time
- Admin bypass SQL query ready
### Problem 3: Database Migration Failure
**Risk**: Schema changes could fail
**Solution**:
- Migrations only ADD tables/columns (no modifications)
- Automatic backup before migration
- Rollback plan ready
- SQLite is forgiving with schema changes
### Problem 4: Performance Impact
**Risk**: Login tracking could slow down auth
**Solution**:
- Indexed columns for performance
- Automatic cleanup of old records
- Async logging (non-blocking)
## Step-by-Step Activation Plan
### Phase 1: Pre-Flight Checks (NOW)
```bash
# Run safety check script
cd backend
node scripts/safe-auth-deployment.js
```
This will:
- ✓ Check database health
- ✓ Count active sessions
- ✓ Create backup
- ✓ Test enhanced auth modules
### Phase 2: Database Preparation (SAFE)
```bash
# Run in Docker
docker exec wedding-photo-sharing-backend-1 npx knex migrate:latest
```
Creates:
- `login_attempts` table (new)
- Security columns in `admin_users` (nullable)
### Phase 3: Test Without Activation
```bash
# Test enhanced auth endpoints
chmod +x scripts/test-auth-deployment.sh
./scripts/test-auth-deployment.sh
```
Verifies enhanced auth works before switching
### Phase 4: Gradual Activation
#### Option A: Canary Deployment (SAFEST)
Add temporary route to test:
```javascript
// In server.js, add both temporarily
app.use('/api/auth', authRoutes); // Original
app.use('/api/auth-new', authEnhancedRoutes); // Test enhanced
```
Test with `/api/auth-new/admin/login` first
#### Option B: Feature Flag (RECOMMENDED)
```javascript
// In server.js
const useEnhancedAuth = process.env.USE_ENHANCED_AUTH === 'true';
const authRoutes = useEnhancedAuth
? require('./src/routes/auth-enhanced')
: require('./src/routes/auth');
```
Then activate with environment variable
#### Option C: Direct Switch (FASTER)
```javascript
// Change in server.js
const authRoutes = require('./src/routes/auth-enhanced');
// Add after DB init
const { initializeCleanupJob } = require('./src/utils/authSecurity');
initializeCleanupJob();
```
### Phase 5: Monitor After Activation
```bash
# Run monitoring script
node scripts/monitor-auth-health.js
```
Watch for:
- Sudden spike in failures
- Multiple lockouts
- Low success rate
## Rollback Procedures
### Quick Rollback (< 30 seconds):
```bash
# In server.js, revert to:
const authRoutes = require('./src/routes/auth');
# Restart
docker-compose restart backend
```
### Clear All Lockouts:
```bash
docker exec wedding-photo-sharing-backend-1 node -e "
const {db} = require('./src/database/db');
db('login_attempts').where('success', false).delete()
.then(() => console.log('Lockouts cleared'))
.then(() => db.destroy());
"
```
### Emergency Admin Access:
```sql
-- If admin is locked out
DELETE FROM login_attempts WHERE identifier = 'admin';
```
## Success Criteria
After activation, you should see:
1. ✅ Failed login attempts recorded in database
2. ✅ Account lockout after 5 failures
3. ✅ Logout endpoint working
4. ✅ No increase in auth errors
5. ✅ Existing users still able to login
## Timeline Recommendation
**Day 1 (Now)**:
- Run migrations ✓
- Deploy code ✓
- Test endpoints
**Day 2**:
- Monitor current auth patterns
- Run test script during low traffic
**Day 3**:
- Activate with feature flag
- Monitor closely for 2 hours
- Full activation if stable
**Day 4+**:
- Review login_attempts data
- Adjust thresholds if needed
- Plan 2FA implementation
## Commands Reference
```bash
# Activate enhanced auth
docker exec -it wedding-photo-sharing-backend-1 /bin/sh
vi server.js # Make changes
exit
docker-compose restart backend
# Monitor
docker-compose logs -f backend | grep -i auth
# Check lockouts
docker exec wedding-photo-sharing-backend-1 node -e "
const {db} = require('./src/database/db');
db('login_attempts')
.select('identifier')
.where('success', false)
.where('attempt_time', '>', new Date(Date.now() - 15*60*1000).toISOString())
.groupBy('identifier')
.havingRaw('COUNT(*) >= 5')
.then(locked => console.log('Locked accounts:', locked))
.then(() => db.destroy());
"
```
## Final Safety Notes
1. **It's been tested**: 10/10 unit tests pass
2. **It's backward compatible**: Old tokens work
3. **It's gradual**: Can activate features separately
4. **It's reversible**: Quick rollback available
5. **It's monitored**: Health checking included
The enhanced auth is designed to be transparent to users while significantly improving security. The only visible change is lockout messages after failed attempts.
-167
View File
@@ -1,167 +0,0 @@
# Security Fixes Deployment Complete ✅
## Current Protection Status
### 🛡️ FULLY PROTECTED Against:
1. **SQL Injection**
- All `whereRaw` queries replaced with parameterized queries
- LIKE patterns properly escaped
- Input validation for all user inputs
- **Status**: ACTIVE & PROTECTING
2. **Brute Force Attacks**
- Account lockout after 5 failed attempts
- 30-minute lockout duration
- IP and user agent tracking
- **Status**: ACTIVE & PROTECTING
3. **User Enumeration**
- Generic error messages for all auth failures
- Returns "Invalid credentials" consistently
- **Status**: ACTIVE & PROTECTING
4. **Session Security**
- Enhanced JWT with issuer validation
- IP tracking in tokens
- Password change detection
- Logout endpoint functional
- **Status**: ACTIVE & PROTECTING
5. **Audit Trail**
- All login attempts tracked in database
- Success/failure logging with timestamps
- IP address and user agent recording
- **Status**: ACTIVE & LOGGING
## What Was Done
### Database Changes
- ✅ Created `login_attempts` table for tracking
- ✅ Added security columns to `admin_users`:
- `password_changed_at`
- `last_login_ip`
- `two_factor_enabled`
- `two_factor_secret`
### Code Changes
- ✅ SQL injection fixes in 3 files
- ✅ Enhanced auth middleware deployed
- ✅ Enhanced auth routes active
- ✅ Security utilities in place
- ✅ Cleanup job running
### Files Modified/Created
```
backend/
├── src/
│ ├── utils/
│ │ ├── sqlSecurity.js ✅
│ │ └── authSecurity.js ✅
│ ├── middleware/
│ │ └── auth-enhanced.js ✅
│ └── routes/
│ ├── auth-enhanced.js ✅
│ ├── adminDashboard.js ✅ (SQL fixes)
│ ├── adminEvents.js ✅ (SQL fixes)
│ └── adminPhotos.js ✅ (SQL fixes)
└── server.js ✅ (using enhanced auth)
```
## Monitoring Commands
### Check Login Attempts
```bash
docker exec wedding-photo-sharing-backend-1 node -e "
const {db} = require('./src/database/db');
db('login_attempts')
.orderBy('attempt_time', 'desc')
.limit(10)
.then(attempts => {
console.log('Recent login attempts:');
attempts.forEach(a => {
console.log(\`\${a.attempt_time} - \${a.identifier} - \${a.success ? 'SUCCESS' : 'FAILED'}\`);
});
})
.then(() => db.destroy());
"
```
### Check Locked Accounts
```bash
docker exec wedding-photo-sharing-backend-1 node -e "
const {db} = require('./src/database/db');
db('login_attempts')
.select('identifier')
.where('success', false)
.where('attempt_time', '>', new Date(Date.now() - 15*60*1000).toISOString())
.groupBy('identifier')
.havingRaw('COUNT(*) >= 5')
.then(locked => console.log('Locked accounts:', locked))
.then(() => db.destroy());
"
```
### Monitor Health
```bash
node scripts/monitor-auth-health.js
```
## Rollback Plan (If Needed)
### Quick Rollback
```bash
# Restore original server.js
cp server.js.backup.1752359680463 server.js
# Restart
docker-compose restart backend
```
### Clear Lockouts
```bash
docker exec wedding-photo-sharing-backend-1 node -e "
const {db} = require('./src/database/db');
db('login_attempts').where('success', false).delete()
.then(() => console.log('All lockouts cleared'))
.then(() => db.destroy());
"
```
## Next Steps
### Immediate
1. Monitor logs for any auth errors
2. Watch for excessive lockouts
3. Review login attempts daily
### Short Term (1-2 weeks)
1. Analyze login patterns
2. Adjust lockout thresholds if needed
3. Set up alerts for suspicious activity
### Long Term
1. Implement 2FA (columns already added)
2. Add IP whitelisting for admins
3. Implement password complexity requirements
4. Add password expiration policies
## Security Improvements Summary
| Vulnerability | Before | After | Impact |
|--------------|---------|--------|---------|
| SQL Injection | ❌ Direct interpolation | ✅ Parameterized queries | Critical fix |
| Brute Force | ❌ Unlimited attempts | ✅ 5 attempt lockout | High impact |
| User Enum | ❌ Different errors | ✅ Generic errors | Medium impact |
| Audit Trail | ❌ No tracking | ✅ Complete logging | High value |
| Session Mgmt | ❌ Basic JWT | ✅ Enhanced validation | Medium impact |
## Final Notes
- All fixes are backward compatible
- Existing sessions remain valid
- No user impact expected
- Quick rollback available
- Monitoring in place
The application is now significantly more secure with protection against common attack vectors. The enhanced authentication system provides defense-in-depth with multiple layers of security.
-158
View File
@@ -1,158 +0,0 @@
# SQL Injection Fix Migration Guide
## Overview
This document describes the SQL injection vulnerability fixes applied to the PicPeak backend and the migration process for deploying these fixes to production.
## Vulnerabilities Fixed
### 1. WhereRaw Date Queries (High Risk)
**Location**: `adminDashboard.js`
- **Issue**: Direct string interpolation in SQL date calculations
- **Example**: `.whereRaw(\`timestamp >= datetime("now", "-${days} days")\`)`
- **Fix**: Replaced with parameterized queries using ISO date strings
### 2. LIKE Pattern Injection (Medium Risk)
**Locations**: `adminEvents.js`, `adminPhotos.js`
- **Issue**: Unescaped user input in LIKE queries
- **Example**: `.where('event_name', 'like', \`%${search}%\`)`
- **Fix**: Added proper escaping for LIKE special characters (%, _, \)
### 3. Dynamic Column/Order Injection (Low Risk)
**Locations**: Various sorting operations
- **Issue**: Unvalidated column names in ORDER BY
- **Fix**: Whitelist validation for sort columns and orders
## Files Changed
1. **Created**: `backend/src/utils/sqlSecurity.js`
- Central security utility functions
- `sanitizeDays()` - Validates numeric input
- `escapeLikePattern()` - Escapes LIKE wildcards
- `validateSortColumn()` - Whitelist validation
- `validateSortOrder()` - Ensures only 'asc' or 'desc'
2. **Modified**: `backend/src/routes/adminDashboard.js`
- Lines 21-24, 39-41, 45-47, 57-61, 65-68: Replaced whereRaw with parameterized queries
- Line 4: Added security utility imports
- Line 198: Added sanitizeDays for analytics
3. **Modified**: `backend/src/routes/adminEvents.js`
- Line 11: Added escapeLikePattern import
- Lines 156-161: Escaped search patterns in LIKE queries
4. **Modified**: `backend/src/routes/adminPhotos.js`
- Line 9: Added escapeLikePattern import
- Lines 477-478: Escaped search patterns in LIKE queries
## Migration Steps
### 1. Pre-Deployment Testing
```bash
# Run security utility tests
cd backend
node scripts/test-sql-security.js
# Run verification script
node scripts/verify-sql-fixes.js
```
### 2. Development Environment Testing
```bash
# Start development server
npm run dev
# Test key endpoints:
curl http://localhost:3001/api/admin/dashboard/stats -H "Authorization: Bearer YOUR_TOKEN"
curl http://localhost:3001/api/admin/events?search=test -H "Authorization: Bearer YOUR_TOKEN"
curl http://localhost:3001/api/admin/dashboard/analytics?days=7 -H "Authorization: Bearer YOUR_TOKEN"
```
### 3. Production Deployment
#### Option A: Docker Deployment
```bash
# Pull latest changes
git pull
# Rebuild and restart
docker-compose down
docker-compose up -d --build
```
#### Option B: PM2 Deployment
```bash
# Pull latest changes
git pull
# Install dependencies (if any)
cd backend
npm install
# Restart with PM2
pm2 restart picpeak-backend
```
### 4. Post-Deployment Verification
1. **Monitor Logs**:
```bash
# Docker
docker-compose logs -f backend
# PM2
pm2 logs picpeak-backend
```
2. **Test Critical Functions**:
- Admin dashboard loads correctly
- Event search works with special characters
- Analytics charts display properly
- Photo search functions normally
3. **Check Error Rates**:
- Monitor for any 500 errors
- Check database query logs for errors
## Testing Special Characters
After deployment, test these scenarios:
1. **Search with wildcards**: Search for "50%" or "user_name"
2. **Search with quotes**: Search for "O'Brien"
3. **Date range**: Change analytics to different day ranges
4. **Malicious input**: Try "'; DROP TABLE --" (should return no results)
## Rollback Instructions
If issues occur, see `SQL_INJECTION_FIX_ROLLBACK.md` for immediate rollback steps.
## Performance Impact
- Minimal performance impact expected
- Date calculations now use ISO strings instead of SQLite functions
- LIKE pattern escaping adds negligible overhead
- All changes maintain existing query optimization
## Security Improvements
1. **Eliminated SQL Injection Vectors**: No more direct string interpolation
2. **Input Validation**: All user inputs are validated/sanitized
3. **Parameterized Queries**: Using Knex's built-in parameterization
4. **Defense in Depth**: Multiple layers of protection
## Future Recommendations
1. Add request validation middleware
2. Implement rate limiting on search endpoints
3. Add SQL query logging for security auditing
4. Consider using prepared statements for complex queries
## Questions/Support
If you encounter any issues during migration:
1. Check the rollback plan first
2. Review error logs for specific issues
3. Test individual endpoints to isolate problems
4. Contact development team if needed
-94
View File
@@ -1,94 +0,0 @@
# SQL Injection Fix Rollback Plan
## Overview
This document provides a rollback plan in case the SQL injection fixes cause issues in production.
## Changes Made
1. **Created**: `backend/src/utils/sqlSecurity.js` - Central security utilities
2. **Modified**: `backend/src/routes/adminDashboard.js` - Replaced whereRaw with parameterized queries
3. **Modified**: `backend/src/routes/adminPhotos.js` - Added LIKE pattern escaping
4. **Modified**: `backend/src/routes/adminEvents.js` - Added LIKE pattern escaping
## Quick Rollback Steps
### Step 1: Revert Code Changes
If issues occur, run these commands to revert:
```bash
# Navigate to backend directory
cd backend
# Revert specific files
git checkout HEAD -- src/routes/adminDashboard.js
git checkout HEAD -- src/routes/adminPhotos.js
git checkout HEAD -- src/routes/adminEvents.js
# Remove the new security utility file
rm src/utils/sqlSecurity.js
```
### Step 2: Restart Services
```bash
# If using Docker
docker-compose restart backend
# If using PM2
pm2 restart picpeak-backend
```
## Verification After Rollback
1. Check admin dashboard loads: `/admin/dashboard`
2. Test event search functionality
3. Test photo search functionality
4. Verify analytics charts display correctly
## Symptoms That May Require Rollback
1. **Dashboard Statistics Not Loading**
- Empty or NaN values in stats
- Analytics charts not rendering
2. **Search Features Broken**
- Event search returns no results
- Photo search returns errors
- Special characters in search causing issues
3. **Date Filtering Issues**
- Activity logs not showing correct date ranges
- Analytics showing incorrect time periods
## Safe Testing Before Production
1. **Test in Development First**:
```bash
cd backend
npm run dev
```
2. **Test Key Features**:
- Admin dashboard stats: `http://localhost:3001/api/admin/dashboard/stats`
- Analytics: `http://localhost:3001/api/admin/dashboard/analytics?days=7`
- Event search: `http://localhost:3001/api/admin/events?search=test`
- Photo search: `http://localhost:3001/api/admin/events/1/photos?search=test`
3. **Monitor Logs**:
```bash
# Docker logs
docker-compose logs -f backend
# PM2 logs
pm2 logs picpeak-backend
```
## Emergency Contacts
- Keep database backups before deploying
- Have monitoring alerts for 500 errors
- Document any custom SQL queries in use
## Post-Rollback Actions
If rollback is needed:
1. Document the specific issue encountered
2. Create test cases for the failure scenario
3. Fix the issue in development
4. Re-test thoroughly before re-deploying
-64
View File
@@ -1,64 +0,0 @@
# SQL Injection Fix Summary
## Quick Overview
Fixed SQL injection vulnerabilities in the admin panel endpoints by:
1. Replacing dangerous `whereRaw` queries with parameterized queries
2. Escaping special characters in LIKE patterns
3. Validating sort columns and orders
## Test Results
✅ All 31 security tests passed
✅ Verification script confirms fixes working
✅ No breaking changes to API functionality
## Changed Files
```
backend/
├── src/
│ ├── utils/
│ │ └── sqlSecurity.js (NEW - 117 lines)
│ └── routes/
│ ├── adminDashboard.js (6 changes)
│ ├── adminEvents.js (2 changes)
│ └── adminPhotos.js (2 changes)
└── scripts/
├── test-sql-security.js (NEW)
└── verify-sql-fixes.js (NEW)
```
## Before & After Examples
### Date Range Queries
```javascript
// ❌ BEFORE (Vulnerable)
.whereRaw(`timestamp >= datetime("now", "-${days} days")`)
// ✅ AFTER (Safe)
const startDate = new Date();
startDate.setDate(startDate.getDate() - sanitizeDays(days));
.where('timestamp', '>=', startDate.toISOString())
```
### LIKE Queries
```javascript
// ❌ BEFORE (Vulnerable)
.where('event_name', 'like', `%${search}%`)
// ✅ AFTER (Safe)
const escapedSearch = escapeLikePattern(search);
.where('event_name', 'like', `%${escapedSearch}%`)
```
## Deployment Checklist
- [ ] Run `node scripts/test-sql-security.js` (should show 31/31 passed)
- [ ] Test in development environment
- [ ] Review rollback plan (`SQL_INJECTION_FIX_ROLLBACK.md`)
- [ ] Deploy to production
- [ ] Monitor logs for errors
- [ ] Test search functionality with special characters
## Risk Assessment
- **Risk Level**: Low (with proper testing)
- **Breaking Changes**: None
- **Performance Impact**: Minimal
- **Rollback Time**: < 2 minutes
+229
View File
@@ -0,0 +1,229 @@
# Enhanced Backup System Test Suite
This directory contains comprehensive tests for the enhanced backup system with S3 support.
## Test Structure
### Unit Tests
- `services/backupService.enhanced.test.js` - Unit tests for the enhanced backup service
- Configuration management
- S3 backup functionality
- Manifest generation
- Error handling and recovery
- Backward compatibility (local and rsync)
- Service lifecycle management
### Integration Tests
- `integration/backup-s3.test.js` - Integration tests for S3 backups
- Real S3/MinIO connection tests
- Full backup process with actual files
- Incremental backup verification
- Manifest storage and retrieval
- Error recovery scenarios
### Manual Integration Test Script
- `../scripts/test-backup-integration.js` - Comprehensive manual testing script
- Can test against MinIO, AWS S3, or any S3-compatible service
- Tests all backup types (S3, local, rsync)
- Performance testing with large files
- Detailed progress reporting
## Running Tests
### Prerequisites
1. **For Unit Tests**: No special setup required, all dependencies are mocked.
2. **For Integration Tests**: Requires a running S3-compatible service (MinIO recommended)
```bash
# Start MinIO using Docker
docker run -d \
-p 9000:9000 \
-p 9001:9001 \
--name minio-test \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
minio/minio server /data --console-address ":9001"
```
3. **Environment Variables** (for integration tests):
```bash
# Optional - defaults work with local MinIO
export TEST_S3_ENDPOINT=http://localhost:9000
export TEST_S3_ACCESS_KEY=minioadmin
export TEST_S3_SECRET_KEY=minioadmin
# Skip S3 tests if no S3 service available
export SKIP_S3_TESTS=true
```
### Running Unit Tests
```bash
# Run all backup service tests
npm test -- __tests__/services/backupService.enhanced.test.js
# Run specific test suite
npm test -- __tests__/services/backupService.enhanced.test.js -t "S3 Backup Functionality"
# Run with coverage
npm test -- --coverage __tests__/services/backupService.enhanced.test.js
```
### Running Integration Tests
```bash
# Ensure MinIO is running first!
# Run S3 integration tests
npm test -- __tests__/integration/backup-s3.test.js
# Run with verbose output
npm test -- __tests__/integration/backup-s3.test.js --verbose
# Skip S3 tests if needed
SKIP_S3_TESTS=true npm test -- __tests__/integration/backup-s3.test.js
```
### Running Manual Integration Tests
```bash
# Test with local MinIO (default)
node scripts/test-backup-integration.js
# Test with AWS S3
node scripts/test-backup-integration.js \
--endpoint https://s3.amazonaws.com \
--access-key YOUR_ACCESS_KEY \
--secret-key YOUR_SECRET_KEY \
--bucket your-test-bucket
# Test local backup
node scripts/test-backup-integration.js --type local
# Test with cleanup after completion
node scripts/test-backup-integration.js --cleanup
# Verbose output
node scripts/test-backup-integration.js --verbose
```
## Test Coverage
The test suite covers:
### Configuration
- ✅ Database configuration retrieval
- ✅ JSON parsing and error handling
- ✅ Configuration validation
- ✅ Required field validation
### S3 Functionality
- ✅ S3 client initialization
- ✅ Connection testing
- ✅ File upload with progress tracking
- ✅ Large file handling (multipart upload)
- ✅ Metadata and custom headers
- ✅ Error handling and retries
### Backup Process
- ✅ Full backup execution
- ✅ Incremental backup (changed files only)
- ✅ File checksum calculation and comparison
- ✅ Database backup inclusion
- ✅ Archive inclusion toggle
- ✅ File size limits
### Manifest Generation
- ✅ Full manifest generation
- ✅ Incremental manifest with parent reference
- ✅ JSON and YAML format support
- ✅ Manifest validation
- ✅ S3 manifest storage and retrieval
- ✅ Checksum verification
### Error Handling
- ✅ S3 connection failures
- ✅ File read errors
- ✅ Individual file failure recovery
- ✅ Retry logic with exponential backoff
- ✅ Email notifications on failure
- ✅ Concurrent backup prevention
### Backward Compatibility
- ✅ Local directory backup
- ✅ Rsync backup
- ✅ Existing manifest format support
### Service Management
- ✅ Cron job scheduling
- ✅ Service start/stop
- ✅ Manual backup triggering
- ✅ Backup history and status
## Mock Setup
The unit tests use comprehensive mocking:
```javascript
// Database mocking
jest.mock('../../src/database/db');
// S3 client mocking
jest.mock('../../src/services/storage/s3Storage');
// File system mocking
const mockFs = require('mock-fs');
// Cron job mocking
jest.mock('node-cron');
```
## CI/CD Integration
To run tests in CI/CD pipeline:
```yaml
# Example GitHub Actions
- name: Run Unit Tests
run: npm test -- __tests__/services/backupService.enhanced.test.js
- name: Start MinIO
run: |
docker run -d \
-p 9000:9000 \
--name minio-test \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
minio/minio server /data
- name: Run Integration Tests
run: npm test -- __tests__/integration/backup-s3.test.js
```
## Debugging Tests
```bash
# Run tests in debug mode
node --inspect-brk ./node_modules/.bin/jest __tests__/services/backupService.enhanced.test.js
# Run single test with console output
npm test -- __tests__/services/backupService.enhanced.test.js -t "should perform S3 backup" --verbose
```
## Performance Considerations
- Integration tests create real files and S3 objects
- Each test run creates a unique S3 bucket to avoid conflicts
- Cleanup is automatic but can be disabled for debugging
- Large file tests (10MB+) are included but can be slow
## Adding New Tests
When adding new backup features:
1. Add unit tests to `backupService.enhanced.test.js`
2. Add integration tests to `backup-s3.test.js` if S3-specific
3. Update manual test script for comprehensive testing
4. Ensure mocks are properly configured
5. Document any new environment requirements
@@ -0,0 +1,506 @@
const { describe, it, expect, jest, beforeAll, afterAll, beforeEach, afterEach } = require('@jest/globals');
const { S3Client, CreateBucketCommand, DeleteBucketCommand, ListObjectsV2Command, DeleteObjectsCommand } = require('@aws-sdk/client-s3');
const path = require('path');
const fs = require('fs').promises;
const crypto = require('crypto');
// Load services
const backupService = require('../../src/services/backupService');
const S3StorageAdapter = require('../../src/services/storage/s3Storage');
const { db, initialize: initDb } = require('../../src/database/db');
const logger = require('../../src/utils/logger');
// Test configuration
const TEST_CONFIG = {
endpoint: process.env.TEST_S3_ENDPOINT || 'http://localhost:9000',
accessKeyId: process.env.TEST_S3_ACCESS_KEY || 'minioadmin',
secretAccessKey: process.env.TEST_S3_SECRET_KEY || 'minioadmin',
bucket: 'test-backup-bucket-' + Date.now(),
region: 'us-east-1'
};
describe('S3 Backup Integration Tests', () => {
let s3Client;
let testStoragePath;
let originalEnv;
beforeAll(async () => {
// Skip if no S3 endpoint configured
if (process.env.SKIP_S3_TESTS === 'true') {
console.log('Skipping S3 integration tests (SKIP_S3_TESTS=true)');
return;
}
// Save original environment
originalEnv = { ...process.env };
// Initialize S3 client for test setup
s3Client = new S3Client({
endpoint: TEST_CONFIG.endpoint,
region: TEST_CONFIG.region,
credentials: {
accessKeyId: TEST_CONFIG.accessKeyId,
secretAccessKey: TEST_CONFIG.secretAccessKey
},
forcePathStyle: true
});
// Create test bucket
try {
await s3Client.send(new CreateBucketCommand({ Bucket: TEST_CONFIG.bucket }));
console.log(`Created test bucket: ${TEST_CONFIG.bucket}`);
} catch (error) {
if (error.name !== 'BucketAlreadyOwnedByYou') {
console.error('Failed to create test bucket:', error);
throw error;
}
}
// Initialize database
await initDb();
await db.migrate.latest();
// Create test storage directory
testStoragePath = path.join(__dirname, '../fixtures/test-storage');
await fs.mkdir(testStoragePath, { recursive: true });
process.env.STORAGE_PATH = testStoragePath;
// Set up test data
await setupTestData();
// Mock logger to reduce noise
logger.info = jest.fn();
logger.debug = jest.fn();
logger.warn = jest.fn();
logger.error = jest.fn();
});
afterAll(async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
try {
// Clean up S3 bucket
await cleanupS3Bucket();
await s3Client.send(new DeleteBucketCommand({ Bucket: TEST_CONFIG.bucket }));
console.log(`Deleted test bucket: ${TEST_CONFIG.bucket}`);
} catch (error) {
console.error('Failed to cleanup S3 bucket:', error);
}
// Clean up test storage
await fs.rm(testStoragePath, { recursive: true, force: true });
// Restore environment
process.env = originalEnv;
// Close database
await db.destroy();
});
beforeEach(async () => {
if (process.env.SKIP_S3_TESTS === 'true') {
return;
}
// Clean backup tables
await db('backup_runs').del();
await db('backup_file_states').del();
await db('database_backup_runs').del();
// Configure S3 backup settings
await configureS3Backup();
});
afterEach(async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
// Clean up S3 objects created during test
await cleanupS3Bucket();
});
describe('S3 Connection and Configuration', () => {
it('should successfully connect to S3-compatible storage', async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
const s3Adapter = new S3StorageAdapter({
...TEST_CONFIG,
bucket: TEST_CONFIG.bucket,
forcePathStyle: true,
sslEnabled: false
});
const connected = await s3Adapter.testConnection();
expect(connected).toBe(true);
});
it('should validate S3 configuration before backup', async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
// Remove required configuration
await db('app_settings')
.where('setting_key', 'backup_s3_secret_key')
.del();
await backupService.runBackup();
const lastRun = await db('backup_runs')
.orderBy('started_at', 'desc')
.first();
expect(lastRun.status).toBe('failed');
expect(lastRun.error_message).toContain('S3 backup configuration incomplete');
});
});
describe('Full S3 Backup Process', () => {
it('should perform complete S3 backup with all file types', async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
// Run backup
await backupService.runBackup();
// Verify backup run completed
const backupRun = await db('backup_runs')
.orderBy('started_at', 'desc')
.first();
expect(backupRun.status).toBe('completed');
expect(backupRun.files_backed_up).toBeGreaterThan(0);
expect(backupRun.total_size_bytes).toBeGreaterThan(0);
// Verify files in S3
const s3Objects = await listS3Objects();
expect(s3Objects.length).toBeGreaterThan(0);
// Check for expected file types
const hasPhotos = s3Objects.some(obj => obj.Key.includes('events/active'));
const hasThumbnails = s3Objects.some(obj => obj.Key.includes('thumbnails'));
const hasManifest = s3Objects.some(obj => obj.Key.includes('backup-manifest'));
const hasSummary = s3Objects.some(obj => obj.Key.includes('backup-summary.json'));
expect(hasPhotos).toBe(true);
expect(hasThumbnails).toBe(true);
expect(hasManifest).toBe(true);
expect(hasSummary).toBe(true);
});
it('should handle large file uploads with multipart', async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
// Create a large test file (15MB)
const largeFilePath = path.join(testStoragePath, 'events/active/large-photo.jpg');
const largeFileSize = 15 * 1024 * 1024; // 15MB
const largeFileContent = Buffer.alloc(largeFileSize, 'x');
await fs.writeFile(largeFilePath, largeFileContent);
// Run backup
await backupService.runBackup();
// Verify large file was uploaded
const s3Objects = await listS3Objects();
const largeFileUploaded = s3Objects.some(obj =>
obj.Key.includes('large-photo.jpg') && obj.Size === largeFileSize
);
expect(largeFileUploaded).toBe(true);
});
it('should include database backup when available', async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
// Create a mock database backup
const dbBackupPath = path.join(testStoragePath, 'backups/db-backup.sql');
await fs.mkdir(path.dirname(dbBackupPath), { recursive: true });
await fs.writeFile(dbBackupPath, 'CREATE TABLE test (id INT);');
// Record database backup
await db('database_backup_runs').insert({
started_at: new Date(),
completed_at: new Date(),
status: 'completed',
backup_type: 'sqlite',
file_path: dbBackupPath,
file_size_bytes: 100,
checksum: 'test123',
statistics: JSON.stringify({ tables: {} }),
table_checksums: JSON.stringify({})
});
// Configure to include database
await db('app_settings')
.where('setting_key', 'backup_include_database')
.update({ setting_value: 'true' });
// Run backup
await backupService.runBackup();
// Verify database backup in S3
const s3Objects = await listS3Objects();
const hasDbBackup = s3Objects.some(obj => obj.Key.includes('database/db-backup.sql'));
expect(hasDbBackup).toBe(true);
});
});
describe('Incremental Backup', () => {
it('should only upload changed files in incremental backup', async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
// First backup - full
await backupService.runBackup();
const firstRun = await db('backup_runs')
.orderBy('started_at', 'desc')
.first();
const firstObjectCount = (await listS3Objects()).length;
// Wait a moment to ensure different timestamps
await new Promise(resolve => setTimeout(resolve, 100));
// Modify one file
const modifiedFile = path.join(testStoragePath, 'events/active/event1/photo1.jpg');
await fs.writeFile(modifiedFile, 'modified content');
// Second backup - incremental
await backupService.runBackup();
const secondRun = await db('backup_runs')
.orderBy('started_at', 'desc')
.first();
expect(secondRun.id).not.toBe(firstRun.id);
expect(secondRun.files_backed_up).toBe(1); // Only modified file
// Check manifest indicates incremental
if (secondRun.manifest_path) {
const manifest = await backupService.getBackupManifest(secondRun.id);
expect(manifest.manifest.incremental).toBeDefined();
expect(manifest.manifest.incremental.modified_files_count).toBe(1);
}
});
it('should track file states across backups', async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
await backupService.runBackup();
// Check file states are recorded
const fileStates = await db('backup_file_states').select('*');
expect(fileStates.length).toBeGreaterThan(0);
// Verify checksums are stored
const hasChecksums = fileStates.every(state => state.checksum !== null);
expect(hasChecksums).toBe(true);
});
});
describe('S3 Manifest Storage', () => {
it('should upload manifest to S3 and retrieve it', async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
// Configure YAML manifest format
await db('app_settings')
.where('setting_key', 'backup_manifest_format')
.update({ setting_value: '"yaml"' });
await backupService.runBackup();
const backupRun = await db('backup_runs')
.orderBy('started_at', 'desc')
.first();
expect(backupRun.manifest_path).toMatch(/^s3:\/\//);
// Retrieve manifest
const { manifest, summary } = await backupService.getBackupManifest(backupRun.id);
expect(manifest).toBeDefined();
expect(manifest.backup.id).toBeDefined();
expect(summary).toContain('BACKUP MANIFEST SUMMARY');
});
it('should validate manifest integrity', async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
await backupService.runBackup();
const backupRun = await db('backup_runs')
.orderBy('started_at', 'desc')
.first();
const validationResult = await backupService.validateBackupManifest(backupRun.manifest_path);
expect(validationResult.valid).toBe(true);
expect(validationResult.manifest).toBeDefined();
});
});
describe('Error Recovery', () => {
it('should handle S3 connection failures gracefully', async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
// Configure with invalid endpoint
await db('app_settings')
.where('setting_key', 'backup_s3_endpoint')
.update({ setting_value: '"http://invalid-endpoint:9999"' });
await backupService.runBackup();
const backupRun = await db('backup_runs')
.orderBy('started_at', 'desc')
.first();
expect(backupRun.status).toBe('failed');
expect(backupRun.error_message).toBeDefined();
});
it('should continue backup despite individual file failures', async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
// Create a file that will be deleted during backup
const tempFile = path.join(testStoragePath, 'events/active/temp.jpg');
await fs.writeFile(tempFile, 'temporary');
// Mock file deletion during backup
const originalUpload = S3StorageAdapter.prototype.upload;
let callCount = 0;
S3StorageAdapter.prototype.upload = jest.fn(async function(localPath, s3Key, options) {
callCount++;
if (callCount === 2) {
// Delete the temp file to cause an error
await fs.unlink(tempFile).catch(() => {});
}
return originalUpload.call(this, localPath, s3Key, options);
});
await backupService.runBackup();
const backupRun = await db('backup_runs')
.orderBy('started_at', 'desc')
.first();
// Should complete despite one file error
expect(backupRun.status).toBe('completed');
expect(backupRun.files_backed_up).toBeGreaterThan(0);
// Restore original method
S3StorageAdapter.prototype.upload = originalUpload;
});
it('should retry failed uploads with exponential backoff', async () => {
if (process.env.SKIP_S3_TESTS === 'true') return;
// Mock S3 upload to fail twice then succeed
const originalUpload = S3StorageAdapter.prototype.upload;
let attemptCount = 0;
S3StorageAdapter.prototype.upload = jest.fn(async function(localPath, s3Key, options) {
attemptCount++;
if (attemptCount <= 2) {
const error = new Error('Network timeout');
error.code = 'ETIMEDOUT';
throw error;
}
return originalUpload.call(this, localPath, s3Key, options);
});
await backupService.runBackup();
const backupRun = await db('backup_runs')
.orderBy('started_at', 'desc')
.first();
// Should succeed after retries
expect(backupRun.status).toBe('completed');
expect(attemptCount).toBeGreaterThan(2);
// Restore original method
S3StorageAdapter.prototype.upload = originalUpload;
});
});
// Helper functions
async function setupTestData() {
// Create test directory structure
const dirs = [
'events/active/event1',
'events/active/event2',
'events/archived',
'thumbnails',
'uploads'
];
for (const dir of dirs) {
await fs.mkdir(path.join(testStoragePath, dir), { recursive: true });
}
// Create test files
const files = [
{ path: 'events/active/event1/photo1.jpg', content: 'photo1 content' },
{ path: 'events/active/event1/photo2.jpg', content: 'photo2 content' },
{ path: 'events/active/event2/photo3.jpg', content: 'photo3 content' },
{ path: 'events/archived/old-event.zip', content: 'archived content' },
{ path: 'thumbnails/thumb1.jpg', content: 'thumbnail content' },
{ path: 'uploads/logo.png', content: 'logo content' }
];
for (const file of files) {
await fs.writeFile(
path.join(testStoragePath, file.path),
file.content
);
}
}
async function configureS3Backup() {
const settings = [
{ setting_key: 'backup_enabled', setting_value: 'true' },
{ setting_key: 'backup_destination_type', setting_value: '"s3"' },
{ setting_key: 'backup_s3_bucket', setting_value: `"${TEST_CONFIG.bucket}"` },
{ setting_key: 'backup_s3_region', setting_value: `"${TEST_CONFIG.region}"` },
{ setting_key: 'backup_s3_endpoint', setting_value: `"${TEST_CONFIG.endpoint}"` },
{ setting_key: 'backup_s3_access_key', setting_value: `"${TEST_CONFIG.accessKeyId}"` },
{ setting_key: 'backup_s3_secret_key', setting_value: `"${TEST_CONFIG.secretAccessKey}"` },
{ setting_key: 'backup_s3_force_path_style', setting_value: 'true' },
{ setting_key: 'backup_s3_ssl_enabled', setting_value: 'false' },
{ setting_key: 'backup_include_archived', setting_value: 'true' },
{ setting_key: 'backup_incremental', setting_value: 'true' },
{ setting_key: 'backup_max_file_size_mb', setting_value: '100' }
];
for (const setting of settings) {
await db('app_settings')
.insert({
setting_type: 'backup',
...setting,
created_at: new Date(),
updated_at: new Date()
})
.onConflict(['setting_type', 'setting_key'])
.merge();
}
}
async function listS3Objects() {
const response = await s3Client.send(new ListObjectsV2Command({
Bucket: TEST_CONFIG.bucket
}));
return response.Contents || [];
}
async function cleanupS3Bucket() {
try {
const objects = await listS3Objects();
if (objects.length > 0) {
await s3Client.send(new DeleteObjectsCommand({
Bucket: TEST_CONFIG.bucket,
Delete: {
Objects: objects.map(obj => ({ Key: obj.Key }))
}
}));
}
} catch (error) {
console.error('Failed to cleanup S3 objects:', error);
}
}
});
@@ -0,0 +1,751 @@
const { describe, it, expect, jest, beforeEach, afterEach } = require('@jest/globals');
const mockFs = require('mock-fs');
const path = require('path');
const crypto = require('crypto');
const { EventEmitter } = require('events');
// Mock dependencies before requiring the module
jest.mock('../../src/database/db');
jest.mock('../../src/utils/logger');
jest.mock('../../src/services/emailProcessor');
jest.mock('node-cron');
jest.mock('../../src/services/backupManifest');
jest.mock('../../src/services/storage/s3Storage');
const backupService = require('../../src/services/backupService');
const { db } = require('../../src/database/db');
const logger = require('../../src/utils/logger');
const { queueEmail } = require('../../src/services/emailProcessor');
const cron = require('node-cron');
const backupManifest = require('../../src/services/backupManifest');
const S3StorageAdapter = require('../../src/services/storage/s3Storage');
describe('Enhanced Backup Service Tests', () => {
let mockDb;
let mockS3Client;
let mockCronJob;
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
// Mock database
mockDb = {
select: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
first: jest.fn(),
insert: jest.fn(),
update: jest.fn(),
delete: jest.fn()
};
db.mockReturnValue(mockDb);
// Mock cron job
mockCronJob = {
stop: jest.fn()
};
cron.schedule.mockReturnValue(mockCronJob);
// Mock S3 client
mockS3Client = {
testConnection: jest.fn().mockResolvedValue(true),
upload: jest.fn().mockResolvedValue({ Location: 's3://bucket/key' }),
uploadStream: jest.fn().mockResolvedValue({ Location: 's3://bucket/key' }),
download: jest.fn().mockResolvedValue(),
exists: jest.fn().mockResolvedValue(false),
delete: jest.fn().mockResolvedValue(),
list: jest.fn().mockResolvedValue({ Contents: [] })
};
S3StorageAdapter.mockImplementation(() => mockS3Client);
// Mock backup manifest
backupManifest.generateManifest = jest.fn().mockResolvedValue({
backup: { id: 'test-backup-123' },
version: '2.0'
});
backupManifest.saveManifest = jest.fn().mockResolvedValue('/path/to/manifest.json');
backupManifest.loadManifest = jest.fn().mockResolvedValue({});
backupManifest.validateManifest = jest.fn();
backupManifest.generateSummaryReport = jest.fn().mockReturnValue('Summary report');
// Mock logger
logger.info = jest.fn();
logger.error = jest.fn();
logger.warn = jest.fn();
logger.debug = jest.fn();
});
afterEach(() => {
mockFs.restore();
});
describe('getBackupConfig', () => {
it('should retrieve and parse backup configuration from database', async () => {
const mockSettings = [
{ setting_key: 'backup_enabled', setting_value: 'true' },
{ setting_key: 'backup_destination_type', setting_value: '"s3"' },
{ setting_key: 'backup_s3_bucket', setting_value: '"test-bucket"' },
{ setting_key: 'backup_retention_days', setting_value: '30' }
];
mockDb.select.mockResolvedValue(mockSettings);
const config = await backupService.getBackupConfig();
expect(config).toEqual({
backup_enabled: true,
backup_destination_type: 's3',
backup_s3_bucket: 'test-bucket',
backup_retention_days: 30
});
expect(db).toHaveBeenCalledWith('app_settings');
expect(mockDb.where).toHaveBeenCalledWith('setting_type', 'backup');
});
it('should handle JSON parse errors gracefully', async () => {
const mockSettings = [
{ setting_key: 'backup_enabled', setting_value: 'invalid-json' }
];
mockDb.select.mockResolvedValue(mockSettings);
const config = await backupService.getBackupConfig();
expect(config).toEqual({
backup_enabled: 'invalid-json'
});
});
it('should return null on database error', async () => {
mockDb.select.mockRejectedValue(new Error('Database error'));
const config = await backupService.getBackupConfig();
expect(config).toBeNull();
expect(logger.error).toHaveBeenCalled();
});
});
describe('S3 Backup Functionality', () => {
beforeEach(() => {
// Mock file system
mockFs({
'/storage/events/active/event1': {
'photo1.jpg': Buffer.from('photo1 content'),
'photo2.jpg': Buffer.from('photo2 content')
},
'/storage/events/archived/event2.zip': Buffer.from('archived content'),
'/storage/thumbnails': {
'thumb1.jpg': Buffer.from('thumb1 content')
},
'/storage/uploads': {
'logo.png': Buffer.from('logo content')
}
});
process.env.STORAGE_PATH = '/storage';
});
it('should perform S3 backup with correct configuration', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 's3',
backup_s3_bucket: 'test-bucket',
backup_s3_region: 'us-east-1',
backup_s3_endpoint: 'https://s3.amazonaws.com',
backup_s3_access_key: 'test-key',
backup_s3_secret_key: 'test-secret',
backup_include_archived: true,
backup_max_file_size_mb: 100
};
mockDb.select.mockResolvedValue([]);
mockDb.where.mockReturnThis();
mockDb.first.mockResolvedValue(null);
mockDb.insert.mockResolvedValue([1]);
jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
jest.spyOn(backupService, 'getDatabaseBackupInfo').mockResolvedValue({
type: 'sqlite',
backupFile: null,
hasChanged: true
});
await backupService.runBackup();
expect(S3StorageAdapter).toHaveBeenCalledWith({
bucket: 'test-bucket',
region: 'us-east-1',
endpoint: 'https://s3.amazonaws.com',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
forcePathStyle: false,
sslEnabled: true,
maxRetries: 3,
retryDelay: 1000
});
expect(mockS3Client.testConnection).toHaveBeenCalled();
expect(mockS3Client.upload).toHaveBeenCalled();
});
it('should handle S3 upload failures gracefully', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 's3',
backup_s3_bucket: 'test-bucket',
backup_s3_access_key: 'test-key',
backup_s3_secret_key: 'test-secret'
};
mockDb.select.mockResolvedValue([]);
mockDb.insert.mockResolvedValue([1]);
mockDb.first.mockResolvedValue(null);
jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
mockS3Client.testConnection.mockRejectedValue(new Error('Connection failed'));
await backupService.runBackup();
expect(logger.error).toHaveBeenCalledWith('S3 backup failed:', expect.any(Error));
expect(mockDb.update).toHaveBeenCalledWith(expect.objectContaining({
status: 'failed',
error_message: expect.stringContaining('Connection failed')
}));
});
it('should skip unchanged files in incremental backup', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 's3',
backup_s3_bucket: 'test-bucket',
backup_s3_access_key: 'test-key',
backup_s3_secret_key: 'test-secret',
backup_incremental: true
};
// Mock existing file state
mockDb.first.mockImplementation((query) => {
if (query === undefined) {
return Promise.resolve({
file_path: 'events/active/event1/photo1.jpg',
checksum: crypto.createHash('sha256').update('photo1 content').digest('hex')
});
}
return Promise.resolve(null);
});
mockDb.select.mockResolvedValue([]);
mockDb.insert.mockResolvedValue([1]);
jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
await backupService.runBackup();
// Should skip unchanged file
const uploadCalls = mockS3Client.upload.mock.calls;
const photo1Uploaded = uploadCalls.some(call =>
call[1].includes('photo1.jpg')
);
expect(photo1Uploaded).toBe(false);
});
it('should include database backup when configured', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 's3',
backup_s3_bucket: 'test-bucket',
backup_s3_access_key: 'test-key',
backup_s3_secret_key: 'test-secret',
backup_include_database: true
};
mockDb.select.mockResolvedValue([]);
mockDb.insert.mockResolvedValue([1]);
jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
jest.spyOn(backupService, 'getDatabaseBackupInfo').mockResolvedValue({
type: 'sqlite',
backupFile: '/backup/db-backup.sql',
size: 1024000,
checksum: 'abc123',
hasChanged: false
});
// Mock database backup file
mockFs({
'/storage/events/active': {},
'/backup/db-backup.sql': Buffer.from('database backup content')
});
await backupService.runBackup();
// Verify database backup was uploaded
const uploadCalls = mockS3Client.upload.mock.calls;
const dbBackupUploaded = uploadCalls.some(call =>
call[1].includes('database/db-backup.sql')
);
expect(dbBackupUploaded).toBe(true);
});
it('should validate required S3 configuration', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 's3',
backup_s3_bucket: 'test-bucket'
// Missing access key and secret key
};
mockDb.select.mockResolvedValue([]);
mockDb.insert.mockResolvedValue([1]);
jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
await backupService.runBackup();
expect(logger.error).toHaveBeenCalledWith(
'S3 backup failed:',
expect.objectContaining({
message: expect.stringContaining('S3 backup configuration incomplete')
})
);
});
});
describe('Manifest Generation', () => {
it('should generate and save manifest for successful backup', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 'local',
backup_destination_path: '/backup',
backup_manifest_format: 'json'
};
mockDb.select.mockResolvedValue([]);
mockDb.insert.mockResolvedValue([1]);
mockDb.first.mockResolvedValue(null);
jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
mockFs({
'/storage/events/active/event1': {
'photo1.jpg': Buffer.from('photo1 content')
},
'/backup': {}
});
await backupService.runBackup();
expect(backupManifest.generateManifest).toHaveBeenCalledWith(
expect.objectContaining({
backupType: 'full',
backupPath: '/backup',
format: 'json'
})
);
expect(backupManifest.saveManifest).toHaveBeenCalled();
});
it('should generate incremental manifest when parent exists', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 'local',
backup_destination_path: '/backup'
};
const lastBackup = {
id: 1,
manifest_path: '/backup/manifests/previous.json',
manifest_id: 'previous-backup-123'
};
mockDb.select.mockResolvedValue([]);
mockDb.insert.mockResolvedValue([2]);
mockDb.first.mockImplementation(() => Promise.resolve(lastBackup));
mockDb.orderBy.mockReturnThis();
mockDb.where.mockReturnThis();
mockDb.whereNot = jest.fn().mockReturnThis();
jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
mockFs({
'/storage/events/active': {},
'/backup': {}
});
await backupService.runBackup();
expect(backupManifest.loadManifest).toHaveBeenCalledWith('/backup/manifests/previous.json');
expect(backupManifest.generateIncrementalManifest).toHaveBeenCalled();
});
it('should upload manifest to S3 for S3 backups', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 's3',
backup_s3_bucket: 'test-bucket',
backup_s3_access_key: 'test-key',
backup_s3_secret_key: 'test-secret',
backup_manifest_format: 'yaml'
};
mockDb.select.mockResolvedValue([]);
mockDb.insert.mockResolvedValue([1]);
mockDb.first.mockResolvedValue(null);
jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
const manifest = {
backup: { id: 'backup-123' },
version: '2.0'
};
backupManifest.generateManifest.mockResolvedValue(manifest);
mockFs({
'/storage/events/active': {},
'/storage/temp': {}
});
await backupService.runBackup();
// Verify manifest was uploaded to S3
const uploadCalls = mockS3Client.upload.mock.calls;
const manifestUploaded = uploadCalls.some(call =>
call[1].includes('manifests/backup-manifest-backup-123.yaml')
);
expect(manifestUploaded).toBe(true);
});
});
describe('Backward Compatibility', () => {
it('should support local backup destination', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 'local',
backup_destination_path: '/backup/local'
};
mockDb.select.mockResolvedValue([]);
mockDb.insert.mockResolvedValue([1]);
mockDb.first.mockResolvedValue(null);
jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
mockFs({
'/storage/events/active/event1': {
'photo1.jpg': Buffer.from('photo1 content')
},
'/backup/local': {}
});
await backupService.runBackup();
// Verify files were copied to local destination
const fs = require('fs');
const destPath = '/backup/local/events/active/event1/photo1.jpg';
expect(fs.existsSync(destPath)).toBe(true);
});
it('should support rsync backup destination', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 'rsync',
backup_rsync_host: 'backup.example.com',
backup_rsync_user: 'backup',
backup_rsync_path: '/remote/backup'
};
mockDb.select.mockResolvedValue([]);
mockDb.insert.mockResolvedValue([1]);
mockDb.first.mockResolvedValue(null);
jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
// Mock exec for rsync
const { exec } = require('child_process');
const mockExec = jest.fn((cmd, callback) => {
callback(null, { stdout: 'Number of files transferred: 1\nTotal file size: 1024 bytes' });
});
exec.mockImplementation(mockExec);
mockFs({
'/storage/events/active': {}
});
await backupService.runBackup();
expect(mockExec).toHaveBeenCalledWith(
expect.stringContaining('rsync'),
expect.any(Function)
);
});
});
describe('Error Handling and Recovery', () => {
it('should handle file read errors gracefully', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 's3',
backup_s3_bucket: 'test-bucket',
backup_s3_access_key: 'test-key',
backup_s3_secret_key: 'test-secret'
};
mockDb.select.mockResolvedValue([]);
mockDb.insert.mockResolvedValue([1]);
mockDb.first.mockResolvedValue(null);
jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
// Mock file that throws error on read
const fs = require('fs');
const originalCreateReadStream = fs.createReadStream;
fs.createReadStream = jest.fn((path) => {
if (path.includes('error.jpg')) {
const stream = new EventEmitter();
process.nextTick(() => stream.emit('error', new Error('File read error')));
return stream;
}
return originalCreateReadStream(path);
});
mockFs({
'/storage/events/active': {
'error.jpg': Buffer.from('content'),
'good.jpg': Buffer.from('content')
}
});
await backupService.runBackup();
// Should continue with other files despite error
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('Failed to backup file'),
expect.any(Error)
);
fs.createReadStream = originalCreateReadStream;
});
it('should send failure email on backup error', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 's3',
backup_s3_bucket: 'test-bucket',
backup_email_on_failure: true
};
const admins = [
{ email: 'admin1@example.com', is_active: true },
{ email: 'admin2@example.com', is_active: true }
];
mockDb.select.mockResolvedValue([]);
mockDb.insert.mockResolvedValue([1]);
mockDb.where.mockReturnThis();
jest.spyOn(backupService, 'getBackupConfig')
.mockResolvedValueOnce(config)
.mockResolvedValueOnce(config);
// Force an error
jest.spyOn(backupService, 'getFilesToBackup').mockRejectedValue(new Error('Storage error'));
// Mock admin users query
db.mockImplementation((table) => {
if (table === 'admin_users') {
return {
where: jest.fn().mockResolvedValue(admins)
};
}
return mockDb;
});
await backupService.runBackup();
expect(queueEmail).toHaveBeenCalledTimes(2);
expect(queueEmail).toHaveBeenCalledWith(
null,
'admin1@example.com',
'backup_failed',
expect.objectContaining({
error_message: 'Storage error'
})
);
});
it('should handle concurrent backup attempts', async () => {
const config = {
backup_enabled: true,
backup_destination_type: 'local',
backup_destination_path: '/backup'
};
jest.spyOn(backupService, 'getBackupConfig').mockResolvedValue(config);
mockFs({
'/storage/events/active': {},
'/backup': {}
});
// Start two backups concurrently
const backup1 = backupService.runBackup();
const backup2 = backupService.runBackup();
await Promise.all([backup1, backup2]);
// Second backup should be skipped
expect(logger.warn).toHaveBeenCalledWith('Backup already running, skipping');
});
});
describe('Service Lifecycle', () => {
it('should start backup service with cron schedule', async () => {
const config = {
backup_enabled: true,
backup_schedule: '0 3 * * *' // 3 AM daily
};
mockDb.select.mockResolvedValue(
Object.entries(config).map(([key, value]) => ({
setting_key: key,
setting_value: value.toString()
}))
);
await backupService.startBackupService();
expect(cron.schedule).toHaveBeenCalledWith('0 3 * * *', expect.any(Function));
expect(logger.info).toHaveBeenCalledWith('Backup service started with schedule: 0 3 * * *');
});
it('should stop existing job when restarting service', async () => {
const config = {
backup_enabled: true,
backup_schedule: '0 2 * * *'
};
mockDb.select.mockResolvedValue(
Object.entries(config).map(([key, value]) => ({
setting_key: key,
setting_value: value.toString()
}))
);
// Start service twice
await backupService.startBackupService();
await backupService.startBackupService();
expect(mockCronJob.stop).toHaveBeenCalled();
});
it('should not start service when backup is disabled', async () => {
const config = {
backup_enabled: false
};
mockDb.select.mockResolvedValue([
{ setting_key: 'backup_enabled', setting_value: 'false' }
]);
await backupService.startBackupService();
expect(cron.schedule).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith('Backup service is disabled');
});
});
describe('Backup Status and History', () => {
it('should return backup status with recent runs', async () => {
const recentRuns = [
{
id: 1,
started_at: new Date(),
completed_at: new Date(),
status: 'completed',
files_backed_up: 100,
total_size_bytes: 1024000,
manifest_path: '/backup/manifest.json'
}
];
mockDb.limit.mockResolvedValue(recentRuns);
backupManifest.validateManifest.mockImplementation(() => true);
const status = await backupService.getBackupStatus();
expect(status).toEqual({
isRunning: false,
isHealthy: true,
lastRun: expect.objectContaining({
...recentRuns[0],
manifestValid: true
}),
recentRuns: recentRuns,
nextScheduledRun: expect.any(String)
});
});
it('should clean up old backup runs', async () => {
mockDb.delete.mockResolvedValue(5);
await backupService.cleanupOldBackupRuns(30);
expect(mockDb.where).toHaveBeenCalledWith('started_at', '<', expect.any(Date));
expect(mockDb.delete).toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith('Cleaned up 5 old backup runs');
});
});
describe('getBackupManifest', () => {
it('should retrieve manifest from local filesystem', async () => {
const backupRun = {
id: 1,
manifest_path: '/backup/manifests/backup-123.json'
};
mockDb.first.mockResolvedValue(backupRun);
const manifest = { backup: { id: 'backup-123' } };
backupManifest.loadManifest.mockResolvedValue(manifest);
backupManifest.generateSummaryReport.mockReturnValue('Summary');
const result = await backupService.getBackupManifest(1);
expect(result).toEqual({
manifest: manifest,
summary: 'Summary'
});
});
it('should retrieve manifest from S3', async () => {
const backupRun = {
id: 1,
manifest_path: 's3://test-bucket/backups/manifests/backup-123.json'
};
mockDb.first.mockResolvedValue(backupRun);
mockDb.select.mockResolvedValue([
{ setting_key: 'backup_s3_access_key', setting_value: '"test-key"' },
{ setting_key: 'backup_s3_secret_key', setting_value: '"test-secret"' }
]);
const manifest = { backup: { id: 'backup-123' } };
backupManifest.loadManifest.mockResolvedValue(manifest);
await backupService.getBackupManifest(1);
expect(S3StorageAdapter).toHaveBeenCalled();
expect(mockS3Client.download).toHaveBeenCalledWith(
'backups/manifests/backup-123.json',
expect.any(String)
);
});
});
});
+22
View File
@@ -0,0 +1,22 @@
{
"auditReportVersion": 2,
"vulnerabilities": {},
"metadata": {
"vulnerabilities": {
"info": 0,
"low": 0,
"moderate": 0,
"high": 0,
"critical": 0,
"total": 0
},
"dependencies": {
"prod": 329,
"dev": 307,
"optional": 54,
"peer": 1,
"peerOptional": 0,
"total": 690
}
}
}
View File
Binary file not shown.
View File
View File
File diff suppressed because it is too large Load Diff
+161
View File
@@ -0,0 +1,161 @@
# Security Logging Documentation
## Overview
This document describes the comprehensive security logging implemented in the PicPeak application to track authentication failures, rate limiting, and suspicious activities.
## Log Files
### 1. **security.log**
- Location: `logs/security.log`
- Contains: All security-related events (authentication, rate limiting, suspicious activity)
- Max Size: 20MB with rotation (keeps 10 files)
- Format: JSON with timestamp
### 2. **error.log**
- Location: `logs/error.log`
- Contains: All error-level logs including auth failures
- Max Size: 10MB with rotation (keeps 5 files)
### 3. **combined.log**
- Location: `logs/combined.log`
- Contains: All logs (info, warn, error)
- Max Size: 50MB with rotation (keeps 10 files)
## Security Events Logged
### Rate Limiting
When rate limits are exceeded, the following is logged:
```json
{
"timestamp": "2024-01-18 14:23:45.123",
"level": "warn",
"message": "Rate limit exceeded",
"security": true,
"ip": "192.168.1.1",
"path": "/api/admin/login",
"method": "POST",
"authenticated": false,
"userAgent": "Mozilla/5.0...",
"referer": "https://app.example.com",
"origin": "https://app.example.com",
"headers": {
"x-forwarded-for": "192.168.1.1",
"x-real-ip": "192.168.1.1"
},
"requestUrl": "/api/admin/login",
"rateLimitInfo": {
"limit": 5,
"current": 6,
"remaining": 0,
"resetTime": "2024-01-18T14:38:45.123Z"
}
}
```
### Authentication Failures
#### Admin Login Failures
- Tracked in `login_attempts` table
- Logged with: IP address, username, user agent, timestamp
- Account lockout after 5 failures in 15 minutes
#### Gallery Password Failures
- Tracked in `access_logs` table with action='login_fail'
- Logged with: event_id, IP address, user agent
- Gallery lockout after 5 failures in 15 minutes
### JWT Validation Failures
```json
{
"timestamp": "2024-01-18 14:23:45.123",
"level": "warn",
"message": "JWT validation failed",
"ip": "192.168.1.1",
"path": "/api/admin/events",
"method": "GET",
"userAgent": "Mozilla/5.0...",
"error": "TokenExpiredError",
"message": "jwt expired"
}
```
### Suspicious Activity
- Multiple IPs attempting login for same account
- Token usage from different IP than issued
- Token usage after password change
- Revoked token usage attempts
## Configuration Settings
All rate limiting settings are configurable via the admin panel:
| Setting | Default | Range | Description |
|---------|---------|-------|-------------|
| rate_limit_enabled | true | - | Enable/disable rate limiting |
| rate_limit_window_minutes | 15 | 1-60 | Time window for rate limit |
| rate_limit_max_requests | 1000 | 10-10000 | Max requests for general endpoints |
| rate_limit_auth_max_requests | 5 | 1-100 | Max requests for auth endpoints |
| rate_limit_skip_authenticated | true | - | Skip rate limit for authenticated requests |
| rate_limit_public_endpoints_only | false | - | Only rate limit public endpoints |
## Database Tables
### login_attempts
```sql
- id
- username
- ip_address
- user_agent
- success (boolean)
- created_at
```
### access_logs
```sql
- id
- event_id
- ip_address
- user_agent
- action ('view', 'download', 'login_success', 'login_fail')
- photo_id (nullable)
- created_at
```
## Environment Variables
- `LOG_LEVEL`: Set logging level (default: 'info')
- `LOG_TO_CONSOLE`: Enable console logging in production (default: false)
## Monitoring Recommendations
1. **Set up alerts for:**
- Rate limit exceeded events (possible DDoS)
- Multiple failed login attempts from same IP
- Account lockout events
- JWT validation failures spike
2. **Regular review:**
- Check security.log for patterns
- Review login_attempts table for brute force attempts
- Monitor access_logs for suspicious gallery access patterns
3. **Log analysis tools:**
- Use log aggregation tools (ELK stack, Splunk)
- Set up dashboards for security metrics
- Configure alerts for threshold breaches
## Production Deployment Notes
1. Ensure logs directory has proper permissions
2. Set up log rotation outside of application if needed
3. Consider shipping logs to centralized logging service
4. Monitor disk space for log files
5. Set `LOG_TO_CONSOLE=true` for container deployments
## Security Best Practices
1. Never log sensitive data (passwords, tokens)
2. Use generic error messages to prevent user enumeration
3. Clean up old login attempts regularly (7 days retention)
4. Monitor for unusual patterns in real-time
5. Keep rate limit settings appropriate for your usage
+50
View File
@@ -0,0 +1,50 @@
#!/bin/sh
# init-production.sh - Production initialization script
set -e
echo "🚀 Initializing PicPeak Production Environment..."
# Wait for services to be ready
echo "⏳ Waiting for database to be fully ready..."
sleep 3
# Fix permissions if running as root (shouldn't happen with proper Dockerfile)
if [ "$(id -u)" = "0" ]; then
echo "🔧 Fixing file permissions..."
chown -R nodejs:nodejs /app/storage /app/data /app/logs 2>/dev/null || true
fi
# Create required directories
echo "📁 Creating required directories..."
mkdir -p /app/storage/events/active \
/app/storage/events/archived \
/app/storage/thumbnails \
/app/storage/uploads/logos \
/app/storage/uploads/favicons \
/app/data \
/app/logs
# Run migrations with safe runner
echo "🗄️ Running database migrations (safe mode)..."
NODE_ENV=production npm run migrate:safe
# Create admin user if environment variables are set
if [ -n "$ADMIN_EMAIL" ] && [ -n "$ADMIN_PASSWORD" ]; then
echo "👤 Creating admin user..."
node scripts/create-admin.js \
--email "$ADMIN_EMAIL" \
--username "${ADMIN_USERNAME:-admin}" \
--password "$ADMIN_PASSWORD" || echo "Admin user might already exist"
fi
# Initialize email configuration if variables are set
if [ -n "$SMTP_HOST" ]; then
echo "📧 Email configuration detected via environment variables"
fi
echo "✅ Production initialization complete!"
echo "🌐 Starting application server..."
# Start the application
exec node server.js
+59
View File
@@ -0,0 +1,59 @@
require('dotenv').config();
const path = require('path');
// Database configuration for different environments
const config = {
development: {
client: process.env.DATABASE_CLIENT || 'sqlite3',
connection: process.env.DATABASE_CLIENT === 'pg' ? {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_NAME || 'photo_sharing'
} : {
filename: path.join(__dirname, process.env.DATABASE_PATH || './data/photo_sharing.db')
},
useNullAsDefault: process.env.DATABASE_CLIENT !== 'pg',
migrations: {
directory: './migrations'
},
seeds: {
directory: './seeds'
}
},
production: {
client: process.env.DATABASE_CLIENT || 'pg',
connection: {
host: process.env.DB_HOST || 'db',
port: process.env.DB_PORT || 5432,
user: process.env.DB_USER || 'picpeak',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'picpeak',
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
// Connection stability settings
connectionTimeoutMillis: 30000,
idleTimeoutMillis: 30000,
keepAlive: true,
keepAliveInitialDelayMillis: 0
},
pool: {
min: 5,
max: 25,
acquireTimeoutMillis: 60000,
createTimeoutMillis: 60000,
idleTimeoutMillis: 30000,
reapIntervalMillis: 1000,
createRetryIntervalMillis: 200,
propagateCreateError: false
},
migrations: {
directory: './migrations'
},
acquireConnectionTimeout: 60000
}
};
module.exports = config[process.env.NODE_ENV || 'development'];
@@ -0,0 +1,23 @@
exports.up = async function(knex) {
// Check if created_at column already exists
const hasCreatedAt = await knex.schema.hasColumn('email_queue', 'created_at');
if (!hasCreatedAt) {
await knex.schema.table('email_queue', (table) => {
table.datetime('created_at').defaultTo(knex.fn.now());
});
// Update existing rows to have a created_at value based on scheduled_at
await knex('email_queue')
.whereNull('created_at')
.update({
created_at: knex.ref('scheduled_at')
});
}
};
exports.down = async function(knex) {
await knex.schema.table('email_queue', (table) => {
table.dropColumn('created_at');
});
};
@@ -0,0 +1,35 @@
exports.up = async function(knex) {
// Check current column structure
const hasSubjectEn = await knex.schema.hasColumn('email_templates', 'subject_en');
const hasSubject = await knex.schema.hasColumn('email_templates', 'subject');
if (hasSubjectEn && !hasSubject) {
// The language migration was applied, need to add back basic columns
await knex.schema.alterTable('email_templates', function(table) {
table.string('subject');
table.text('body_html');
table.text('body_text');
});
// Copy English values to the basic columns
await knex('email_templates').update({
subject: knex.raw('subject_en'),
body_html: knex.raw('body_html_en'),
body_text: knex.raw('body_text_en')
});
}
};
exports.down = async function(knex) {
// Check if we have the basic columns
const hasSubject = await knex.schema.hasColumn('email_templates', 'subject');
const hasSubjectEn = await knex.schema.hasColumn('email_templates', 'subject_en');
if (hasSubject && hasSubjectEn) {
await knex.schema.alterTable('email_templates', function(table) {
table.dropColumn('subject');
table.dropColumn('body_html');
table.dropColumn('body_text');
});
}
};
@@ -0,0 +1,91 @@
exports.up = async function(knex) {
// Check if we have the default email templates
const templates = await knex('email_templates').select('template_key');
const existingKeys = templates.map(t => t.template_key);
// Check which columns exist in the table
const hasSubjectEn = await knex.schema.hasColumn('email_templates', 'subject_en');
const hasSubject = await knex.schema.hasColumn('email_templates', 'subject');
// Determine which columns to use based on schema
const subjectCol = hasSubjectEn ? 'subject_en' : 'subject';
const bodyHtmlCol = hasSubjectEn ? 'body_html_en' : 'body_html';
const bodyTextCol = hasSubjectEn ? 'body_text_en' : 'body_text';
const defaultTemplates = [
{
template_key: 'gallery_created',
[subjectCol]: 'Your Photo Gallery is Ready!',
[bodyHtmlCol]: `<h2>Gallery Created Successfully</h2>
<p>Dear {{host_name}},</p>
<p>Your photo gallery "{{event_name}}" has been created successfully!</p>
<p><strong>Gallery Details:</strong></p>
<ul>
<li>Event Date: {{event_date}}</li>
<li>Gallery Link: {{gallery_link}}</li>
<li>Password: {{gallery_password}}</li>
<li>Expires: {{expiry_date}}</li>
</ul>
<p>Share this link and password with your guests to allow them to view and download photos.</p>`,
[bodyTextCol]: 'Gallery Created Successfully\n\nDear {{host_name}},\n\nYour photo gallery "{{event_name}}" has been created successfully!',
variables: JSON.stringify(['host_name', 'event_name', 'event_date', 'gallery_link', 'gallery_password', 'expiry_date'])
},
{
template_key: 'expiration_warning',
[subjectCol]: 'Your Photo Gallery Expires Soon',
[bodyHtmlCol]: `<h2>Gallery Expiring Soon</h2>
<p>Dear {{host_name}},</p>
<p>Your photo gallery "{{event_name}}" will expire in {{days_remaining}} days.</p>
<p>After expiration, the gallery will be archived and no longer accessible to guests.</p>
<p><a href="{{gallery_link}}">Visit Gallery</a></p>`,
[bodyTextCol]: 'Gallery Expiring Soon\n\nDear {{host_name}},\n\nYour photo gallery "{{event_name}}" will expire in {{days_remaining}} days.',
variables: JSON.stringify(['host_name', 'event_name', 'days_remaining', 'gallery_link'])
},
{
template_key: 'gallery_expired',
[subjectCol]: 'Your Photo Gallery Has Expired',
[bodyHtmlCol]: `<h2>Gallery Expired</h2>
<p>Dear {{host_name}},</p>
<p>Your photo gallery "{{event_name}}" has expired and been archived.</p>
<p>The photos are safely stored in our archive system. If you need access to the archived photos, please contact support.</p>`,
[bodyTextCol]: 'Gallery Expired\n\nDear {{host_name}},\n\nYour photo gallery "{{event_name}}" has expired and been archived.',
variables: JSON.stringify(['host_name', 'event_name'])
},
{
template_key: 'archive_complete',
[subjectCol]: 'Gallery Archive Complete',
[bodyHtmlCol]: `<h2>Archive Complete</h2>
<p>Dear {{host_name}},</p>
<p>Your photo gallery "{{event_name}}" has been successfully archived.</p>
<p>Archive size: {{archive_size}}</p>
<p>The archive is stored securely and can be retrieved if needed.</p>`,
[bodyTextCol]: 'Archive Complete\n\nDear {{host_name}},\n\nYour photo gallery "{{event_name}}" has been successfully archived.',
variables: JSON.stringify(['host_name', 'event_name', 'archive_size'])
}
];
// Insert missing templates
for (const template of defaultTemplates) {
if (!existingKeys.includes(template.template_key)) {
// If we have language columns, also set German versions with same content
if (hasSubjectEn) {
template.subject_de = template[subjectCol];
template.body_html_de = template[bodyHtmlCol];
template.body_text_de = template[bodyTextCol];
// Also ensure we have the basic columns if they exist
if (hasSubject) {
template.subject = template[subjectCol];
template.body_html = template[bodyHtmlCol];
template.body_text = template[bodyTextCol];
}
}
await knex('email_templates').insert(template);
}
}
};
exports.down = async function(knex) {
// Don't remove templates on rollback as they might have been customized
};
@@ -0,0 +1,121 @@
exports.up = async function(knex) {
// Check if CMS pages already exist
const impressumExists = await knex('cms_pages')
.where('slug', 'impressum')
.first();
const datenschutzExists = await knex('cms_pages')
.where('slug', 'datenschutz')
.first();
const pagesToInsert = [];
// Add Impressum page if it doesn't exist
if (!impressumExists) {
pagesToInsert.push({
slug: 'impressum',
title_en: 'Legal Notice',
title_de: 'Impressum',
content_en: `<h1>Legal Notice</h1>
<p>Information according to § 5 TMG</p>
<h2>Responsible for content</h2>
<p>[Your Name]<br>
[Your Address]<br>
[Postal Code City]</p>
<h2>Contact</h2>
<p>Email: [Your Email Address]<br>
Phone: [Your Phone Number]</p>
<h2>Disclaimer</h2>
<h3>Liability for content</h3>
<p>The contents of our pages were created with great care. However, we cannot guarantee the accuracy, completeness and timeliness of the content.</p>
<h3>Liability for links</h3>
<p>Our website contains links to external third-party websites over whose content we have no influence. Therefore, we cannot accept any liability for this third-party content.</p>`,
content_de: `<h1>Impressum</h1>
<p>Angaben gemäß § 5 TMG</p>
<h2>Verantwortlich für den Inhalt</h2>
<p>[Ihr Name]<br>
[Ihre Adresse]<br>
[PLZ Ort]</p>
<h2>Kontakt</h2>
<p>E-Mail: [Ihre E-Mail-Adresse]<br>
Telefon: [Ihre Telefonnummer]</p>
<h2>Haftungsausschluss</h2>
<h3>Haftung für Inhalte</h3>
<p>Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen.</p>
<h3>Haftung für Links</h3>
<p>Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen.</p>`,
updated_at: new Date()
});
}
// Add Datenschutz page if it doesn't exist
if (!datenschutzExists) {
pagesToInsert.push({
slug: 'datenschutz',
title_en: 'Privacy Policy',
title_de: 'Datenschutzerklärung',
content_en: `<h1>Privacy Policy</h1>
<h2>1. Privacy at a Glance</h2>
<h3>General Information</h3>
<p>The following information provides a simple overview of what happens to your personal data when you visit this website.</p>
<h3>Data Collection on This Website</h3>
<p><strong>Who is responsible for data collection on this website?</strong></p>
<p>Data processing on this website is carried out by the website operator. Their contact details can be found in the legal notice of this website.</p>
<p><strong>How do we collect your data?</strong></p>
<p>Your data is collected when you provide it to us. This could be data that you enter into a contact form, for example.</p>
<p><strong>What do we use your data for?</strong></p>
<p>Some of the data is collected to ensure error-free provision of the website. Other data may be used to analyze your user behavior.</p>
<h2>2. Hosting</h2>
<p>This website is hosted externally. The personal data collected on this website is stored on the servers of the host.</p>
<h2>3. General Information and Mandatory Information</h2>
<h3>Data Protection</h3>
<p>The operators of these pages take the protection of your personal data very seriously. We treat your personal data confidentially and in accordance with the statutory data protection regulations and this privacy policy.</p>`,
content_de: `<h1>Datenschutzerklärung</h1>
<h2>1. Datenschutz auf einen Blick</h2>
<h3>Allgemeine Hinweise</h3>
<p>Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen.</p>
<h3>Datenerfassung auf dieser Website</h3>
<p><strong>Wer ist verantwortlich für die Datenerfassung auf dieser Website?</strong></p>
<p>Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten können Sie dem Impressum dieser Website entnehmen.</p>
<p><strong>Wie erfassen wir Ihre Daten?</strong></p>
<p>Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z.B. um Daten handeln, die Sie in ein Kontaktformular eingeben.</p>
<p><strong>Wofür nutzen wir Ihre Daten?</strong></p>
<p>Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung der Website zu gewährleisten. Andere Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden.</p>
<h2>2. Hosting</h2>
<p>Diese Website wird extern gehostet. Die personenbezogenen Daten, die auf dieser Website erfasst werden, werden auf den Servern des Hosters gespeichert.</p>
<h2>3. Allgemeine Hinweise und Pflichtinformationen</h2>
<h3>Datenschutz</h3>
<p>Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.</p>`,
updated_at: new Date()
});
}
// Insert pages if any need to be added
if (pagesToInsert.length > 0) {
await knex('cms_pages').insert(pagesToInsert);
}
};
exports.down = async function(knex) {
// Don't remove CMS pages on rollback as they might have been customized
};
@@ -0,0 +1,68 @@
exports.up = async function(knex) {
console.log('Fixing JSON columns in database...');
// Fix email_templates variables column
const templates = await knex('email_templates').select('id', 'template_key', 'variables');
for (const template of templates) {
if (template.variables && typeof template.variables === 'string') {
try {
// Check if it's already valid JSON
JSON.parse(template.variables);
} catch (e) {
console.log(`Fixing invalid JSON in email template ${template.template_key}`);
// Attempt to fix common issues
let fixed = template.variables;
// If it looks like an array but isn't valid JSON, try to fix it
if (fixed.startsWith('[') && fixed.endsWith(']')) {
// Extract the content and properly format it
const content = fixed.slice(1, -1);
const items = content.split(',').map(item => item.trim().replace(/['"]/g, ''));
fixed = JSON.stringify(items);
} else {
// Default to empty array if we can't fix it
fixed = JSON.stringify([]);
}
await knex('email_templates')
.where('id', template.id)
.update({ variables: fixed });
}
} else if (!template.variables) {
// Set default empty array for null values
await knex('email_templates')
.where('id', template.id)
.update({ variables: JSON.stringify([]) });
}
}
// Fix activity_logs metadata column
const activities = await knex('activity_logs').select('id', 'metadata');
for (const activity of activities) {
if (activity.metadata && typeof activity.metadata === 'string') {
try {
// Check if it's already valid JSON
JSON.parse(activity.metadata);
} catch (e) {
console.log(`Fixing invalid JSON in activity log ${activity.id}`);
// Default to empty object if we can't parse it
await knex('activity_logs')
.where('id', activity.id)
.update({ metadata: JSON.stringify({}) });
}
} else if (!activity.metadata) {
// Set default empty object for null values
await knex('activity_logs')
.where('id', activity.id)
.update({ metadata: JSON.stringify({}) });
}
}
console.log('JSON columns fixed successfully');
};
exports.down = async function(knex) {
// No rollback needed - data fixes only
};
@@ -0,0 +1,22 @@
/**
* Ensure PostgreSQL compatibility for all insert operations
* This migration doesn't change the schema but ensures all tables
* are compatible with .returning() syntax
*/
exports.up = async function(knex) {
// This migration is informational only
// All insert operations should use .returning('id') going forward
console.log('PostgreSQL compatibility check:');
console.log('- All INSERT operations should use .returning("id")');
console.log('- All date operations should use ISO strings');
console.log('- Boolean values are handled automatically by Knex');
return Promise.resolve();
};
exports.down = async function(knex) {
// No rollback needed
return Promise.resolve();
};
@@ -0,0 +1,29 @@
/**
* Fix boolean compatibility issues between PostgreSQL and SQLite
* This migration updates the database configuration and existing data
*/
exports.up = async function(knex) {
const isPostgres = knex.client.config.client === 'pg';
if (!isPostgres) {
// Enable foreign keys for SQLite
await knex.raw('PRAGMA foreign_keys = ON');
// Note: SQLite stores booleans as 0/1
// No data migration needed as Knex handles this automatically
// But queries must use formatBoolean() helper
console.log('SQLite boolean compatibility check:');
console.log('- SQLite stores booleans as 0/1');
console.log('- All boolean comparisons should use formatBoolean() helper');
console.log('- Foreign keys enabled');
}
return Promise.resolve();
};
exports.down = async function(knex) {
// No rollback needed
return Promise.resolve();
};
@@ -0,0 +1,33 @@
/**
* Fix email_queue table by ensuring it doesn't have updated_at column
* This migration addresses the PostgreSQL error where queries are trying to update
* a non-existent updated_at column
*/
exports.up = async function(knex) {
// First, check if the column exists
const hasUpdatedAt = await knex.schema.hasColumn('email_queue', 'updated_at');
if (hasUpdatedAt) {
console.log('Found updated_at column in email_queue table, removing it...');
await knex.schema.table('email_queue', (table) => {
table.dropColumn('updated_at');
});
}
// Also ensure the table has all required columns
const hasCreatedAt = await knex.schema.hasColumn('email_queue', 'created_at');
if (!hasCreatedAt) {
console.log('Adding missing created_at column to email_queue table...');
await knex.schema.table('email_queue', (table) => {
table.datetime('created_at').defaultTo(knex.fn.now());
});
}
console.log('email_queue table schema fixed');
};
exports.down = async function(knex) {
// In the down migration, we don't add back updated_at since it shouldn't exist
// This is intentionally left minimal
};
@@ -0,0 +1,234 @@
exports.up = async function(knex) {
// Update gallery_created template with proper German translation
await knex('email_templates')
.where('template_key', 'gallery_created')
.update({
subject_de: 'Ihre Fotogalerie ist bereit!',
body_html_de: `<h2>Galerie erfolgreich erstellt</h2>
<p>Liebe(r) {{host_name}},</p>
<p>Ihre Fotogalerie "{{event_name}}" wurde erfolgreich erstellt!</p>
{{#if welcome_message}}
<div style="background-color: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
<p style="margin: 0 0 10px 0; font-weight: 600; color: #374151;">Persönliche Nachricht:</p>
<p style="margin: 0; color: #4b5563;">{{welcome_message}}</p>
</div>
{{/if}}
<p><strong>Galerie-Details:</strong></p>
<ul>
<li>Veranstaltungsdatum: {{event_date}}</li>
<li>Galerie-Link: <a href="{{gallery_link}}" style="color: #5C8762;">{{gallery_link}}</a></li>
<li>Passwort: {{gallery_password}}</li>
<li>Ablaufdatum: {{expiry_date}}</li>
</ul>
<p>Teilen Sie diesen Link und das Passwort mit Ihren Gästen, damit diese die Fotos ansehen und herunterladen können.</p>
<p style="background-color: #FEF3C7; padding: 15px; border-radius: 5px; border-left: 4px solid #F59E0B;">
<strong>Wichtig:</strong> Diese Galerie läuft am {{expiry_date}} ab. Nach diesem Datum werden die Fotos archiviert und sind nicht mehr zugänglich.
</p>
<a href="{{gallery_link}}" style="display: inline-block; padding: 12px 30px; background-color: #5C8762; color: white; text-decoration: none; border-radius: 5px; font-weight: 500; margin: 20px 0;">Galerie anzeigen</a>
<p>Mit freundlichen Grüßen,<br>Ihr Foto-Sharing-Team</p>`,
body_text_de: `Galerie erfolgreich erstellt
Liebe(r) {{host_name}},
Ihre Fotogalerie "{{event_name}}" wurde erfolgreich erstellt!
{{#if welcome_message}}
Persönliche Nachricht:
{{welcome_message}}
{{/if}}
Galerie-Details:
- Veranstaltungsdatum: {{event_date}}
- Galerie-Link: {{gallery_link}}
- Passwort: {{gallery_password}}
- Ablaufdatum: {{expiry_date}}
Teilen Sie diesen Link und das Passwort mit Ihren Gästen, damit diese die Fotos ansehen und herunterladen können.
WICHTIG: Diese Galerie läuft am {{expiry_date}} ab. Nach diesem Datum werden die Fotos archiviert und sind nicht mehr zugänglich.
Mit freundlichen Grüßen,
Ihr Foto-Sharing-Team`
});
// Update expiration_warning template with proper German translation
await knex('email_templates')
.where('template_key', 'expiration_warning')
.update({
subject_de: 'Ihre Fotogalerie läuft bald ab',
body_html_de: `<h2>Galerie läuft bald ab</h2>
<p>Liebe(r) {{host_name}},</p>
<p>Ihre Fotogalerie "{{event_name}}" läuft in <strong>{{days_remaining}} Tagen</strong> ab.</p>
<p>Nach Ablauf wird die Galerie archiviert und ist für Gäste nicht mehr zugänglich. Bitte stellen Sie sicher, dass alle gewünschten Fotos heruntergeladen wurden.</p>
<p><strong>Ablaufdatum:</strong> {{expiry_date}}</p>
<a href="{{gallery_link}}" style="display: inline-block; padding: 12px 30px; background-color: #5C8762; color: white; text-decoration: none; border-radius: 5px; font-weight: 500; margin: 20px 0;">Galerie jetzt besuchen</a>
<p style="background-color: #FEE2E2; padding: 15px; border-radius: 5px; border-left: 4px solid #EF4444;">
<strong>Erinnerung:</strong> Nach dem {{expiry_date}} können Ihre Gäste nicht mehr auf die Galerie zugreifen.
</p>
<p>Mit freundlichen Grüßen,<br>Ihr Foto-Sharing-Team</p>`,
body_text_de: `Galerie läuft bald ab
Liebe(r) {{host_name}},
Ihre Fotogalerie "{{event_name}}" läuft in {{days_remaining}} Tagen ab.
Nach Ablauf wird die Galerie archiviert und ist für Gäste nicht mehr zugänglich. Bitte stellen Sie sicher, dass alle gewünschten Fotos heruntergeladen wurden.
Ablaufdatum: {{expiry_date}}
Galerie-Link: {{gallery_link}}
ERINNERUNG: Nach dem {{expiry_date}} können Ihre Gäste nicht mehr auf die Galerie zugreifen.
Mit freundlichen Grüßen,
Ihr Foto-Sharing-Team`
});
// Update gallery_expired template with proper German translation
await knex('email_templates')
.where('template_key', 'gallery_expired')
.update({
subject_de: 'Ihre Fotogalerie {{event_name}} ist abgelaufen',
body_html_de: `<h2>Galerie abgelaufen</h2>
<p>Liebe(r) {{host_name}},</p>
<p>Ihre Fotogalerie "{{event_name}}" ist am {{expiry_date}} abgelaufen und wurde archiviert.</p>
<p>Die Galerie ist nicht mehr für Gäste zugänglich. Alle Fotos wurden sicher in unserem Archivsystem gespeichert.</p>
<p>Wenn Sie wieder Zugriff auf die archivierten Fotos benötigen, wenden Sie sich bitte an unseren Support:</p>
<p style="background-color: #F3F4F6; padding: 15px; border-radius: 5px;">
<strong>Kontakt:</strong><br>
E-Mail: <a href="mailto:{{admin_email}}" style="color: #5C8762;">{{admin_email}}</a><br>
{{#if support_phone}}Telefon: {{support_phone}}{{/if}}
</p>
<p>Vielen Dank für die Nutzung unseres Foto-Sharing-Services!</p>
<p>Mit freundlichen Grüßen,<br>Ihr Foto-Sharing-Team</p>`,
body_text_de: `Galerie abgelaufen
Liebe(r) {{host_name}},
Ihre Fotogalerie "{{event_name}}" ist am {{expiry_date}} abgelaufen und wurde archiviert.
Die Galerie ist nicht mehr für Gäste zugänglich. Alle Fotos wurden sicher in unserem Archivsystem gespeichert.
Wenn Sie wieder Zugriff auf die archivierten Fotos benötigen, wenden Sie sich bitte an unseren Support:
E-Mail: {{admin_email}}
{{#if support_phone}}Telefon: {{support_phone}}{{/if}}
Vielen Dank für die Nutzung unseres Foto-Sharing-Services!
Mit freundlichen Grüßen,
Ihr Foto-Sharing-Team`
});
// Update archive_complete template with proper German translation
await knex('email_templates')
.where('template_key', 'archive_complete')
.update({
subject_de: 'Archivierung abgeschlossen: {{event_name}}',
body_html_de: `<h2>Archivierung abgeschlossen</h2>
<p>Liebe(r) {{host_name}},</p>
<p>Die Fotogalerie "{{event_name}}" wurde erfolgreich archiviert.</p>
<p><strong>Archiv-Details:</strong></p>
<ul>
<li>Archivgröße: {{archive_size}}</li>
<li>Archivierungsdatum: {{archive_date}}</li>
<li>Anzahl der Fotos: {{photo_count}}</li>
</ul>
<p>Das Archiv wird sicher in unserem System aufbewahrt. Bei Bedarf können Sie sich an unseren Support wenden, um Zugriff auf die archivierten Fotos zu erhalten.</p>
<p style="background-color: #F0FDF4; padding: 15px; border-radius: 5px; border-left: 4px solid #22C55E;">
<strong>✓ Erfolgreich archiviert:</strong> Ihre Fotos sind sicher gespeichert und können bei Bedarf wiederhergestellt werden.
</p>
<p>Kontakt für Archivzugriff:<br>
E-Mail: <a href="mailto:{{admin_email}}" style="color: #5C8762;">{{admin_email}}</a></p>
<p>Mit freundlichen Grüßen,<br>Ihr Foto-Sharing-Team</p>`,
body_text_de: `Archivierung abgeschlossen
Liebe(r) {{host_name}},
Die Fotogalerie "{{event_name}}" wurde erfolgreich archiviert.
Archiv-Details:
- Archivgröße: {{archive_size}}
- Archivierungsdatum: {{archive_date}}
- Anzahl der Fotos: {{photo_count}}
Das Archiv wird sicher in unserem System aufbewahrt. Bei Bedarf können Sie sich an unseren Support wenden, um Zugriff auf die archivierten Fotos zu erhalten.
✓ ERFOLGREICH ARCHIVIERT: Ihre Fotos sind sicher gespeichert und können bei Bedarf wiederhergestellt werden.
Kontakt für Archivzugriff:
E-Mail: {{admin_email}}
Mit freundlichen Grüßen,
Ihr Foto-Sharing-Team`
});
// Also update the non-language-specific fields to match German for consistency
await knex('email_templates')
.where('template_key', 'gallery_created')
.update({
subject: knex.raw('subject_de'),
body_html: knex.raw('body_html_de'),
body_text: knex.raw('body_text_de')
});
await knex('email_templates')
.where('template_key', 'expiration_warning')
.update({
subject: knex.raw('subject_de'),
body_html: knex.raw('body_html_de'),
body_text: knex.raw('body_text_de')
});
await knex('email_templates')
.where('template_key', 'gallery_expired')
.update({
subject: knex.raw('subject_de'),
body_html: knex.raw('body_html_de'),
body_text: knex.raw('body_text_de')
});
await knex('email_templates')
.where('template_key', 'archive_complete')
.update({
subject: knex.raw('subject_de'),
body_html: knex.raw('body_html_de'),
body_text: knex.raw('body_text_de')
});
};
exports.down = async function(knex) {
// Revert to previous German translations
// This is a simplified rollback - in production you might want to store the old values
await knex('email_templates')
.where('template_key', 'gallery_created')
.update({
subject: knex.raw('subject_en'),
body_html: knex.raw('body_html_en'),
body_text: knex.raw('body_text_en')
});
await knex('email_templates')
.where('template_key', 'expiration_warning')
.update({
subject: knex.raw('subject_en'),
body_html: knex.raw('body_html_en'),
body_text: knex.raw('body_text_en')
});
await knex('email_templates')
.where('template_key', 'gallery_expired')
.update({
subject: knex.raw('subject_en'),
body_html: knex.raw('body_html_en'),
body_text: knex.raw('body_text_en')
});
await knex('email_templates')
.where('template_key', 'archive_complete')
.update({
subject: knex.raw('subject_en'),
body_html: knex.raw('body_html_en'),
body_text: knex.raw('body_text_en')
});
};
@@ -0,0 +1,41 @@
exports.up = async function(knex) {
// Add language column to events table if it doesn't exist
const hasLanguageInEvents = await knex.schema.hasColumn('events', 'language');
if (!hasLanguageInEvents) {
await knex.schema.alterTable('events', function(table) {
table.string('language', 5).defaultTo('en');
});
}
// Add default_language to email_configs if it doesn't exist
const hasDefaultLanguage = await knex.schema.hasColumn('email_configs', 'default_language');
if (!hasDefaultLanguage) {
await knex.schema.alterTable('email_configs', function(table) {
table.string('default_language', 5).defaultTo('en');
});
}
// Set default language to German for the existing email config
await knex('email_configs')
.update({
default_language: 'de'
});
};
exports.down = async function(knex) {
// Remove language column from events table
const hasLanguageInEvents = await knex.schema.hasColumn('events', 'language');
if (hasLanguageInEvents) {
await knex.schema.alterTable('events', function(table) {
table.dropColumn('language');
});
}
// Remove default_language from email_configs
const hasDefaultLanguage = await knex.schema.hasColumn('email_configs', 'default_language');
if (hasDefaultLanguage) {
await knex.schema.alterTable('email_configs', function(table) {
table.dropColumn('default_language');
});
}
};
@@ -0,0 +1,63 @@
exports.up = async function(knex) {
// Add rate limit settings to app_settings
const rateLimitSettings = [
{
setting_key: 'rate_limit_enabled',
setting_value: JSON.stringify(true),
setting_type: 'security'
},
{
setting_key: 'rate_limit_window_minutes',
setting_value: JSON.stringify(15),
setting_type: 'security'
},
{
setting_key: 'rate_limit_max_requests',
setting_value: JSON.stringify(1000),
setting_type: 'security'
},
{
setting_key: 'rate_limit_auth_max_requests',
setting_value: JSON.stringify(5),
setting_type: 'security'
},
{
setting_key: 'rate_limit_skip_authenticated',
setting_value: JSON.stringify(true),
setting_type: 'security'
},
{
setting_key: 'rate_limit_public_endpoints_only',
setting_value: JSON.stringify(false),
setting_type: 'security'
}
];
// Insert settings if they don't exist
for (const setting of rateLimitSettings) {
const exists = await knex('app_settings')
.where('setting_key', setting.setting_key)
.first();
if (!exists) {
await knex('app_settings').insert({
...setting,
updated_at: knex.fn.now()
});
}
}
};
exports.down = async function(knex) {
// Remove rate limit settings
await knex('app_settings')
.whereIn('setting_key', [
'rate_limit_enabled',
'rate_limit_window_minutes',
'rate_limit_max_requests',
'rate_limit_auth_max_requests',
'rate_limit_skip_authenticated',
'rate_limit_public_endpoints_only'
])
.del();
};
@@ -0,0 +1,307 @@
exports.up = async function(knex) {
// Update English templates to match the quality and content of German templates
// 1. Gallery Created - Match German version with proper styling and conditionals
await knex('email_templates')
.where('template_key', 'gallery_created')
.update({
subject_en: 'Your photo gallery is ready',
body_html_en: `
<h2>Hello {{host_name}},</h2>
<p>Your photo gallery <strong>{{event_name}}</strong> for {{event_date}} has been successfully created and is now online!</p>
{{#if welcome_message}}
<div style="background-color: #f0f8ff; border-left: 4px solid #5C8762; padding: 15px; margin: 20px 0; border-radius: 4px;">
<p style="margin: 0;"><strong>Personal message from your photographer:</strong></p>
<p style="margin: 10px 0 0 0;">{{welcome_message}}</p>
</div>
{{/if}}
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3 style="margin-top: 0;">Your access data:</h3>
<ul style="list-style: none; padding: 0;">
<li style="margin-bottom: 10px;"><strong>Gallery link:</strong> <a href="{{gallery_link}}" style="color: #5C8762;">{{gallery_link}}</a></li>
<li style="margin-bottom: 10px;"><strong>Password:</strong> {{gallery_password}}</li>
</ul>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="{{gallery_link}}" style="display: inline-block; padding: 12px 30px; background-color: #5C8762; color: white; text-decoration: none; border-radius: 5px; font-weight: 500;">View Gallery</a>
</div>
<div style="background-color: #fff3cd; border: 1px solid #ffeaa7; color: #856404; padding: 15px; border-radius: 4px; margin: 20px 0;">
<p style="margin: 0;"><strong>Important:</strong> Your gallery will be available until <strong>{{expiry_date}}</strong>. After this date, the photos will be archived and will only be available upon request.</p>
</div>
<p>We hope you enjoy your photos!</p>
<p>Best regards,<br>
Your Photo Sharing Team</p>`,
body_text_en: `Hello {{host_name}},
Your photo gallery "{{event_name}}" for {{event_date}} has been successfully created and is now online!
{{#if welcome_message}}
Personal message from your photographer:
{{welcome_message}}
{{/if}}
Your access data:
- Gallery link: {{gallery_link}}
- Password: {{gallery_password}}
Important: Your gallery will be available until {{expiry_date}}. After this date, the photos will be archived and will only be available upon request.
We hope you enjoy your photos!
Best regards,
Your Photo Sharing Team`
});
// 2. Expiration Warning - Match German version with urgency and styling
await knex('email_templates')
.where('template_key', 'expiration_warning')
.update({
subject_en: 'Your photo gallery expires soon',
body_html_en: `
<h2>Hello {{host_name}},</h2>
<p>Your photo gallery <strong>{{event_name}}</strong> will expire in <strong style="color: #e74c3c; font-size: 18px;">{{days_remaining}} days</strong>!</p>
<div style="background-color: #fee; border: 1px solid #fcc; color: #c33; padding: 20px; border-radius: 8px; margin: 20px 0;">
<p style="margin: 0; font-weight: bold; font-size: 16px;">⚠️ Important Notice</p>
<p style="margin: 10px 0 0 0;">After {{expiry_date}}, your gallery will no longer be accessible online. The photos will be archived and will only be available upon special request.</p>
</div>
<p><strong>Don't miss out download your photos now!</strong></p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{gallery_link}}" style="display: inline-block; padding: 14px 35px; background-color: #e74c3c; color: white; text-decoration: none; border-radius: 5px; font-weight: 600; font-size: 16px;">Visit Gallery Now</a>
</div>
<div style="background-color: #f9f9f9; padding: 15px; border-radius: 8px; margin: 20px 0;">
<p style="margin: 0;"><strong>Quick reminder of your access data:</strong></p>
<ul style="list-style: none; padding: 0; margin: 10px 0 0 0;">
<li>Gallery link: <a href="{{gallery_link}}" style="color: #5C8762;">{{gallery_link}}</a></li>
<li>Password: {{gallery_password}}</li>
</ul>
</div>
<p>If you have any questions, please don't hesitate to contact us.</p>
<p>Best regards,<br>
Your Photo Sharing Team</p>`,
body_text_en: `Hello {{host_name}},
Your photo gallery "{{event_name}}" will expire in {{days_remaining}} days!
⚠️ Important Notice
After {{expiry_date}}, your gallery will no longer be accessible online. The photos will be archived and will only be available upon special request.
Don't miss out download your photos now!
Quick reminder of your access data:
- Gallery link: {{gallery_link}}
- Password: {{gallery_password}}
If you have any questions, please don't hesitate to contact us.
Best regards,
Your Photo Sharing Team`
});
// 3. Gallery Expired - Match German version with contact information
await knex('email_templates')
.where('template_key', 'gallery_expired')
.update({
subject_en: 'Your photo gallery has expired',
body_html_en: `
<h2>Hello {{host_name}},</h2>
<p>Your photo gallery <strong>{{event_name}}</strong> expired on {{expiry_date}} and is no longer accessible online.</p>
<div style="background-color: #f9f9f9; border-left: 4px solid #5C8762; padding: 20px; margin: 20px 0; border-radius: 4px;">
<h3 style="margin-top: 0;">Your photos are safely archived</h3>
<p>Don't worry your photos have been securely archived and are not lost. If you need access to your photos, please contact us:</p>
<ul style="list-style: none; padding: 0;">
<li style="margin-bottom: 8px;">📧 Email: <a href="mailto:{{support_email}}" style="color: #5C8762;">{{support_email}}</a></li>
{{#if support_phone}}
<li>📞 Phone: {{support_phone}}</li>
{{/if}}
</ul>
</div>
<p>Please have the following information ready when contacting us:</p>
<ul>
<li>Event name: {{event_name}}</li>
<li>Event date: {{event_date}}</li>
<li>Expiry date: {{expiry_date}}</li>
</ul>
<p>We'll be happy to help you access your archived photos.</p>
<p>Best regards,<br>
Your Photo Sharing Team</p>`,
body_text_en: `Hello {{host_name}},
Your photo gallery "{{event_name}}" expired on {{expiry_date}} and is no longer accessible online.
Your photos are safely archived
Don't worry your photos have been securely archived and are not lost. If you need access to your photos, please contact us:
📧 Email: {{support_email}}
{{#if support_phone}}📞 Phone: {{support_phone}}{{/if}}
Please have the following information ready when contacting us:
- Event name: {{event_name}}
- Event date: {{event_date}}
- Expiry date: {{expiry_date}}
We'll be happy to help you access your archived photos.
Best regards,
Your Photo Sharing Team`
});
// 4. Archive Complete - Match German version with success message and details
await knex('email_templates')
.where('template_key', 'archive_complete')
.update({
subject_en: 'Your photo gallery has been successfully archived',
body_html_en: `
<h2>Hello {{host_name}},</h2>
<p>Your photo gallery <strong>{{event_name}}</strong> has been successfully archived.</p>
<div style="background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 20px; border-radius: 8px; margin: 20px 0;">
<p style="margin: 0; font-weight: bold;">✅ Archive successfully created</p>
<p style="margin: 10px 0 0 0;">Your photos are now safely stored in our archive.</p>
</div>
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3 style="margin-top: 0;">Archive details:</h3>
<ul style="list-style: none; padding: 0;">
<li style="margin-bottom: 8px;"><strong>Event:</strong> {{event_name}}</li>
<li style="margin-bottom: 8px;"><strong>Archive date:</strong> {{archive_date}}</li>
<li style="margin-bottom: 8px;"><strong>Number of photos:</strong> {{photo_count}}</li>
<li><strong>Archive size:</strong> {{archive_size}}</li>
</ul>
</div>
<p>If you need access to your archived photos in the future, please contact us at:</p>
<p style="margin-left: 20px;">
📧 <a href="mailto:{{support_email}}" style="color: #5C8762;">{{support_email}}</a><br>
{{#if support_phone}}📞 {{support_phone}}{{/if}}
</p>
<p>Thank you for using our photo sharing service!</p>
<p>Best regards,<br>
Your Photo Sharing Team</p>`,
body_text_en: `Hello {{host_name}},
Your photo gallery "{{event_name}}" has been successfully archived.
✅ Archive successfully created
Your photos are now safely stored in our archive.
Archive details:
- Event: {{event_name}}
- Archive date: {{archive_date}}
- Number of photos: {{photo_count}}
- Archive size: {{archive_size}}
If you need access to your archived photos in the future, please contact us at:
📧 {{support_email}}
{{#if support_phone}}📞 {{support_phone}}{{/if}}
Thank you for using our photo sharing service!
Best regards,
Your Photo Sharing Team`
});
// 5. Test Email - Update to match German style
await knex('email_templates')
.where('template_key', 'test_email')
.update({
subject_en: 'Test Email - Photo Sharing Platform',
body_html_en: `
<h2>Test Email</h2>
<p>This is a test email from your photo sharing platform.</p>
<div style="background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 15px; border-radius: 4px; margin: 20px 0;">
<p style="margin: 0;"><strong>✅ Email configuration successful!</strong></p>
<p style="margin: 10px 0 0 0;">Your email settings have been configured correctly and emails can be sent.</p>
</div>
<div style="background-color: #f9f9f9; padding: 15px; border-radius: 8px; margin: 20px 0;">
<p style="margin: 0;"><strong>Configuration details:</strong></p>
<ul style="margin: 10px 0 0 0;">
<li>Timestamp: {{timestamp}}</li>
<li>Sender: {{from_email}}</li>
</ul>
</div>
<p>Best regards,<br>
Your Photo Sharing Team</p>`,
body_text_en: `Test Email
This is a test email from your photo sharing platform.
✅ Email configuration successful!
Your email settings have been configured correctly and emails can be sent.
Configuration details:
- Timestamp: {{timestamp}}
- Sender: {{from_email}}
Best regards,
Your Photo Sharing Team`
});
};
exports.down = async function(knex) {
// Revert to previous simpler English templates
await knex('email_templates')
.where('template_key', 'gallery_created')
.update({
subject_en: 'Your Photo Gallery is Ready',
body_html_en: '<h2>Hello,</h2><p>Your photo gallery "{{event_name}}" has been created.</p><p><strong>Access Link:</strong> <a href="{{gallery_link}}">{{gallery_link}}</a></p><p><strong>Password:</strong> {{gallery_password}}</p><p>The gallery will be available until {{expiry_date}}.</p>',
body_text_en: 'Your photo gallery "{{event_name}}" has been created. Access Link: {{gallery_link}} Password: {{gallery_password}} The gallery will be available until {{expiry_date}}.'
});
await knex('email_templates')
.where('template_key', 'expiration_warning')
.update({
subject_en: 'Gallery Expires in {{days_remaining}} Days',
body_html_en: '<h2>Reminder</h2><p>Your photo gallery "{{event_name}}" will expire in {{days_remaining}} days.</p><p>Please download your photos before {{expiry_date}}.</p><p><a href="{{gallery_link}}">Access Gallery</a></p>',
body_text_en: 'Your photo gallery "{{event_name}}" will expire in {{days_remaining}} days. Please download your photos before {{expiry_date}}. Access Gallery: {{gallery_link}}'
});
await knex('email_templates')
.where('template_key', 'gallery_expired')
.update({
subject_en: 'Gallery Expired',
body_html_en: '<h2>Gallery Expired</h2><p>Your photo gallery "{{event_name}}" has expired and is no longer accessible.</p><p>If you need access to your photos, please contact support.</p>',
body_text_en: 'Your photo gallery "{{event_name}}" has expired and is no longer accessible. If you need access to your photos, please contact support.'
});
await knex('email_templates')
.where('template_key', 'archive_complete')
.update({
subject_en: 'Gallery Archived',
body_html_en: '<h2>Archive Complete</h2><p>Your gallery "{{event_name}}" has been archived.</p><p>Archive size: {{archive_size}}</p>',
body_text_en: 'Your gallery "{{event_name}}" has been archived. Archive size: {{archive_size}}'
});
await knex('email_templates')
.where('template_key', 'test_email')
.update({
subject_en: 'Test Email',
body_html_en: '<p>This is a test email sent at {{timestamp}}.</p>',
body_text_en: 'This is a test email sent at {{timestamp}}.'
});
};
@@ -0,0 +1,243 @@
const { db } = require('../src/database/db');
async function up() {
console.log('Adding backup service tables and settings...');
// Create backup_runs table to track backup history
const hasBackupRunsTable = await db.schema.hasTable('backup_runs');
if (!hasBackupRunsTable) {
await db.schema.createTable('backup_runs', (table) => {
table.increments('id').primary();
table.datetime('started_at').notNullable();
table.datetime('completed_at');
table.string('status').defaultTo('running'); // running, completed, failed
table.string('backup_type'); // full, incremental
table.integer('files_backed_up').defaultTo(0);
table.bigInteger('total_size_bytes').defaultTo(0);
table.integer('duration_seconds');
table.text('error_message');
table.json('statistics'); // Detailed stats about the backup
table.json('file_checksums'); // Store checksums for change detection
});
}
// Create backup_file_states table to track individual file states
const hasBackupFileStatesTable = await db.schema.hasTable('backup_file_states');
if (!hasBackupFileStatesTable) {
await db.schema.createTable('backup_file_states', (table) => {
table.increments('id').primary();
table.string('file_path').notNullable();
table.string('checksum').notNullable();
table.bigInteger('size_bytes');
table.datetime('last_modified');
table.datetime('last_backed_up');
table.boolean('is_archived').defaultTo(false);
table.index(['file_path'], 'idx_backup_file_path');
table.index(['checksum'], 'idx_backup_checksum');
});
}
// Add backup-related settings to app_settings
const backupSettings = [
{
setting_key: 'backup_enabled',
setting_value: JSON.stringify(false),
setting_type: 'backup'
},
{
setting_key: 'backup_schedule',
setting_value: JSON.stringify('0 2 * * *'), // Default: 2 AM daily
setting_type: 'backup'
},
{
setting_key: 'backup_destination_type',
setting_value: JSON.stringify('local'), // local, rsync, s3
setting_type: 'backup'
},
{
setting_key: 'backup_destination_path',
setting_value: JSON.stringify('/backup/picpeak'),
setting_type: 'backup'
},
{
setting_key: 'backup_rsync_host',
setting_value: JSON.stringify(''),
setting_type: 'backup'
},
{
setting_key: 'backup_rsync_user',
setting_value: JSON.stringify(''),
setting_type: 'backup'
},
{
setting_key: 'backup_rsync_path',
setting_value: JSON.stringify(''),
setting_type: 'backup'
},
{
setting_key: 'backup_rsync_ssh_key',
setting_value: JSON.stringify(''),
setting_type: 'backup'
},
{
setting_key: 'backup_s3_endpoint',
setting_value: JSON.stringify(''),
setting_type: 'backup'
},
{
setting_key: 'backup_s3_bucket',
setting_value: JSON.stringify(''),
setting_type: 'backup'
},
{
setting_key: 'backup_s3_access_key',
setting_value: JSON.stringify(''),
setting_type: 'backup'
},
{
setting_key: 'backup_s3_secret_key',
setting_value: JSON.stringify(''),
setting_type: 'backup'
},
{
setting_key: 'backup_s3_region',
setting_value: JSON.stringify('us-east-1'),
setting_type: 'backup'
},
{
setting_key: 'backup_retention_days',
setting_value: JSON.stringify(30),
setting_type: 'backup'
},
{
setting_key: 'backup_include_archived',
setting_value: JSON.stringify(true),
setting_type: 'backup'
},
{
setting_key: 'backup_compression',
setting_value: JSON.stringify(true),
setting_type: 'backup'
},
{
setting_key: 'backup_email_on_failure',
setting_value: JSON.stringify(true),
setting_type: 'backup'
},
{
setting_key: 'backup_email_on_success',
setting_value: JSON.stringify(false),
setting_type: 'backup'
},
{
setting_key: 'backup_max_file_size_mb',
setting_value: JSON.stringify(5000), // Skip files larger than 5GB
setting_type: 'backup'
},
{
setting_key: 'backup_exclude_patterns',
setting_value: JSON.stringify(['*.tmp', '.DS_Store', 'Thumbs.db']),
setting_type: 'backup'
}
];
// Insert backup settings if they don't exist
for (const setting of backupSettings) {
const exists = await db('app_settings')
.where('setting_key', setting.setting_key)
.first();
if (!exists) {
await db('app_settings').insert(setting);
}
}
// Add backup-related email templates
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>
<p>The scheduled backup has failed and requires immediate attention.</p>
<p><strong>Error Details:</strong></p>
<ul>
<li>Start Time: {{start_time}}</li>
<li>Backup Type: {{backup_type}}</li>
<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.',
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>
<p>The scheduled backup has been completed successfully.</p>
<p><strong>Backup Summary:</strong></p>
<ul>
<li>Start Time: {{start_time}}</li>
<li>Duration: {{duration}}</li>
<li>Files Backed Up: {{files_count}}</li>
<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}}',
variables: JSON.stringify(['start_time', 'duration', 'files_count', 'total_size', 'backup_type'])
}
];
// Insert backup email templates if they don't exist
for (const template of backupEmailTemplates) {
const exists = await db('email_templates')
.where('template_key', template.template_key)
.first();
if (!exists) {
await db('email_templates').insert(template);
}
}
console.log('Backup service tables and settings added successfully');
}
async function down() {
// Remove backup tables
await db.schema.dropTableIfExists('backup_file_states');
await db.schema.dropTableIfExists('backup_runs');
// Remove backup settings
await db('app_settings')
.where('setting_type', 'backup')
.delete();
// Remove backup email templates
await db('email_templates')
.whereIn('template_key', ['backup_failed', 'backup_completed'])
.delete();
}
module.exports = { up, down };
@@ -0,0 +1,182 @@
const { db } = require('../src/database/db');
async function up() {
console.log('Adding database backup tables and settings...');
// Create database_backup_runs table to track database backup history
const hasDatabaseBackupRunsTable = await db.schema.hasTable('database_backup_runs');
if (!hasDatabaseBackupRunsTable) {
await db.schema.createTable('database_backup_runs', (table) => {
table.increments('id').primary();
table.datetime('started_at').notNullable();
table.datetime('completed_at');
table.string('status').defaultTo('running'); // running, completed, failed
table.string('backup_type'); // sqlite, postgresql
table.string('destination_path');
table.string('file_path');
table.bigInteger('file_size_bytes').defaultTo(0);
table.bigInteger('original_size_bytes').defaultTo(0);
table.integer('duration_seconds');
table.string('checksum'); // SHA256 checksum of backup file
table.float('compression_ratio'); // Compression percentage
table.json('table_checksums'); // Individual table checksums
table.text('error_message');
table.json('statistics'); // Detailed stats about the backup
table.index(['started_at'], 'idx_db_backup_started');
table.index(['status'], 'idx_db_backup_status');
});
}
// Add database backup-related settings to app_settings
const databaseBackupSettings = [
{
setting_key: 'database_backup_enabled',
setting_value: JSON.stringify(false),
setting_type: 'database_backup'
},
{
setting_key: 'database_backup_schedule',
setting_value: JSON.stringify('0 3 * * *'), // Default: 3 AM daily
setting_type: 'database_backup'
},
{
setting_key: 'database_backup_destination_path',
setting_value: JSON.stringify('/backup/database'),
setting_type: 'database_backup'
},
{
setting_key: 'database_backup_compress',
setting_value: JSON.stringify(true),
setting_type: 'database_backup'
},
{
setting_key: 'database_backup_validate_integrity',
setting_value: JSON.stringify(true),
setting_type: 'database_backup'
},
{
setting_key: 'database_backup_include_checksums',
setting_value: JSON.stringify(true),
setting_type: 'database_backup'
},
{
setting_key: 'database_backup_retention_days',
setting_value: JSON.stringify(30),
setting_type: 'database_backup'
},
{
setting_key: 'database_backup_email_on_failure',
setting_value: JSON.stringify(true),
setting_type: 'database_backup'
},
{
setting_key: 'database_backup_email_on_success',
setting_value: JSON.stringify(false),
setting_type: 'database_backup'
},
{
setting_key: 'database_backup_max_retries',
setting_value: JSON.stringify(3),
setting_type: 'database_backup'
}
];
// Insert database backup settings if they don't exist
for (const setting of databaseBackupSettings) {
const exists = await db('app_settings')
.where('setting_key', setting.setting_key)
.first();
if (!exists) {
await db('app_settings').insert(setting);
}
}
// Add database backup-related email templates
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>
<p>The scheduled database backup has failed and requires immediate attention.</p>
<p><strong>Error Details:</strong></p>
<ul>
<li>Backup Type: {{backup_type}}</li>
<li>Timestamp: {{timestamp}}</li>
<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.',
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>
<p>The scheduled database backup has been completed successfully.</p>
<p><strong>Backup Summary:</strong></p>
<ul>
<li>Backup Type: {{backup_type}}</li>
<li>Duration: {{duration}}</li>
<li>File Size: {{file_size}}</li>
<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}}',
variables: JSON.stringify(['backup_type', 'duration', 'file_size', 'compression_ratio', 'file_path'])
}
];
// Insert database backup email templates if they don't exist
for (const template of databaseBackupEmailTemplates) {
const exists = await db('email_templates')
.where('template_key', template.template_key)
.first();
if (!exists) {
await db('email_templates').insert(template);
}
}
console.log('Database backup tables and settings added successfully');
}
async function down() {
// Remove database backup tables
await db.schema.dropTableIfExists('database_backup_runs');
// Remove database backup settings
await db('app_settings')
.where('setting_type', 'database_backup')
.delete();
// Remove database backup email templates
await db('email_templates')
.whereIn('template_key', ['database_backup_failed', 'database_backup_completed'])
.delete();
}
module.exports = { up, down };
@@ -0,0 +1,86 @@
const { db } = require('../src/database/db');
const logger = require('../src/utils/logger');
async function up() {
console.log('Adding backup manifest columns...');
// Add manifest columns to backup_runs table
const hasManifestPath = await db.schema.hasColumn('backup_runs', 'manifest_path');
if (!hasManifestPath) {
await db.schema.alterTable('backup_runs', (table) => {
table.string('manifest_path'); // Path to the manifest file
table.string('manifest_id'); // Unique manifest ID
table.string('manifest_format').defaultTo('json'); // json or yaml
});
}
// Add backup manifest-related settings to app_settings
const manifestSettings = [
{
setting_key: 'backup_manifest_enabled',
setting_value: JSON.stringify(true),
setting_type: 'backup'
},
{
setting_key: 'backup_manifest_format',
setting_value: JSON.stringify('json'), // json or yaml
setting_type: 'backup'
},
{
setting_key: 'backup_manifest_path',
setting_value: JSON.stringify('/backup/manifests'),
setting_type: 'backup'
},
{
setting_key: 'backup_manifest_validate',
setting_value: JSON.stringify(true),
setting_type: 'backup'
},
{
setting_key: 'backup_manifest_include_checksums',
setting_value: JSON.stringify(true),
setting_type: 'backup'
}
];
// Insert manifest settings if they don't exist
for (const setting of manifestSettings) {
const exists = await db('app_settings')
.where('setting_key', setting.setting_key)
.first();
if (!exists) {
await db('app_settings').insert(setting);
}
}
console.log('✓ Backup manifest columns and settings added');
}
async function down() {
// Remove manifest columns from backup_runs table
const hasManifestPath = await db.schema.hasColumn('backup_runs', 'manifest_path');
if (hasManifestPath) {
await db.schema.alterTable('backup_runs', (table) => {
table.dropColumn('manifest_path');
table.dropColumn('manifest_id');
table.dropColumn('manifest_format');
});
}
// Remove manifest settings
await db('app_settings')
.where('setting_type', 'backup')
.whereIn('setting_key', [
'backup_manifest_enabled',
'backup_manifest_format',
'backup_manifest_path',
'backup_manifest_validate',
'backup_manifest_include_checksums'
])
.delete();
console.log('✓ Backup manifest columns and settings removed');
}
module.exports = { up, down };
@@ -0,0 +1,249 @@
// No helpers needed for this migration
/**
* Add restore_runs table for tracking restore operations
*/
exports.up = async function(knex) {
// Create restore_runs table
await knex.schema.createTable('restore_runs', table => {
table.increments('id').primary();
// Timing
table.timestamp('started_at').notNullable().defaultTo(knex.fn.now());
table.timestamp('completed_at');
table.integer('duration_seconds');
// Status and type
table.string('status', 50).notNullable().defaultTo('running');
table.string('restore_type', 50).notNullable(); // full, database, files, selective
// Source information
table.string('source', 500).notNullable(); // Backup source path or S3 URL
table.string('manifest_path', 500); // Path to manifest file
// Results
table.text('error_message');
table.text('statistics'); // JSON object with detailed statistics
table.text('restore_log'); // JSON array of log entries
// Safety backup
table.string('pre_restore_backup_path', 500); // Path to pre-restore safety backup
// Flags
table.boolean('is_dry_run').defaultTo(false);
table.boolean('was_rollback_attempted').defaultTo(false);
table.boolean('was_successful').defaultTo(false);
// Operator information
table.string('operator_type', 50).defaultTo('manual'); // manual, scheduled, api
table.integer('operator_user_id').references('id').inTable('admin_users').onDelete('SET NULL');
table.string('operator_ip', 50);
// Metadata
table.text('metadata'); // JSON object for additional data
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 => {
table.increments('id').primary();
table.integer('restore_run_id').notNullable()
.references('id').inTable('restore_runs').onDelete('CASCADE');
table.string('file_path', 500).notNullable();
table.string('operation', 50).notNullable(); // restore, skip, error
table.string('status', 50).notNullable(); // pending, in_progress, completed, failed
table.bigInteger('file_size');
table.string('checksum', 64);
table.boolean('checksum_verified').defaultTo(false);
table.text('error_message');
table.timestamp('started_at');
table.timestamp('completed_at');
table.index(['restore_run_id', 'status']);
table.index(['file_path']);
});
// Create restore_validation_results table
await knex.schema.createTable('restore_validation_results', table => {
table.increments('id').primary();
table.integer('restore_run_id').notNullable()
.references('id').inTable('restore_runs').onDelete('CASCADE');
table.string('validation_type', 50).notNullable(); // pre-restore, post-restore
table.boolean('is_valid').notNullable();
table.text('errors'); // JSON array of errors
table.text('warnings'); // JSON array of warnings
table.text('checksums'); // JSON object with checksum comparisons
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([
{
setting_key: 'restore_allow_force',
setting_value: JSON.stringify(false),
setting_type: 'restore',
description: 'Allow force restore with warnings',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
},
{
setting_key: 'restore_require_pre_backup',
setting_value: JSON.stringify(true),
setting_type: 'restore',
description: 'Require pre-restore backup',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
},
{
setting_key: 'restore_max_file_size_mb',
setting_value: '5000',
setting_type: 'restore',
description: 'Maximum file size for restore (MB)',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
},
{
setting_key: 'restore_verify_checksums',
setting_value: JSON.stringify(true),
setting_type: 'restore',
description: 'Verify file checksums during restore',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
},
{
setting_key: 'restore_email_on_completion',
setting_value: JSON.stringify(true),
setting_type: 'restore',
description: 'Send email on restore completion',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
},
{
setting_key: 'restore_retention_days',
setting_value: '30',
setting_type: 'restore',
description: 'Days to retain restore history',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
}
]);
// Add new email templates for restore notifications
const emailTemplates = [
{
name: 'restore_completed',
subject: '✅ Restore Completed Successfully',
body: `<h2>Restore Operation Completed</h2>
<p>A restore operation has completed successfully.</p>
<h3>Details:</h3>
<ul>
<li><strong>Restore Type:</strong> {{restore_type}}</li>
<li><strong>Duration:</strong> {{duration}}</li>
<li><strong>Files Restored:</strong> {{files_restored}}</li>
<li><strong>Backup ID:</strong> {{backup_id}}</li>
<li><strong>Timestamp:</strong> {{timestamp}}</li>
</ul>
<p>Please verify that all systems are functioning correctly after the restore.</p>`,
language: 'en',
is_active: true,
created_at: knex.fn.now(),
updated_at: knex.fn.now()
},
{
name: 'restore_failed',
subject: '❌ Restore Operation Failed',
body: `<h2>Restore Operation Failed</h2>
<p>A restore operation has failed and requires attention.</p>
<h3>Details:</h3>
<ul>
<li><strong>Restore Type:</strong> {{restore_type}}</li>
<li><strong>Error:</strong> {{error_message}}</li>
<li><strong>Timestamp:</strong> {{timestamp}}</li>
</ul>
<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>`,
language: 'en',
is_active: true,
created_at: knex.fn.now(),
updated_at: knex.fn.now()
},
{
name: 'restore_completed',
subject: '✅ Wiederherstellung erfolgreich abgeschlossen',
body: `<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>`,
language: 'de',
is_active: true,
created_at: knex.fn.now(),
updated_at: knex.fn.now()
},
{
name: 'restore_failed',
subject: '❌ Wiederherstellungsvorgang fehlgeschlagen',
body: `<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>`,
language: 'de',
is_active: true,
created_at: knex.fn.now(),
updated_at: knex.fn.now()
}
];
await knex('email_templates').insert(emailTemplates);
};
exports.down = async function(knex) {
// Remove email templates
await knex('email_templates')
.whereIn('name', ['restore_completed', 'restore_failed'])
.delete();
// Remove settings
await knex('app_settings')
.where('setting_type', 'restore')
.delete();
// Drop tables
await knex.schema.dropTableIfExists('restore_validation_results');
await knex.schema.dropTableIfExists('restore_file_operations');
await knex.schema.dropTableIfExists('restore_runs');
};
@@ -0,0 +1,131 @@
// No helpers needed for boolean values
exports.up = async function(knex) {
console.log('Adding gallery feedback tables...');
// Create event_feedback_settings table
await knex.schema.createTable('event_feedback_settings', (table) => {
table.increments('id').primary();
table.integer('event_id').references('id').inTable('events').onDelete('CASCADE');
table.boolean('feedback_enabled').defaultTo(false);
table.boolean('allow_ratings').defaultTo(true);
table.boolean('allow_likes').defaultTo(true);
table.boolean('allow_comments').defaultTo(false);
table.boolean('allow_favorites').defaultTo(true);
table.boolean('require_name_email').defaultTo(false);
table.boolean('moderate_comments').defaultTo(true);
table.boolean('show_feedback_to_guests').defaultTo(true);
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
table.unique(['event_id']);
});
// Create photo_feedback table
await knex.schema.createTable('photo_feedback', (table) => {
table.increments('id').primary();
table.integer('photo_id').references('id').inTable('photos').onDelete('CASCADE');
table.integer('event_id').references('id').inTable('events').onDelete('CASCADE');
table.string('feedback_type', 20).notNullable();
table.integer('rating');
table.text('comment_text');
table.string('guest_name', 100);
table.string('guest_email', 255);
table.string('guest_identifier', 64);
table.string('ip_address', 45);
table.text('user_agent');
table.boolean('is_approved').defaultTo(true);
table.boolean('is_hidden').defaultTo(false);
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
// Add indexes
table.index(['photo_id']);
table.index(['event_id']);
table.index(['feedback_type']);
table.index(['guest_identifier']);
// Add check constraint for rating (PostgreSQL)
if (knex.client.config.client === 'pg') {
table.check('?? >= 1 AND ?? <= 5', ['rating', 'rating']);
}
});
// Create feedback_rate_limits table
await knex.schema.createTable('feedback_rate_limits', (table) => {
table.increments('id').primary();
table.string('identifier', 64).notNullable();
table.integer('event_id').references('id').inTable('events').onDelete('CASCADE');
table.string('action_type', 20).notNullable();
table.integer('action_count').defaultTo(1);
table.timestamp('window_start').defaultTo(knex.fn.now());
// Add indexes
table.index(['identifier', 'event_id', 'action_type']);
table.index(['window_start']);
});
// Create feedback_word_filters table
await knex.schema.createTable('feedback_word_filters', (table) => {
table.increments('id').primary();
table.string('word', 100).notNullable();
table.string('severity', 20).defaultTo('moderate');
table.boolean('is_active').defaultTo(true);
table.timestamp('created_at').defaultTo(knex.fn.now());
table.unique(['word']);
});
// Add feedback summary columns to photos table
await knex.schema.alterTable('photos', (table) => {
table.integer('feedback_count').defaultTo(0);
table.integer('like_count').defaultTo(0);
table.decimal('average_rating', 3, 2).defaultTo(0);
table.integer('favorite_count').defaultTo(0);
});
// Add feedback notification settings to app_settings
await knex('app_settings').insert([
{
setting_key: 'feedback_notification_email',
setting_value: JSON.stringify(''),
setting_type: 'feedback',
updated_at: new Date()
},
{
setting_key: 'feedback_rate_limits',
setting_value: JSON.stringify({
rating: { max: 100, window: 3600 }, // 100 ratings per hour
comment: { max: 20, window: 3600 }, // 20 comments per hour
like: { max: 200, window: 3600 } // 200 likes per hour
}),
setting_type: 'feedback',
updated_at: new Date()
}
]);
console.log('Gallery feedback tables created successfully');
};
exports.down = async function(knex) {
console.log('Removing gallery feedback tables...');
// Remove feedback settings from app_settings
await knex('app_settings')
.whereIn('setting_key', ['feedback_notification_email', 'feedback_rate_limits'])
.delete();
// Remove feedback columns from photos table
await knex.schema.alterTable('photos', (table) => {
table.dropColumn('feedback_count');
table.dropColumn('like_count');
table.dropColumn('average_rating');
table.dropColumn('favorite_count');
});
// Drop tables in reverse order
await knex.schema.dropTableIfExists('feedback_word_filters');
await knex.schema.dropTableIfExists('feedback_rate_limits');
await knex.schema.dropTableIfExists('photo_feedback');
await knex.schema.dropTableIfExists('event_feedback_settings');
console.log('Gallery feedback tables removed');
};
@@ -0,0 +1,139 @@
const { db } = require('../src/database/db');
async function up() {
console.log('Adding version tracking to backup tables...');
// Add version columns to database_backup_runs table
const hasDatabaseBackupRunsTable = await db.schema.hasTable('database_backup_runs');
if (hasDatabaseBackupRunsTable) {
const hasAppVersion = await db.schema.hasColumn('database_backup_runs', 'app_version');
if (!hasAppVersion) {
await db.schema.alterTable('database_backup_runs', (table) => {
table.string('app_version'); // Application version
table.string('node_version'); // Node.js version
table.string('db_schema_version'); // Database schema version (migration name)
table.json('environment_info'); // Additional environment information
});
console.log('Added version columns to database_backup_runs table');
}
}
// Add version columns to backup_runs table (file backups)
const hasBackupRunsTable = await db.schema.hasTable('backup_runs');
if (hasBackupRunsTable) {
const hasAppVersion = await db.schema.hasColumn('backup_runs', 'app_version');
if (!hasAppVersion) {
await db.schema.alterTable('backup_runs', (table) => {
table.string('app_version'); // Application version
table.string('node_version'); // Node.js version
table.string('db_schema_version'); // Database schema version
table.json('manifest_info'); // Manifest summary information
});
console.log('Added version columns to backup_runs table');
}
}
// Add restore tracking table
const hasRestoreHistoryTable = await db.schema.hasTable('restore_history');
if (!hasRestoreHistoryTable) {
await db.schema.createTable('restore_history', (table) => {
table.increments('id').primary();
table.datetime('started_at').notNullable();
table.datetime('completed_at');
table.string('status').defaultTo('running'); // running, completed, failed, partial
table.string('restore_type'); // database, files, full
table.string('backup_id'); // Reference to the backup that was restored
table.string('backup_app_version'); // Version of app that created the backup
table.string('restore_app_version'); // Version of app performing the restore
table.string('backup_node_version'); // Node version that created the backup
table.string('restore_node_version'); // Node version performing the restore
table.string('backup_schema_version'); // Schema version in the backup
table.string('restore_schema_version'); // Current schema version
table.json('version_compatibility'); // Compatibility check results
table.json('restore_options'); // Options used during restore
table.json('statistics'); // Restore statistics
table.text('warnings'); // Any warnings during restore
table.text('error_message'); // Error details if failed
table.string('restored_by'); // User who initiated the restore
table.index(['started_at'], 'idx_restore_started');
table.index(['backup_id'], 'idx_restore_backup_id');
});
console.log('Created restore_history table');
}
// Add version compatibility settings
const versionSettings = [
{
setting_key: 'backup_require_version_match',
setting_value: JSON.stringify(false), // If true, exact version match required for restore
setting_type: 'backup'
},
{
setting_key: 'backup_allow_minor_version_mismatch',
setting_value: JSON.stringify(true), // Allow restoring from same major version
setting_type: 'backup'
},
{
setting_key: 'backup_warn_on_version_mismatch',
setting_value: JSON.stringify(true), // Show warning when versions don't match
setting_type: 'backup'
},
{
setting_key: 'backup_check_schema_compatibility',
setting_value: JSON.stringify(true), // Check if migrations are compatible
setting_type: 'backup'
}
];
// Insert version settings if they don't exist
for (const setting of versionSettings) {
const exists = await db('app_settings')
.where('setting_key', setting.setting_key)
.first();
if (!exists) {
await db('app_settings').insert(setting);
}
}
console.log('Version tracking for backups added successfully');
}
async function down() {
// Remove version columns from database_backup_runs
const hasDatabaseBackupRunsTable = await db.schema.hasTable('database_backup_runs');
if (hasDatabaseBackupRunsTable) {
await db.schema.alterTable('database_backup_runs', (table) => {
table.dropColumn('app_version');
table.dropColumn('node_version');
table.dropColumn('db_schema_version');
table.dropColumn('environment_info');
});
}
// Remove version columns from backup_runs
const hasBackupRunsTable = await db.schema.hasTable('backup_runs');
if (hasBackupRunsTable) {
await db.schema.alterTable('backup_runs', (table) => {
table.dropColumn('app_version');
table.dropColumn('node_version');
table.dropColumn('db_schema_version');
table.dropColumn('manifest_info');
});
}
// Drop restore_history table
await db.schema.dropTableIfExists('restore_history');
// Remove version settings
await db('app_settings')
.whereIn('setting_key', [
'backup_require_version_match',
'backup_allow_minor_version_mismatch',
'backup_warn_on_version_mismatch',
'backup_check_schema_compatibility'
])
.delete();
}
module.exports = { up, down };
@@ -0,0 +1,228 @@
const { db } = require('../src/database/db');
async function up() {
console.log('Enhancing backup system...');
// Add new settings to app_settings table if they don't exist
const backupSettings = [
{
setting_key: 'backup_s3_force_path_style',
setting_value: JSON.stringify(false),
setting_type: 'backup',
description: 'Force path-style S3 URLs (for MinIO/self-hosted)'
},
{
setting_key: 'backup_s3_ssl_enabled',
setting_value: JSON.stringify(true),
setting_type: 'backup',
description: 'Enable SSL/TLS for S3 connections'
},
{
setting_key: 'backup_s3_prefix',
setting_value: JSON.stringify(''),
setting_type: 'backup',
description: 'S3 key prefix for organizing backups'
},
{
setting_key: 'backup_incremental',
setting_value: JSON.stringify(false),
setting_type: 'backup',
description: 'Enable incremental backups'
},
{
setting_key: 'backup_include_database',
setting_value: JSON.stringify(true),
setting_type: 'backup',
description: 'Include database dumps in backups'
},
{
setting_key: 'backup_encryption_enabled',
setting_value: JSON.stringify(false),
setting_type: 'backup',
description: 'Enable backup encryption'
},
{
setting_key: 'backup_database_schedule',
setting_value: JSON.stringify(''),
setting_type: 'backup',
description: 'Separate cron schedule for database-only backups'
}
];
// Insert settings if they don't exist
for (const setting of backupSettings) {
const exists = await db('app_settings')
.where('setting_key', setting.setting_key)
.first();
if (!exists) {
await db('app_settings').insert(setting);
}
}
// Check and add columns to backup_runs table
const hasBackupRunsTable = await db.schema.hasTable('backup_runs');
if (hasBackupRunsTable) {
// Check for existing columns before adding
const hasManifestPath = await db.schema.hasColumn('backup_runs', 'manifest_path');
const hasManifestId = await db.schema.hasColumn('backup_runs', 'manifest_id');
const hasManifestFormat = await db.schema.hasColumn('backup_runs', 'manifest_format');
const hasParentBackupId = await db.schema.hasColumn('backup_runs', 'parent_backup_id');
const hasBackupMode = await db.schema.hasColumn('backup_runs', 'backup_mode');
if (!hasManifestPath || !hasManifestId || !hasManifestFormat || !hasParentBackupId || !hasBackupMode) {
await db.schema.alterTable('backup_runs', (table) => {
if (!hasManifestPath) {
table.string('manifest_path', 500).comment('Path to backup manifest file');
}
if (!hasManifestId) {
table.uuid('manifest_id').comment('Unique identifier for the manifest');
}
if (!hasManifestFormat) {
table.enum('manifest_format', ['json', 'yaml']).comment('Format of the manifest file');
}
if (!hasParentBackupId) {
table.integer('parent_backup_id').unsigned().references('id').inTable('backup_runs').onDelete('SET NULL').comment('Parent backup for incremental backups');
}
if (!hasBackupMode) {
table.enum('backup_mode', ['full', 'incremental', 'database']).defaultTo('full').comment('Type of backup performed');
}
});
}
// Add indexes if they don't exist
try {
await db.raw('CREATE INDEX IF NOT EXISTS idx_backup_runs_mode_status ON backup_runs(backup_mode, status)');
await db.raw('CREATE INDEX IF NOT EXISTS idx_backup_runs_parent ON backup_runs(parent_backup_id)');
await db.raw('CREATE INDEX IF NOT EXISTS idx_backup_runs_created_mode ON backup_runs(created_at, backup_mode)');
} catch (error) {
console.log('Note: Some indexes may already exist, continuing...');
}
}
// Create backup_manifest table if it doesn't exist
const hasManifestTable = await db.schema.hasTable('backup_manifest');
if (!hasManifestTable) {
await db.schema.createTable('backup_manifest', (table) => {
table.increments('id').primary();
table.integer('backup_run_id').unsigned().notNullable().references('id').inTable('backup_runs').onDelete('CASCADE');
table.uuid('manifest_id').notNullable().unique().comment('Unique identifier matching backup_runs.manifest_id');
table.string('version', 20).notNullable().defaultTo('1.0.0').comment('Manifest schema version');
table.enum('format', ['json', 'yaml']).notNullable().defaultTo('json');
// Backup metadata
table.timestamp('backup_start').notNullable();
table.timestamp('backup_end').notNullable();
table.bigInteger('total_size').unsigned().comment('Total size of backup in bytes');
table.integer('file_count').unsigned().comment('Number of files in backup');
table.integer('photo_count').unsigned().comment('Number of photos backed up');
table.integer('event_count').unsigned().comment('Number of events backed up');
// Incremental backup metadata
table.boolean('is_incremental').defaultTo(false);
table.uuid('parent_manifest_id').comment('Parent manifest ID for incremental backups');
table.timestamp('incremental_since').comment('Timestamp for incremental backup baseline');
// Content checksums
table.string('checksum_algorithm', 50).defaultTo('sha256').comment('Algorithm used for checksums');
table.text('manifest_checksum').comment('Checksum of the manifest file itself');
// Storage information
table.string('storage_location', 500).comment('Primary storage location (local path or S3 URI)');
table.string('storage_provider', 50).comment('Storage provider (local, s3, etc.)');
// Encryption metadata
table.boolean('is_encrypted').defaultTo(false);
table.string('encryption_algorithm', 100).comment('Encryption algorithm used');
table.string('encryption_key_id', 255).comment('ID of encryption key used');
// Additional metadata as JSON
table.json('metadata').comment('Additional metadata as JSON');
// Timestamps
table.timestamps(true, true);
// Indexes
table.index(['backup_run_id'], 'idx_manifest_backup_run');
table.index(['manifest_id'], 'idx_manifest_uuid');
table.index(['parent_manifest_id'], 'idx_manifest_parent');
table.index(['backup_start', 'backup_end'], 'idx_manifest_time_range');
table.index(['is_incremental', 'created_at'], 'idx_manifest_incremental_created');
});
}
// Add composite indexes for common query patterns
try {
await db.raw(`
CREATE INDEX IF NOT EXISTS idx_backup_runs_recent_successful
ON backup_runs(created_at DESC)
WHERE status = 'completed' AND backup_mode = 'full';
`);
await db.raw(`
CREATE INDEX IF NOT EXISTS idx_backup_runs_incremental_chain
ON backup_runs(parent_backup_id, created_at)
WHERE backup_mode = 'incremental';
`);
} catch (error) {
console.log('Note: Some composite indexes may already exist, continuing...');
}
console.log('Backup system enhancements completed');
}
async function down() {
// Drop indexes first
try {
await db.raw('DROP INDEX IF EXISTS idx_backup_runs_incremental_chain;');
await db.raw('DROP INDEX IF EXISTS idx_backup_runs_recent_successful;');
} catch (error) {
// Ignore errors if indexes don't exist
}
// Drop backup_manifest table
await db.schema.dropTableIfExists('backup_manifest');
// Remove columns from backup_runs if they exist
const hasBackupRunsTable = await db.schema.hasTable('backup_runs');
if (hasBackupRunsTable) {
const hasBackupMode = await db.schema.hasColumn('backup_runs', 'backup_mode');
const hasParentBackupId = await db.schema.hasColumn('backup_runs', 'parent_backup_id');
const hasManifestFormat = await db.schema.hasColumn('backup_runs', 'manifest_format');
const hasManifestId = await db.schema.hasColumn('backup_runs', 'manifest_id');
const hasManifestPath = await db.schema.hasColumn('backup_runs', 'manifest_path');
if (hasBackupMode || hasParentBackupId || hasManifestFormat || hasManifestId || hasManifestPath) {
await db.schema.alterTable('backup_runs', (table) => {
if (hasBackupMode) table.dropColumn('backup_mode');
if (hasParentBackupId) table.dropColumn('parent_backup_id');
if (hasManifestFormat) table.dropColumn('manifest_format');
if (hasManifestId) table.dropColumn('manifest_id');
if (hasManifestPath) table.dropColumn('manifest_path');
});
}
// Drop indexes
try {
await db.raw('DROP INDEX IF EXISTS idx_backup_runs_mode_status');
await db.raw('DROP INDEX IF EXISTS idx_backup_runs_parent');
await db.raw('DROP INDEX IF EXISTS idx_backup_runs_created_mode');
} catch (error) {
// Ignore errors if indexes don't exist
}
}
// Remove settings
await db('app_settings')
.whereIn('setting_key', [
'backup_s3_force_path_style',
'backup_s3_ssl_enabled',
'backup_s3_prefix',
'backup_incremental',
'backup_include_database',
'backup_encryption_enabled',
'backup_database_schedule'
])
.delete();
}
module.exports = { up, down };
@@ -1,25 +0,0 @@
const { db } = require('../src/database/db');
async function addMustChangePasswordColumn() {
try {
// Check if the column already exists
const hasMustChangePassword = await db.schema.hasColumn('admin_users', 'must_change_password');
if (!hasMustChangePassword) {
await db.schema.table('admin_users', (table) => {
table.boolean('must_change_password').defaultTo(false);
});
console.log('✅ Added must_change_password column to admin_users table');
} else {
console.log('️ must_change_password column already exists');
}
process.exit(0);
} catch (error) {
console.error('❌ Migration failed:', error);
process.exit(1);
}
}
addMustChangePasswordColumn();
+83
View File
@@ -0,0 +1,83 @@
/**
* Migration helper functions for production-safe migrations
*/
/**
* Create a table only if it doesn't already exist
*/
async function createTableIfNotExists(knex, tableName, callback) {
const exists = await knex.schema.hasTable(tableName);
if (!exists) {
console.log(`Creating table: ${tableName}`);
return knex.schema.createTable(tableName, callback);
} else {
console.log(`Table ${tableName} already exists, skipping...`);
}
}
/**
* Add column to table only if it doesn't exist
*/
async function addColumnIfNotExists(knex, tableName, columnName, callback) {
const hasColumn = await knex.schema.hasColumn(tableName, columnName);
if (!hasColumn) {
console.log(`Adding column ${columnName} to table ${tableName}`);
return knex.schema.alterTable(tableName, (table) => {
callback(table);
});
} else {
console.log(`Column ${columnName} already exists in table ${tableName}, skipping...`);
}
}
/**
* Insert data only if it doesn't already exist
*/
async function insertIfNotExists(knex, tableName, data, uniqueField) {
const exists = await knex(tableName)
.where(uniqueField, data[uniqueField])
.first();
if (!exists) {
console.log(`Inserting ${uniqueField}: ${data[uniqueField]} into ${tableName}`);
return knex(tableName).insert(data);
} else {
console.log(`${uniqueField}: ${data[uniqueField]} already exists in ${tableName}, skipping...`);
}
}
/**
* Create index only if it doesn't exist
*/
async function createIndexIfNotExists(knex, tableName, columns, indexName) {
// This is database-specific, works for PostgreSQL
if (knex.client.config.client === 'pg') {
const result = await knex.raw(`
SELECT 1 FROM pg_indexes
WHERE tablename = ? AND indexname = ?
`, [tableName, indexName]);
if (result.rows.length === 0) {
console.log(`Creating index ${indexName} on ${tableName}`);
return knex.schema.alterTable(tableName, (table) => {
table.index(columns, indexName);
});
}
} else {
// For SQLite, just try to create and ignore errors
try {
await knex.schema.alterTable(tableName, (table) => {
table.index(columns, indexName);
});
} catch (error) {
// Index probably already exists
}
}
}
module.exports = {
createTableIfNotExists,
addColumnIfNotExists,
insertIfNotExists,
createIndexIfNotExists
};
+170
View File
@@ -0,0 +1,170 @@
const fs = require('fs').promises;
const path = require('path');
const { db } = require('../src/database/db');
/**
* Production-safe migration runner that handles existing schema
*/
// Create or verify migrations tracking table
async function ensureMigrationsTable() {
const tableExists = await db.schema.hasTable('migrations');
if (!tableExists) {
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 if a migration has been applied
async function isMigrationApplied(filename) {
const result = await db('migrations').where('filename', filename).first();
return !!result;
}
// 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`);
}
// Detect existing schema and mark migrations as applied
async function detectExistingSchema() {
console.log('Detecting existing schema...');
const tableChecks = [
{ table: 'events', migration: 'init.js' },
{ table: 'photos', migration: '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' },
];
for (const check of tableChecks) {
const exists = await db.schema.hasTable(check.table);
if (exists) {
const isApplied = await isMigrationApplied(check.migration);
if (!isApplied) {
await markMigrationAsApplied(check.migration);
}
}
}
}
// Run a single migration safely
async function runMigrationSafely(filename) {
try {
const migrationPath = path.join(__dirname, filename);
const migration = require(migrationPath);
if (migration.up) {
console.log(`Running migration: ${filename}`);
// Run migration in a transaction if possible
if (db.client.config.client === 'pg') {
await db.transaction(async (trx) => {
await migration.up(trx);
});
} else {
await migration.up(db);
}
await db('migrations').insert({ filename });
console.log(`Migration ${filename} 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);
} else {
throw error;
}
}
}
// Main migration runner
async function runMigrations() {
let connection;
try {
console.log('Starting production-safe database migrations...');
// Ensure database connection is ready
await db.raw('SELECT 1');
console.log('Database connection verified');
// Create migrations tracking table
await ensureMigrationsTable();
// Detect and mark existing schema
await detectExistingSchema();
// 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);
});
// Run pending migrations
let pendingCount = 0;
let skippedCount = 0;
for (const file of migrationFiles) {
const isApplied = await isMigrationApplied(file);
if (!isApplied) {
await runMigrationSafely(file);
pendingCount++;
} else {
skippedCount++;
}
}
console.log(`\nMigration Summary:`);
console.log(`- Applied: ${pendingCount} migration(s)`);
console.log(`- Skipped: ${skippedCount} migration(s) (already applied)`);
console.log(`- Total: ${migrationFiles.length} migration(s)`);
console.log('\nAll migrations completed successfully');
// Close database connection
await db.destroy();
process.exit(0);
} catch (error) {
console.error('\n❌ Migration failed:', error.message);
console.error('Error details:', error);
// Close database connection on error
try {
await db.destroy();
} catch (e) {
// Ignore
}
process.exit(1);
}
}
// Add delay for database readiness in production
async function waitAndRun() {
if (process.env.NODE_ENV === 'production') {
console.log('Waiting 2 seconds for database readiness...');
await new Promise(resolve => setTimeout(resolve, 2000));
}
await runMigrations();
}
// Only run if called directly
if (require.main === module) {
waitAndRun();
}
module.exports = { runMigrations };
+2586 -308
View File
File diff suppressed because it is too large Load Diff
+20 -9
View File
@@ -1,39 +1,50 @@
{
"name": "picpeak-backend",
"version": "1.0.1",
"version": "1.0.80",
"description": "Backend for PicPeak event photo sharing platform",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"migrate": "node migrations/run-migrations.js",
"migrate:safe": "node migrations/run-migrations-safe.js",
"fix-temp-photos": "node scripts/fix-temp-photos.js",
"test": "jest",
"lint": "eslint src/"
"lint": "eslint src/",
"test-backup": "node scripts/test-backup-service.js",
"test-restore": "node scripts/test-restore-service.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.850.0",
"@aws-sdk/lib-storage": "^3.850.0",
"@aws-sdk/s3-request-presigner": "^3.850.0",
"adm-zip": "^0.5.16",
"archiver": "^5.3.1",
"axios": "^1.10.0",
"bcrypt": "^5.1.0",
"chokidar": "^3.5.3",
"bcrypt": "6.0.0",
"chokidar": "4.0.3",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"express-validator": "^7.0.1",
"form-data": "^4.0.3",
"form-data": "^4.0.4",
"handlebars": "^4.7.8",
"helmet": "^7.0.0",
"i18next": "^25.3.1",
"i18next": "25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"joi": "^17.9.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"knex": "^2.4.2",
"multer": "^2.0.1",
"mime-types": "^3.0.1",
"multer": "^2.0.2",
"node-cron": "^3.0.2",
"nodemailer": "^6.9.1",
"nodemailer": "7.0.5",
"pg": "^8.16.3",
"react-i18next": "^15.6.0",
"sharp": "^0.32.0",
"sharp": "0.34.3",
"sqlite3": "^5.1.6",
"uuid": "^11.1.0",
"winston": "^3.8.2",
-80
View File
@@ -1,80 +0,0 @@
#!/usr/bin/env node
/**
* Activate enhanced authentication in server.js
* This script safely updates the server configuration
*/
const fs = require('fs').promises;
const path = require('path');
async function activateEnhancedAuth() {
console.log('=== Activating Enhanced Authentication ===\n');
try {
const serverPath = path.join(__dirname, '../server.js');
// Read current server.js
let serverContent = await fs.readFile(serverPath, 'utf8');
// Backup current server.js
const backupPath = `${serverPath}.backup.${Date.now()}`;
await fs.writeFile(backupPath, serverContent);
console.log(`✓ Created backup: ${path.basename(backupPath)}`);
// Check current state
if (serverContent.includes("require('./src/routes/auth-enhanced')")) {
console.log('! Enhanced auth already active');
return;
}
// Replace auth routes import
const originalLine = "const authRoutes = require('./src/routes/auth');";
const enhancedLine = "const authRoutes = require('./src/routes/auth-enhanced');";
if (!serverContent.includes(originalLine)) {
console.log('✗ Could not find original auth import line');
console.log('Please manually update server.js');
return;
}
serverContent = serverContent.replace(originalLine, enhancedLine);
console.log('✓ Updated auth routes import');
// Add cleanup job initialization after database init
const dbInitLine = 'initializeDatabase()';
const cleanupAddition = `
// Initialize auth security cleanup job
const { initializeCleanupJob } = require('./src/utils/authSecurity');
initializeCleanupJob();
`;
if (!serverContent.includes('initializeCleanupJob')) {
const dbInitIndex = serverContent.indexOf(dbInitLine);
if (dbInitIndex !== -1) {
const insertPoint = serverContent.indexOf('\n', dbInitIndex) + 1;
serverContent = serverContent.slice(0, insertPoint) + cleanupAddition + serverContent.slice(insertPoint);
console.log('✓ Added cleanup job initialization');
}
}
// Write updated server.js
await fs.writeFile(serverPath, serverContent);
console.log('✓ Updated server.js');
console.log('\n✅ Enhanced authentication activated!');
console.log('\nNext steps:');
console.log('1. Restart the backend:');
console.log(' docker-compose restart backend');
console.log('\n2. Monitor auth health:');
console.log(' node scripts/monitor-auth-health.js');
console.log('\n3. To rollback if needed:');
console.log(` cp ${path.basename(backupPath)} server.js`);
console.log(' docker-compose restart backend');
} catch (error) {
console.error('❌ Error activating enhanced auth:', error);
}
}
activateEnhancedAuth();
-94
View File
@@ -1,94 +0,0 @@
#!/usr/bin/env node
/**
* Add authentication security tables to existing database
*/
const { db } = require('../src/database/db');
async function addAuthTables() {
console.log('Adding authentication security tables...\n');
try {
// 1. Create login_attempts table
const hasLoginAttempts = await db.schema.hasTable('login_attempts');
if (!hasLoginAttempts) {
await db.schema.createTable('login_attempts', table => {
table.increments('id').primary();
table.string('identifier').notNullable();
table.string('ip_address', 45).notNullable();
table.text('user_agent');
table.timestamp('attempt_time').defaultTo(db.fn.now());
table.boolean('success').defaultTo(false);
// Indexes for performance
table.index('identifier');
table.index('attempt_time');
table.index(['identifier', 'success', 'attempt_time']);
});
console.log('✓ Created login_attempts table');
} else {
console.log('! login_attempts table already exists');
}
// 2. Add columns to admin_users
const hasPasswordChangedAt = await db.schema.hasColumn('admin_users', 'password_changed_at');
if (!hasPasswordChangedAt) {
await db.schema.table('admin_users', table => {
table.timestamp('password_changed_at').nullable();
});
console.log('✓ Added password_changed_at column');
}
const hasLastLoginIp = await db.schema.hasColumn('admin_users', 'last_login_ip');
if (!hasLastLoginIp) {
await db.schema.table('admin_users', table => {
table.string('last_login_ip', 45).nullable();
});
console.log('✓ Added last_login_ip column');
}
const hasTwoFactorEnabled = await db.schema.hasColumn('admin_users', 'two_factor_enabled');
if (!hasTwoFactorEnabled) {
await db.schema.table('admin_users', table => {
table.boolean('two_factor_enabled').defaultTo(false);
});
console.log('✓ Added two_factor_enabled column');
}
const hasTwoFactorSecret = await db.schema.hasColumn('admin_users', 'two_factor_secret');
if (!hasTwoFactorSecret) {
await db.schema.table('admin_users', table => {
table.string('two_factor_secret').nullable();
});
console.log('✓ Added two_factor_secret column');
}
// 3. Verify everything
console.log('\nVerifying tables...');
const loginAttemptsInfo = await db('login_attempts').columnInfo();
console.log('✓ login_attempts columns:', Object.keys(loginAttemptsInfo).join(', '));
const adminUsersInfo = await db('admin_users').columnInfo();
const securityColumns = ['password_changed_at', 'last_login_ip', 'two_factor_enabled', 'two_factor_secret'];
const hasAllColumns = securityColumns.every(col => adminUsersInfo[col]);
if (hasAllColumns) {
console.log('✓ All security columns present in admin_users');
} else {
console.log('✗ Some security columns missing from admin_users');
}
console.log('\n✅ Authentication security tables ready!');
await db.destroy();
process.exit(0);
} catch (error) {
console.error('\n❌ Error adding auth tables:', error);
await db.destroy();
process.exit(1);
}
}
addAuthTables();
@@ -1,71 +0,0 @@
#!/usr/bin/env node
/**
* Add token revocation tables to existing database
*/
const { db } = require('../src/database/db');
async function addTokenRevocationTables() {
console.log('Adding token revocation tables...\n');
try {
// 1. Create revoked_tokens table
const hasRevokedTokens = await db.schema.hasTable('revoked_tokens');
if (!hasRevokedTokens) {
await db.schema.createTable('revoked_tokens', table => {
table.increments('id').primary();
table.string('token_id').notNullable().unique();
table.integer('user_id').nullable();
table.string('token_type', 20);
table.timestamp('revoked_at').defaultTo(db.fn.now());
table.timestamp('expires_at').notNullable();
table.string('reason', 100);
table.text('metadata');
// Indexes
table.index('token_id');
table.index('user_id');
table.index('expires_at');
});
console.log('✓ Created revoked_tokens table');
} else {
console.log('! revoked_tokens table already exists');
}
// 2. Create user_token_revocations table
const hasUserRevocations = await db.schema.hasTable('user_token_revocations');
if (!hasUserRevocations) {
await db.schema.createTable('user_token_revocations', table => {
table.integer('user_id').primary();
table.timestamp('revoked_at').notNullable();
table.string('reason', 100);
table.index('revoked_at');
});
console.log('✓ Created user_token_revocations table');
} else {
console.log('! user_token_revocations table already exists');
}
// 3. Verify tables
console.log('\nVerifying tables...');
const revokedTokensInfo = await db('revoked_tokens').columnInfo();
console.log('✓ revoked_tokens columns:', Object.keys(revokedTokensInfo).join(', '));
const userRevocationsInfo = await db('user_token_revocations').columnInfo();
console.log('✓ user_token_revocations columns:', Object.keys(userRevocationsInfo).join(', '));
console.log('\n✅ Token revocation tables ready!');
await db.destroy();
process.exit(0);
} catch (error) {
console.error('\n❌ Error adding token revocation tables:', error);
await db.destroy();
process.exit(1);
}
}
addTokenRevocationTables();
+60
View File
@@ -0,0 +1,60 @@
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();
-96
View File
@@ -1,96 +0,0 @@
#!/usr/bin/env node
/**
* Check Docker deployment status for security fixes
*/
console.log('=== Docker Deployment Status Check ===\n');
// Color codes
const GREEN = '\x1b[32m';
const RED = '\x1b[31m';
const YELLOW = '\x1b[33m';
const RESET = '\x1b[0m';
let allGood = true;
// Check SQL Security
console.log('1. SQL Injection Fixes:');
try {
const { sanitizeDays, escapeLikePattern } = require('../src/utils/sqlSecurity');
console.log(`${GREEN}${RESET} sqlSecurity.js exists`);
console.log(`${GREEN}${RESET} Security functions available`);
} catch (e) {
console.log(`${RED}${RESET} sqlSecurity.js missing`);
allGood = false;
}
// Check Auth Security
console.log('\n2. Authentication Security:');
try {
const authSec = require('../src/utils/authSecurity');
console.log(`${GREEN}${RESET} authSecurity.js exists`);
const authEnhanced = require('../src/middleware/auth-enhanced');
console.log(`${GREEN}${RESET} auth-enhanced middleware exists`);
const authRoutes = require('../src/routes/auth-enhanced');
console.log(`${GREEN}${RESET} auth-enhanced routes exist`);
} catch (e) {
console.log(`${YELLOW}!${RESET} Auth security files exist but not active`);
}
// Check Database
console.log('\n3. Database Status:');
const { db } = require('../src/database/db');
async function checkDatabase() {
try {
// Check login_attempts table
await db('login_attempts').count();
console.log(`${GREEN}${RESET} login_attempts table exists`);
} catch (e) {
console.log(`${YELLOW}!${RESET} login_attempts table not created (run migrations)`);
}
try {
// Check admin_users columns
await db('admin_users').select('password_changed_at').limit(1);
console.log(`${GREEN}${RESET} Auth security columns exist`);
} catch (e) {
console.log(`${YELLOW}!${RESET} Auth security columns missing (run migrations)`);
}
// Close database connection
await db.destroy();
}
// Check server configuration
console.log('\n4. Server Configuration:');
const fs = require('fs');
const serverContent = fs.readFileSync('./server.js', 'utf8');
if (serverContent.includes("require('./src/routes/auth-enhanced')")) {
console.log(`${GREEN}${RESET} Using enhanced auth routes`);
} else if (serverContent.includes("require('./src/routes/auth')")) {
console.log(`${YELLOW}!${RESET} Using original auth routes (enhanced not active)`);
}
if (serverContent.includes('initializeCleanupJob')) {
console.log(`${GREEN}${RESET} Auth cleanup job initialized`);
} else {
console.log(`${YELLOW}!${RESET} Auth cleanup job not initialized`);
}
// Run async checks
checkDatabase().then(() => {
console.log('\n=== Summary ===');
if (allGood) {
console.log(`${GREEN}All security fixes are deployed!${RESET}`);
} else {
console.log(`${YELLOW}Some security features need activation:${RESET}`);
console.log('1. Run migrations: npx knex migrate:latest');
console.log('2. Update server.js to use auth-enhanced routes');
console.log('3. Restart the container');
}
});
+131
View File
@@ -0,0 +1,131 @@
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);
});
+157
View File
@@ -0,0 +1,157 @@
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();
+66
View File
@@ -0,0 +1,66 @@
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();
+53
View File
@@ -0,0 +1,53 @@
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();
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env node
/**
* Script to check storage directory structure and verify files
* Usage: node scripts/check-storage.js [eventSlug]
*/
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');
async function checkDirectory(dirPath, description) {
try {
await fs.access(dirPath);
const stats = await fs.stat(dirPath);
const files = await fs.readdir(dirPath);
console.log(`${description}: ${dirPath}`);
console.log(` - Files/Folders: ${files.length}`);
console.log(` - Permissions: ${(stats.mode & parseInt('777', 8)).toString(8)}`);
return true;
} catch (error) {
console.log(`${description}: ${dirPath} - ${error.message}`);
return false;
}
}
async function checkStorageStructure(eventSlug = null) {
console.log('Checking storage structure...');
console.log(`Storage base path: ${STORAGE_PATH}\n`);
// Check main directories
await checkDirectory(STORAGE_PATH, 'Storage root');
await checkDirectory(path.join(STORAGE_PATH, 'events'), 'Events directory');
await checkDirectory(path.join(STORAGE_PATH, 'events/active'), 'Active events');
await checkDirectory(path.join(STORAGE_PATH, 'events/archived'), 'Archived events');
await checkDirectory(path.join(STORAGE_PATH, 'thumbnails'), 'Thumbnails');
await checkDirectory(path.join(STORAGE_PATH, 'uploads'), 'Uploads');
console.log('\n---\n');
// If event slug provided, check specific event
if (eventSlug) {
console.log(`Checking specific event: ${eventSlug}`);
const event = await db('events').where('slug', eventSlug).first();
if (!event) {
console.log(`✗ Event not found in database: ${eventSlug}`);
return;
}
console.log(`✓ Event found in database:`);
console.log(` - ID: ${event.id}`);
console.log(` - Name: ${event.event_name}`);
console.log(` - Active: ${event.is_active}`);
console.log(` - Archived: ${event.is_archived}`);
// Check event directory
const eventDir = path.join(STORAGE_PATH, 'events/active', eventSlug);
const eventExists = await checkDirectory(eventDir, 'Event directory');
if (eventExists) {
const files = await fs.readdir(eventDir);
console.log(` - Photo files: ${files.filter(f => /\.(jpg|jpeg|png|gif)$/i.test(f)).length}`);
}
// Check photos in database
const photos = await db('photos').where('event_id', event.id).select('id', 'filename', 'path', 'thumbnail_path');
console.log(`\nDatabase photos: ${photos.length}`);
// Check if photo files exist
let existingPhotos = 0;
let missingPhotos = 0;
let existingThumbnails = 0;
let missingThumbnails = 0;
for (const photo of photos) {
const photoPath = path.join(STORAGE_PATH, 'events/active', photo.path);
try {
await fs.access(photoPath);
existingPhotos++;
} catch {
missingPhotos++;
console.log(` ✗ Missing photo: ${photo.path}`);
}
if (photo.thumbnail_path) {
const thumbPath = path.join(STORAGE_PATH, photo.thumbnail_path.replace(/^\//, ''));
try {
await fs.access(thumbPath);
existingThumbnails++;
} catch {
missingThumbnails++;
console.log(` ✗ Missing thumbnail: ${photo.thumbnail_path}`);
}
}
}
console.log(`\nFile check summary:`);
console.log(` - Photos: ${existingPhotos} exist, ${missingPhotos} missing`);
console.log(` - Thumbnails: ${existingThumbnails} exist, ${missingThumbnails} missing`);
} else {
// List all event directories
try {
const activeDir = path.join(STORAGE_PATH, 'events/active');
const eventDirs = await fs.readdir(activeDir);
console.log(`Active event directories: ${eventDirs.length}`);
for (const dir of eventDirs.slice(0, 10)) {
console.log(` - ${dir}`);
}
if (eventDirs.length > 10) {
console.log(` ... and ${eventDirs.length - 10} more`);
}
} catch (error) {
console.log('Could not list event directories:', error.message);
}
}
}
// Parse command line arguments
const eventSlug = process.argv[2] || null;
// Run the script
checkStorageStructure(eventSlug).then(async () => {
await db.destroy();
console.log('\nStorage check complete');
}).catch(async error => {
console.error('Error:', error);
await db.destroy();
process.exit(1);
});
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env node
/**
* Script to clean up orphaned and temporary thumbnails
* Usage: node scripts/cleanup-thumbnails.js [--dry-run]
*/
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 cleanupThumbnails(dryRun = false) {
console.log('Starting thumbnail cleanup...');
console.log(`Thumbnails directory: ${THUMBNAILS_DIR}`);
console.log(`Mode: ${dryRun ? 'DRY RUN' : 'LIVE'}\n`);
try {
// Get all thumbnail files
const files = await fs.readdir(THUMBNAILS_DIR);
console.log(`Found ${files.length} files in thumbnails directory`);
// Get all valid thumbnail paths from database
const validThumbnails = await db('photos')
.whereNotNull('thumbnail_path')
.select('thumbnail_path');
const validPaths = new Set(
validThumbnails.map(t => path.basename(t.thumbnail_path))
);
console.log(`Found ${validPaths.size} valid thumbnails in database\n`);
let tempCount = 0;
let orphanedCount = 0;
let validCount = 0;
let deletedCount = 0;
for (const file of files) {
// Skip directories
const filePath = path.join(THUMBNAILS_DIR, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) continue;
// Check if it's a temporary file
if (file.startsWith('thumb_temp_')) {
tempCount++;
console.log(`Temporary file: ${file}`);
if (!dryRun) {
try {
await fs.unlink(filePath);
deletedCount++;
} catch (error) {
console.error(` Failed to delete: ${error.message}`);
}
}
}
// Check if it's an orphaned thumbnail
else if (!validPaths.has(file)) {
orphanedCount++;
console.log(`Orphaned file: ${file}`);
if (!dryRun) {
try {
await fs.unlink(filePath);
deletedCount++;
} catch (error) {
console.error(` Failed to delete: ${error.message}`);
}
}
} else {
validCount++;
}
}
console.log('\n--- Summary ---');
console.log(`Total files: ${files.length}`);
console.log(`Valid thumbnails: ${validCount}`);
console.log(`Temporary files: ${tempCount}`);
console.log(`Orphaned files: ${orphanedCount}`);
if (!dryRun) {
console.log(`Deleted files: ${deletedCount}`);
} else {
console.log(`Files to be deleted: ${tempCount + orphanedCount}`);
}
} catch (error) {
console.error('Error during cleanup:', error);
process.exit(1);
}
}
// Parse command line arguments
const dryRun = process.argv.includes('--dry-run');
// Run the cleanup
cleanupThumbnails(dryRun).then(async () => {
await db.destroy();
console.log('\nCleanup complete');
}).catch(async error => {
console.error('Cleanup failed:', error);
await db.destroy();
process.exit(1);
});
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env node
/**
* Script to create an admin user
* Usage: node scripts/create-admin.js --email admin@example.com --username admin --password yourpassword
*
* If no password is provided, a random one will be generated and displayed
*/
require('dotenv').config();
const bcrypt = require('bcrypt');
const { db } = require('../src/database/db');
const crypto = require('crypto');
// Parse command line arguments
const args = process.argv.slice(2);
const getArg = (name) => {
const index = args.findIndex(arg => arg === `--${name}`);
return index !== -1 && args[index + 1] ? args[index + 1] : null;
};
const email = getArg('email');
const username = getArg('username') || email?.split('@')[0] || 'admin';
let password = getArg('password');
// Validate email
if (!email) {
console.error('Error: Email is required. Use --email admin@example.com');
process.exit(1);
}
// Generate password if not provided
if (!password) {
password = crypto.randomBytes(12).toString('base64').slice(0, 16);
console.log(`Generated password: ${password}`);
console.log('Please save this password securely!');
}
async function createAdmin() {
try {
// Check if user already exists
const existingUser = await db('admin_users')
.where('email', email)
.orWhere('username', username)
.first();
if (existingUser) {
console.error(`Error: User with email "${email}" or username "${username}" already exists`);
process.exit(1);
}
// Hash password
const passwordHash = await bcrypt.hash(password, 10);
// Create admin user
await db('admin_users').insert({
username,
email,
password_hash: passwordHash,
is_active: true,
created_at: new Date(),
updated_at: new Date()
});
console.log(`✅ Admin user created successfully!`);
console.log(` Email: ${email}`);
console.log(` Username: ${username}`);
console.log(` Login URL: ${process.env.ADMIN_URL || 'http://localhost:3000'}/admin/login`);
process.exit(0);
} catch (error) {
console.error('Error creating admin user:', error.message);
process.exit(1);
}
}
createAdmin();
+2 -1
View File
@@ -36,7 +36,8 @@ async function createTestEvent() {
await db('events').where('slug', eventData.slug).delete();
// Insert new event
const [eventId] = await db('events').insert(eventData);
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!');
+92
View File
@@ -0,0 +1,92 @@
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();
+146
View File
@@ -0,0 +1,146 @@
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();
-56
View File
@@ -1,56 +0,0 @@
const knex = require('knex')({
client: 'sqlite3',
connection: { filename: '/app/data/photo_sharing.db' },
useNullAsDefault: true
});
async function debugEventPhotos() {
try {
// Get all photos for event 12
const photos = await knex('photos')
.where('event_id', 12)
.select('id', 'filename', 'path', 'thumbnail_path')
.orderBy('id');
console.log('Total photos for event 12:', photos.length);
console.log('\nSample photos:');
// Show first few and specific IDs that were failing
const sampleIds = [1686, 1687, 1688, 1689, 1715, 1717, 1718, 1719];
const samples = photos.filter(p => sampleIds.includes(p.id));
samples.forEach(p => {
console.log(`\nID ${p.id}: ${p.filename}`);
console.log(` Path: ${p.path}`);
console.log(` Thumbnail: ${p.thumbnail_path}`);
});
// Check for any photos without thumbnails
const noThumbs = photos.filter(p => !p.thumbnail_path);
if (noThumbs.length > 0) {
console.log(`\nPhotos without thumbnails: ${noThumbs.length}`);
noThumbs.forEach(p => console.log(` ID ${p.id}: ${p.filename}`));
}
// Check file existence for failing photos
const fs = require('fs').promises;
console.log('\nChecking file existence for samples:');
for (const photo of samples) {
const thumbPath = `/app/storage/${photo.thumbnail_path}`;
try {
await fs.access(thumbPath);
console.log(`✓ ID ${photo.id}: Thumbnail exists at ${thumbPath}`);
} catch (err) {
console.log(`✗ ID ${photo.id}: Thumbnail NOT FOUND at ${thumbPath}`);
}
}
} catch (error) {
console.error('Error:', error);
} finally {
knex.destroy();
}
}
debugEventPhotos();
-81
View File
@@ -1,81 +0,0 @@
#!/usr/bin/env node
const sqlite3 = require('sqlite3').verbose();
const fs = require('fs');
const path = require('path');
// 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');
});
// Query for photos with IDs 1688 and 1689 where event_id = 12
const query = `
SELECT p.id, p.filename, p.path, p.thumbnail_path, p.event_id,
e.slug as event_slug, e.is_active, e.is_archived
FROM photos p
JOIN events e ON p.event_id = e.id
WHERE p.id IN (1688, 1689) AND p.event_id = 12
`;
console.log('Executing query to get photo details with event information...\n');
db.all(query, [], (err, rows) => {
if (err) {
console.error('Error executing query:', err.message);
db.close();
process.exit(1);
}
console.log(`Found ${rows.length} photo(s):\n`);
if (rows.length === 0) {
console.log('No photos found matching the criteria.');
} else {
rows.forEach((row) => {
console.log('=== Photo ID:', row.id, '===');
console.log('Filename:', row.filename);
console.log('DB Path:', row.path);
console.log('DB Thumbnail Path:', row.thumbnail_path);
console.log('Event ID:', row.event_id);
console.log('Event Slug:', row.event_slug);
console.log('Event is_active:', row.is_active);
console.log('Event is_archived:', row.is_archived);
// Check file existence
const storageBase = '/app/storage';
const eventStatusDir = row.is_active ? 'active' : 'archived';
// Check full image path
const fullImagePath1 = path.join(storageBase, row.path);
const fullImagePath2 = path.join(storageBase, 'events', eventStatusDir, row.path);
console.log('\nChecking full image paths:');
console.log(` Path 1: ${fullImagePath1} - ${fs.existsSync(fullImagePath1) ? 'EXISTS' : 'NOT FOUND'}`);
console.log(` Path 2: ${fullImagePath2} - ${fs.existsSync(fullImagePath2) ? 'EXISTS' : 'NOT FOUND'}`);
// Check thumbnail path
const thumbnailPath = path.join(storageBase, row.thumbnail_path);
console.log('\nChecking thumbnail path:');
console.log(` ${thumbnailPath} - ${fs.existsSync(thumbnailPath) ? 'EXISTS' : 'NOT FOUND'}`);
console.log('\n---\n');
});
}
// Close the database connection
db.close((err) => {
if (err) {
console.error('Error closing database:', err.message);
} else {
console.log('Database connection closed.');
}
});
});

Some files were not shown because too many files have changed in this diff Show More