Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc17e7d59d | |||
| f05ad87602 | |||
| 2efc74a687 | |||
| 85e7fbe73f | |||
| 0a21856a8d | |||
| 349e7c7eb1 | |||
| be07438915 | |||
| b31ae72153 | |||
| 1db08b1e9b | |||
| 237a3332cc | |||
| e2d0a83d51 | |||
| c546657285 | |||
| e8d5ee1a7b | |||
| 1761ebd531 | |||
| 5e5e98601f | |||
| 1cda80792b | |||
| 8740d5e618 | |||
| a619d52d17 | |||
| dd8cc14d30 | |||
| 3b7d723c2a | |||
| dc6252ff56 | |||
| 1d4e79a4f9 | |||
| 0b0e3e22d2 |
-280
@@ -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
|
||||
+35
-23
@@ -1,32 +1,44 @@
|
||||
# Production Environment Configuration Template
|
||||
# Copy this file to .env and fill in your values
|
||||
# PicPeak Production Configuration
|
||||
# Copy this file to .env and update with your values
|
||||
|
||||
# Application URLs
|
||||
ADMIN_URL=https://yourdomain.com
|
||||
FRONTEND_URL=https://yourdomain.com
|
||||
# Required: Security
|
||||
JWT_SECRET=CHANGE_THIS_TO_RANDOM_32_CHAR_STRING
|
||||
|
||||
# Security - CRITICAL: Generate a secure random JWT secret
|
||||
# You can generate one with: openssl rand -base64 32
|
||||
JWT_SECRET=your-secure-random-jwt-secret-here
|
||||
# Required: URLs (update with your domain)
|
||||
FRONTEND_URL=https://your-domain.com
|
||||
BACKEND_URL=https://your-domain.com
|
||||
ADMIN_URL=https://your-domain.com
|
||||
|
||||
# Database Configuration (PostgreSQL)
|
||||
DB_USER=picpeak
|
||||
DB_PASSWORD=your-secure-database-password
|
||||
DB_NAME=picpeak
|
||||
|
||||
# Email Configuration
|
||||
# Required: Email Settings
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=true
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
SMTP_FROM=your-email@gmail.com
|
||||
|
||||
# Umami Analytics (Optional)
|
||||
UMAMI_URL=https://analytics.yourdomain.com
|
||||
UMAMI_WEBSITE_ID=your-website-id
|
||||
UMAMI_HASH_SALT=your-random-hash-salt
|
||||
# Required: Initial Admin Account
|
||||
ADMIN_EMAIL=admin@your-domain.com
|
||||
ADMIN_PASSWORD=change-this-password
|
||||
|
||||
# First Admin User (for initial setup)
|
||||
# Run: docker-compose exec backend node scripts/create-admin.js --email admin@yourdomain.com
|
||||
ADMIN_EMAIL=admin@yourdomain.com
|
||||
# Database (PostgreSQL recommended for production)
|
||||
DATABASE_CLIENT=pg
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_NAME=picpeak
|
||||
DB_USER=picpeak
|
||||
DB_PASSWORD=secure-database-password
|
||||
|
||||
# Optional: Customization
|
||||
SITE_NAME=PicPeak
|
||||
DEFAULT_EXPIRATION_DAYS=30
|
||||
SESSION_TIMEOUT_MINUTES=60
|
||||
|
||||
# Optional: Analytics (Umami)
|
||||
VITE_UMAMI_URL=
|
||||
VITE_UMAMI_WEBSITE_ID=
|
||||
|
||||
# Advanced: Performance Tuning
|
||||
NODE_ENV=production
|
||||
BCRYPT_ROUNDS=12
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
@@ -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
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Mirror to GitHub (Archive Method)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Mirror using git archive
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Configure git
|
||||
git config --global user.name "Gitea Mirror Bot"
|
||||
git config --global user.email "bot@noreply.gitea.local"
|
||||
|
||||
# Copy gitattributes
|
||||
cp .gitattributes-github .gitattributes
|
||||
|
||||
# Create archive excluding files
|
||||
git archive --format=tar HEAD | tar -x -C /tmp/export
|
||||
|
||||
# Initialize new repo in export directory
|
||||
cd /tmp/export
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Mirror from Gitea: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
# Push to GitHub
|
||||
git remote add origin https://x-access-token:${GITHUB_TOKEN}@github.com/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME.git
|
||||
git push -f origin main
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Mirror to GitHub (Rsync Method)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare mirror directory
|
||||
run: |
|
||||
# Create mirror directory
|
||||
mkdir -p /tmp/github-mirror
|
||||
|
||||
# Use rsync to copy files, excluding sensitive ones
|
||||
rsync -av --exclude-from='.github-mirror-exclude' ./ /tmp/github-mirror/
|
||||
|
||||
- name: Push to GitHub
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
cd /tmp/github-mirror
|
||||
|
||||
# Initialize git repo
|
||||
git init
|
||||
git config user.name "Gitea Mirror Bot"
|
||||
git config user.email "bot@noreply.gitea.local"
|
||||
|
||||
# Add all files and commit
|
||||
git add .
|
||||
git commit -m "Mirror from Gitea: $(git --git-dir=$GITHUB_WORKSPACE/.git log -1 --format='%h %s')"
|
||||
|
||||
# Push to GitHub
|
||||
git remote add origin https://x-access-token:${GITHUB_TOKEN}@github.com/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME.git
|
||||
git push -f origin main
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Mirror to GitHub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Full history needed for mirroring
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.name "the-luap"
|
||||
git config --global user.email "paul-nothaft@hotmail.de"
|
||||
|
||||
- name: Create filtered branch
|
||||
run: |
|
||||
# Create a new branch for GitHub
|
||||
git checkout -b github-mirror
|
||||
|
||||
# Remove sensitive files/directories
|
||||
# Example: Remove .env files, private configs, etc.
|
||||
git rm -r --cached .env* || true
|
||||
git rm -r --cached backend/.env* || true
|
||||
git rm -r --cached frontend/.env* || true
|
||||
git rm -r --cached docker-compose.prod.yml || true
|
||||
git rm -r --cached .claudedocs/ || true
|
||||
git rm -r --cached backend/data/ || true
|
||||
git rm -r --cached backend/storage/ || true
|
||||
git rm -r --cached .gitea/ || true
|
||||
git rm -r --cached scripts/install-gitea-runner.sh || true
|
||||
git rm -r --cached .drone* || true
|
||||
git rm -r --cached .github-mirror-exclude || true
|
||||
git rm -r --cached .gitattributes-github || true
|
||||
git rm -r --cached photo-sharing-prd.md || true
|
||||
git rm -r --cached CLAUDE.md || true
|
||||
git rm -r --cached PRODUCTION_DEPLOYMENT_GUIDE.md || true
|
||||
git rm -r --cached logs/ || true
|
||||
|
||||
|
||||
# Commit the changes
|
||||
git commit -m "Remove sensitive files for GitHub mirror" || true
|
||||
|
||||
- name: Push to GitHub
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }}
|
||||
run: |
|
||||
# Add GitHub remote
|
||||
git remote add github https://x-access-token:${GITHUBTOKEN}@github.com/the-luap/picpeak.git
|
||||
|
||||
# Force push the filtered branch to GitHub main
|
||||
git push github github-mirror:main --force
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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! 🎉
|
||||
@@ -1,92 +0,0 @@
|
||||
# Deployment Guide - Traefik Production Setup
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to deploy PicPeak with an external Traefik reverse proxy for production use.
|
||||
|
||||
## Fixed Issues
|
||||
|
||||
1. **Database Migration**: Added missing `created_at` column to `email_queue` table
|
||||
2. **502 Bad Gateway**: Properly configured Traefik routing and backend accessibility
|
||||
3. **Health Checks**: Fixed health check endpoint imports and paths
|
||||
|
||||
## Deployment Steps
|
||||
|
||||
### 1. Update Environment Variables
|
||||
|
||||
Ensure your `.env` file has the correct URLs:
|
||||
```bash
|
||||
ADMIN_URL=https://picpeak.nothaft.cloud
|
||||
FRONTEND_URL=https://picpeak.nothaft.cloud
|
||||
```
|
||||
|
||||
### 2. Build Images
|
||||
|
||||
```bash
|
||||
# Build backend image
|
||||
docker build -t picpeak-backend:latest ./backend
|
||||
|
||||
# Build frontend image
|
||||
docker build -t picpeak-frontend:latest ./frontend \
|
||||
--build-arg VITE_API_URL=/api \
|
||||
--build-arg VITE_UMAMI_URL=${VITE_UMAMI_URL} \
|
||||
--build-arg VITE_UMAMI_WEBSITE_ID=${VITE_UMAMI_WEBSITE_ID}
|
||||
```
|
||||
|
||||
### 3. Deploy with Traefik
|
||||
|
||||
Use the new Traefik-specific compose file:
|
||||
```bash
|
||||
docker-compose -f docker-compose.traefik.yml up -d
|
||||
```
|
||||
|
||||
### 4. Verify Deployment
|
||||
|
||||
Check that all services are healthy:
|
||||
```bash
|
||||
# Check container status
|
||||
docker-compose -f docker-compose.traefik.yml ps
|
||||
|
||||
# Check backend health
|
||||
curl https://picpeak.nothaft.cloud/api/health
|
||||
|
||||
# Check logs
|
||||
docker-compose -f docker-compose.traefik.yml logs -f backend
|
||||
```
|
||||
|
||||
## Key Differences from Standard Deployment
|
||||
|
||||
1. **No Internal Nginx**: Traefik handles all routing externally
|
||||
2. **API Path Stripping**: Traefik strips `/api` prefix when forwarding to backend
|
||||
3. **Network Configuration**: Services join external `traefik` network
|
||||
4. **Health Checks**: Backend exposes `/health` endpoint (not `/api/health`)
|
||||
|
||||
## Why CI/CD Tests Pass But Production Fails
|
||||
|
||||
CI/CD tests typically:
|
||||
- Use in-memory or temporary databases with fresh migrations
|
||||
- Don't test through reverse proxy (direct API calls)
|
||||
- Don't run background services (email processor, etc.)
|
||||
- Have different network configurations
|
||||
|
||||
Production environment has:
|
||||
- Persistent database that may have migration state issues
|
||||
- Reverse proxy routing complexity
|
||||
- All background services running
|
||||
- Different security and network constraints
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 502 Bad Gateway
|
||||
- Check Traefik network connectivity: `docker network ls`
|
||||
- Verify backend is in traefik network: `docker inspect picpeak-backend`
|
||||
- Check Traefik logs: `docker logs traefik`
|
||||
|
||||
### Database Issues
|
||||
- Connect to database: `docker exec -it picpeak-db psql -U picpeak`
|
||||
- Check migration status: `SELECT * FROM migrations;`
|
||||
- Run migrations manually: `docker exec -it picpeak-backend npm run migrate:safe`
|
||||
|
||||
### Email Service Errors
|
||||
- Check email queue: `SELECT * FROM email_queue ORDER BY created_at DESC LIMIT 10;`
|
||||
- Monitor email processor: `docker logs picpeak-backend | grep "email"`
|
||||
+162
-288
@@ -1,346 +1,220 @@
|
||||
# PicPeak Deployment Guide
|
||||
# 🚀 PicPeak Deployment Guide
|
||||
|
||||
This guide covers deploying PicPeak for development and production environments.
|
||||
This guide will help you deploy PicPeak in production. The entire process takes about 10-15 minutes.
|
||||
|
||||
## Table of Contents
|
||||
- [Quick Start (Development)](#quick-start-development)
|
||||
- [Production Deployment](#production-deployment)
|
||||
- [Admin User Setup](#admin-user-setup)
|
||||
- [Configuration Reference](#configuration-reference)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
## 📋 Prerequisites
|
||||
|
||||
## Quick Start (Development)
|
||||
- A server with Docker and Docker Compose installed
|
||||
- A domain name (for SSL certificates)
|
||||
- SMTP credentials for sending emails
|
||||
- Basic command line knowledge
|
||||
|
||||
### 1. Clone and Setup
|
||||
## 🏃 Quick Deploy (Recommended)
|
||||
|
||||
### 1. Clone and Configure
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/picpeak.git
|
||||
# Clone the repository
|
||||
git clone https://github.com/the-luap/picpeak.git
|
||||
cd picpeak
|
||||
|
||||
# Copy environment template
|
||||
cp .env.example .env
|
||||
|
||||
# Start development environment
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
### 2. Access Services
|
||||
|
||||
- Frontend: http://localhost:3005
|
||||
- Backend API: http://localhost:3001
|
||||
- MailHog (email testing): http://localhost:8025
|
||||
|
||||
### 3. Create Admin User
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml exec backend node scripts/create-admin.js \
|
||||
--email admin@localhost \
|
||||
--username admin \
|
||||
--password admin123
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- Domain with DNS configured
|
||||
- SSL/TLS handled by reverse proxy (Traefik, Nginx, etc.)
|
||||
|
||||
### 1. Environment Setup
|
||||
|
||||
```bash
|
||||
# Copy production template
|
||||
cp .env.production.example .env
|
||||
|
||||
# Generate secure secrets
|
||||
# Generate a secure JWT secret
|
||||
echo "JWT_SECRET=$(openssl rand -base64 32)" >> .env
|
||||
echo "DB_PASSWORD=$(openssl rand -base64 24)" >> .env
|
||||
|
||||
# Edit configuration
|
||||
nano .env
|
||||
```
|
||||
|
||||
Edit `.env` with your configuration:
|
||||
### 2. Required Environment Variables
|
||||
|
||||
Edit your `.env` file with these essential settings:
|
||||
|
||||
```env
|
||||
# Your domain
|
||||
ADMIN_URL=https://yourdomain.com
|
||||
FRONTEND_URL=https://yourdomain.com
|
||||
# Application URLs
|
||||
FRONTEND_URL=https://your-domain.com
|
||||
BACKEND_URL=https://your-domain.com
|
||||
|
||||
# Database (PostgreSQL)
|
||||
DB_USER=picpeak
|
||||
DB_NAME=picpeak
|
||||
# DB_PASSWORD already generated above
|
||||
|
||||
# Email
|
||||
# Email Configuration (Required for notifications)
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=true
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
```
|
||||
SMTP_FROM=your-email@gmail.com
|
||||
|
||||
### 2. Frontend Configuration
|
||||
# Admin Configuration
|
||||
ADMIN_EMAIL=admin@your-domain.com
|
||||
ADMIN_PASSWORD=your-secure-password
|
||||
|
||||
```bash
|
||||
# Configure frontend for production
|
||||
echo "VITE_API_URL=/api" > frontend/.env.production
|
||||
# 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
|
||||
# Build and start services
|
||||
# Start all services
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# Check status
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
# Check logs
|
||||
docker-compose logs -f
|
||||
|
||||
# View logs
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
# Access your site at https://your-domain.com
|
||||
```
|
||||
|
||||
### 4. Deploy with Traefik
|
||||
## 🔧 Configuration Options
|
||||
|
||||
If using Traefik, create `docker-compose.override.yml`:
|
||||
### Storage Settings
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
```env
|
||||
# Storage paths (default: ./storage)
|
||||
STORAGE_PATH=./storage
|
||||
ARCHIVE_PATH=./storage/archives
|
||||
|
||||
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"
|
||||
networks:
|
||||
- traefik
|
||||
- picpeak
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
# Gallery expiration (days)
|
||||
DEFAULT_EXPIRATION_DAYS=30
|
||||
WARNING_DAYS_BEFORE_EXPIRY=7
|
||||
```
|
||||
|
||||
## Admin User Setup
|
||||
### Security Settings
|
||||
|
||||
### Create First Admin
|
||||
```env
|
||||
# Session timeout (minutes)
|
||||
SESSION_TIMEOUT=60
|
||||
|
||||
After deployment, create your admin user:
|
||||
# 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
|
||||
# Production
|
||||
docker-compose -f docker-compose.prod.yml exec backend node scripts/create-admin.js \
|
||||
--email admin@yourdomain.com \
|
||||
--username admin \
|
||||
--password yourSecurePassword
|
||||
# Backup database and photos
|
||||
./scripts/backup.sh
|
||||
|
||||
# Auto-generate password
|
||||
docker-compose -f docker-compose.prod.yml exec backend node scripts/create-admin.js \
|
||||
--email admin@yourdomain.com
|
||||
# Backups are stored in ./backups/
|
||||
```
|
||||
|
||||
The script will display:
|
||||
- ✅ Admin user created successfully!
|
||||
- Email: admin@yourdomain.com
|
||||
- Username: admin
|
||||
- Login URL: https://yourdomain.com/admin/login
|
||||
- Password: (save this if auto-generated!)
|
||||
|
||||
### Managing Admin Users
|
||||
|
||||
```bash
|
||||
# List admin users
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||
-c "SELECT id, username, email, is_active, last_login FROM admin_users;"
|
||||
|
||||
# Deactivate user
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||
-c "UPDATE admin_users SET is_active = false WHERE email = 'user@example.com';"
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Database Configuration
|
||||
|
||||
PicPeak automatically detects the environment and uses:
|
||||
- **Development**: SQLite (`./data/photo_sharing.db`)
|
||||
- **Production**: PostgreSQL (configured via environment variables)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
#### Required for Production
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `JWT_SECRET` | JWT signing key | `openssl rand -base64 32` |
|
||||
| `DB_PASSWORD` | PostgreSQL password | `openssl rand -base64 24` |
|
||||
| `ADMIN_URL` | Admin panel URL | `https://yourdomain.com` |
|
||||
| `FRONTEND_URL` | Frontend URL | `https://yourdomain.com` |
|
||||
| `EMAIL_FROM` | Sender email | `noreply@yourdomain.com` |
|
||||
|
||||
#### Email Configuration
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `SMTP_HOST` | SMTP server | `smtp.gmail.com` |
|
||||
| `SMTP_PORT` | SMTP port | `587` |
|
||||
| `SMTP_SECURE` | Use TLS | `true` |
|
||||
| `SMTP_USER` | SMTP username | `your-email@gmail.com` |
|
||||
| `SMTP_PASS` | SMTP password | App-specific password |
|
||||
|
||||
### Storage Paths
|
||||
|
||||
- Photos: `./storage/events/active/`
|
||||
- Archives: `./storage/events/archived/`
|
||||
- Thumbnails: `./storage/thumbnails/`
|
||||
- Uploads: `./storage/uploads/`
|
||||
|
||||
## Backup and Restore
|
||||
|
||||
### Backup Database
|
||||
|
||||
```bash
|
||||
# PostgreSQL backup
|
||||
docker-compose -f docker-compose.prod.yml exec db \
|
||||
pg_dump -U picpeak picpeak > backup-$(date +%Y%m%d).sql
|
||||
|
||||
# Backup storage
|
||||
tar -czf storage-backup-$(date +%Y%m%d).tar.gz ./storage
|
||||
```
|
||||
|
||||
### Restore Database
|
||||
|
||||
```bash
|
||||
# PostgreSQL restore
|
||||
docker-compose -f docker-compose.prod.yml exec -T db \
|
||||
psql -U picpeak picpeak < backup-20240115.sql
|
||||
|
||||
# Restore storage
|
||||
tar -xzf storage-backup-20240115.tar.gz
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Backend health
|
||||
curl https://yourdomain.com/api/health
|
||||
|
||||
# Frontend health
|
||||
curl https://yourdomain.com/health
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
# Last 100 lines
|
||||
docker-compose -f docker-compose.prod.yml logs --tail=100 backend
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Backend Won't Start
|
||||
|
||||
1. Check database connection:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml logs db
|
||||
```
|
||||
|
||||
2. Verify environment variables:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend env | grep DB_
|
||||
```
|
||||
|
||||
### Can't Login as Admin
|
||||
|
||||
1. Verify admin user exists:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||
-c "SELECT * FROM admin_users;"
|
||||
```
|
||||
|
||||
2. Reset admin password:
|
||||
```bash
|
||||
# Create new admin with different email
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
node scripts/create-admin.js --email newadmin@yourdomain.com
|
||||
```
|
||||
|
||||
### Photos Not Loading
|
||||
|
||||
1. Check file permissions:
|
||||
```bash
|
||||
ls -la ./storage/events/active/
|
||||
```
|
||||
|
||||
2. Verify nginx proxy configuration:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec frontend \
|
||||
cat /etc/nginx/conf.d/default.conf
|
||||
```
|
||||
|
||||
### Email Not Sending
|
||||
|
||||
1. Check email configuration:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend env | grep SMTP_
|
||||
```
|
||||
|
||||
2. View email queue:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend \
|
||||
psql postgresql://picpeak:$DB_PASSWORD@db:5432/picpeak \
|
||||
-c "SELECT * FROM email_queue WHERE status = 'failed';"
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Update Application
|
||||
### Update
|
||||
|
||||
```bash
|
||||
# Pull latest changes
|
||||
git pull
|
||||
|
||||
# Rebuild images
|
||||
docker-compose -f docker-compose.prod.yml build
|
||||
|
||||
# Restart services
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
# Rebuild and restart
|
||||
docker-compose -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
### Clean Up
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
# Remove unused images
|
||||
docker image prune -a
|
||||
# View all logs
|
||||
docker-compose logs
|
||||
|
||||
# Clean up logs
|
||||
docker-compose -f docker-compose.prod.yml logs --tail=0 -f
|
||||
|
||||
# Remove old archives
|
||||
find ./storage/events/archived -name "*.zip" -mtime +90 -delete
|
||||
# View specific service
|
||||
docker-compose logs backend
|
||||
docker-compose logs frontend
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
- [ ] Generated secure `JWT_SECRET`
|
||||
- [ ] Generated secure `DB_PASSWORD`
|
||||
- [ ] HTTPS enabled via reverse proxy
|
||||
- [ ] Changed default admin credentials
|
||||
- [ ] Configured real SMTP server
|
||||
- [ ] Set file permissions: `chmod 600 .env`
|
||||
- [ ] Firewall configured
|
||||
- [ ] Regular backups scheduled
|
||||
- [ ] Monitoring enabled
|
||||
### Common Issues
|
||||
|
||||
**Photos not appearing:**
|
||||
- Check storage permissions: `chmod -R 755 storage/`
|
||||
- Verify file watcher is running: `docker-compose logs backend | grep watcher`
|
||||
|
||||
**Email not sending:**
|
||||
- Test SMTP settings: Admin Panel → Settings → Email → Send Test
|
||||
- Check email queue: Admin Panel → System → Email Queue
|
||||
|
||||
**Can't access admin panel:**
|
||||
- Default login: Use email/password from `.env`
|
||||
- Reset password: `docker exec picpeak-backend npm run reset-admin`
|
||||
|
||||
### Health Check
|
||||
|
||||
```bash
|
||||
# Check service status
|
||||
docker-compose ps
|
||||
|
||||
# Test backend API
|
||||
curl https://your-domain.com/api/health
|
||||
|
||||
# Check disk space
|
||||
df -h storage/
|
||||
```
|
||||
|
||||
## 🐳 Alternative Deployment Methods
|
||||
|
||||
### Using Docker Swarm
|
||||
|
||||
For high availability deployments, see [Docker Swarm Setup](deploy/README.md).
|
||||
|
||||
### Manual Installation
|
||||
|
||||
If you prefer not to use Docker:
|
||||
|
||||
1. Install Node.js 18+
|
||||
2. Install PostgreSQL
|
||||
3. Clone repository
|
||||
4. Install dependencies: `npm install` in both `/backend` and `/frontend`
|
||||
5. Build frontend: `cd frontend && npm run build`
|
||||
6. Start services with PM2
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- 📘 [Documentation](https://github.com/the-luap/picpeak)
|
||||
- 🐛 [Report Issues](https://github.com/the-luap/picpeak/issues)
|
||||
- 💬 [Discussions](https://github.com/the-luap/picpeak/discussions)
|
||||
|
||||
---
|
||||
|
||||
**Need help?** Open an issue on GitHub and we'll assist you!
|
||||
@@ -1,111 +0,0 @@
|
||||
# Quick Fix for Migration Error
|
||||
|
||||
## Immediate Fix
|
||||
|
||||
The error "relation photo_categories already exists" occurs because the database already has tables but the migration tracking doesn't know they were applied.
|
||||
|
||||
### Option 1: Use Safe Migration Runner (Recommended)
|
||||
|
||||
Update your `docker-compose.prod.yml` to use the safe migration command:
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
# ... other config ...
|
||||
```
|
||||
|
||||
Then update the `wait-for-db.sh` (already done) to use `npm run migrate:safe` in production.
|
||||
|
||||
### Option 2: Quick Manual Fix
|
||||
|
||||
If you need to fix the running system immediately:
|
||||
|
||||
```bash
|
||||
# 1. Enter the backend container
|
||||
docker-compose -f docker-compose.prod.yml exec backend sh
|
||||
|
||||
# 2. Run the safe migration script
|
||||
npm run migrate:safe
|
||||
|
||||
# 3. If that fails, manually mark migrations as applied:
|
||||
docker-compose -f docker-compose.prod.yml exec db psql -U picpeak -d picpeak
|
||||
|
||||
# In PostgreSQL:
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
filename VARCHAR(255) UNIQUE NOT NULL,
|
||||
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Mark existing migrations as applied
|
||||
INSERT INTO migrations (filename) VALUES
|
||||
('init.js'),
|
||||
('004_add_categories_and_cms.js'),
|
||||
('006_add_photo_counter_to_categories.js'),
|
||||
('007_add_read_at_to_activity_logs.js'),
|
||||
('008_add_language_support_to_email_templates.js'),
|
||||
('009_update_german_email_templates.js'),
|
||||
('010_add_missing_email_templates.js'),
|
||||
('011_add_user_upload_settings.js'),
|
||||
('012_add_hero_photo_id.js'),
|
||||
('013_fix_email_links_and_date_format.js'),
|
||||
('014_add_default_welcome_message.js'),
|
||||
('014_add_host_name_to_events.js'),
|
||||
('015_add_login_attempts_table.js'),
|
||||
('016_add_auth_security_columns.js'),
|
||||
('017_add_token_revocation_tables.js')
|
||||
ON CONFLICT (filename) DO NOTHING;
|
||||
|
||||
\q
|
||||
```
|
||||
|
||||
### Option 3: Fresh Start (Nuclear Option)
|
||||
|
||||
If you don't have important data yet:
|
||||
|
||||
```bash
|
||||
# Stop everything
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
|
||||
# Remove database volume
|
||||
docker volume rm wedding-photo-sharing_postgres_data
|
||||
|
||||
# Start fresh
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
## Root Cause
|
||||
|
||||
The issue happens when:
|
||||
1. Database volume persists between deployments
|
||||
2. Migration tracking table gets out of sync
|
||||
3. The original migration runner doesn't check for existing tables
|
||||
|
||||
## Permanent Solution
|
||||
|
||||
The new safe migration runner (`migrate:safe`) handles this by:
|
||||
1. Checking if tables exist before creating them
|
||||
2. Catching "already exists" errors gracefully
|
||||
3. Auto-detecting existing schema and marking migrations as applied
|
||||
|
||||
## Next Steps
|
||||
|
||||
After fixing the migration issue:
|
||||
|
||||
1. Create admin user:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend node scripts/create-admin.js \
|
||||
--username admin \
|
||||
--email admin@yourdomain.com
|
||||
```
|
||||
|
||||
2. Check health:
|
||||
```bash
|
||||
curl http://yourdomain.com/api/health
|
||||
```
|
||||
|
||||
3. Monitor logs:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml logs -f backend
|
||||
```
|
||||
@@ -1,100 +0,0 @@
|
||||
# Production Deployment Fixes
|
||||
|
||||
This document describes the fixes applied to resolve production deployment issues in Docker.
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
### 1. Database Connection Error: "getaddrinfo ENOTFOUND postgres"
|
||||
**Problem**: The backend was trying to connect to hostname "postgres" but the database service is named "db" in docker-compose.
|
||||
**Solution**:
|
||||
- Updated `knexfile.js` to use correct default host "db" instead of "postgres"
|
||||
- Added `depends_on: db` to backend service in docker-compose.prod.yml
|
||||
|
||||
### 2. Backend Starting Before Database Ready
|
||||
**Problem**: Backend service started before PostgreSQL was ready, causing connection failures.
|
||||
**Solution**:
|
||||
- Created `wait-for-db.sh` script that waits for PostgreSQL to be ready
|
||||
- Updated Dockerfile to install postgresql-client and use the wait script
|
||||
- Script also runs migrations automatically on startup
|
||||
|
||||
### 3. Email Processor Initialization Failure
|
||||
**Problem**: Email processor tried to initialize on module load before database was available.
|
||||
**Solution**:
|
||||
- Modified `emailProcessor.js` to export initialization functions
|
||||
- Updated `server.js` to call initialization after database is ready
|
||||
- Added proper error handling for email service initialization
|
||||
|
||||
### 4. Missing Environment Variables
|
||||
**Problem**: Critical storage path environment variables were missing.
|
||||
**Solution**:
|
||||
- Added STORAGE_PATH, EVENTS_PATH, and ARCHIVE_PATH to docker-compose.prod.yml
|
||||
- Created `.env.example` documenting all required environment variables
|
||||
|
||||
### 5. Enhanced Health Check
|
||||
**Problem**: Basic health check didn't verify database connectivity.
|
||||
**Solution**:
|
||||
- Updated `/api/health` endpoint to check database connection
|
||||
- Returns proper HTTP 503 status when unhealthy
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **backend/knexfile.js** - Fixed production database defaults
|
||||
2. **backend/wait-for-db.sh** - Created database wait script
|
||||
3. **backend/Dockerfile** - Added postgresql-client and wait script
|
||||
4. **docker-compose.prod.yml** - Added dependencies and environment variables
|
||||
5. **backend/src/services/emailProcessor.js** - Disabled auto-initialization
|
||||
6. **backend/server.js** - Added email initialization and improved health check
|
||||
7. **backend/.env.example** - Created environment variable documentation
|
||||
|
||||
## Deployment Steps
|
||||
|
||||
1. Ensure all environment variables are set according to `.env.example`
|
||||
2. Build and deploy with docker-compose:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml build
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
3. The backend will now:
|
||||
- Wait for PostgreSQL to be ready
|
||||
- Run migrations automatically
|
||||
- Initialize all services in proper order
|
||||
- Provide health status at `/api/health`
|
||||
|
||||
## Verification
|
||||
|
||||
Check deployment health:
|
||||
```bash
|
||||
curl http://localhost/api/health
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"database": "connected",
|
||||
"timestamp": "2025-07-13T20:30:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Email Configuration
|
||||
|
||||
Email service requires configuration in the database. If email is not configured:
|
||||
- The service will log a warning but continue running
|
||||
- Emails will be queued but not sent
|
||||
- Configure email settings in the admin panel after deployment
|
||||
|
||||
## PostgreSQL Connection Fix
|
||||
|
||||
### Issue: "no pg_hba.conf entry for host"
|
||||
This error occurs when PostgreSQL requires SSL but the client connects without encryption.
|
||||
|
||||
### Solution:
|
||||
- Disabled SSL requirement for PostgreSQL in Docker environment (`ssl=off`)
|
||||
- Added proper authentication method (`scram-sha-256`)
|
||||
- This is acceptable for internal Docker networks where all traffic is isolated
|
||||
|
||||
### Security Note:
|
||||
For production deployments exposed to the internet:
|
||||
1. Use SSL certificates for PostgreSQL
|
||||
2. Or ensure the database is only accessible within the Docker network
|
||||
3. Never expose PostgreSQL port (5432) directly to the internet
|
||||
@@ -51,7 +51,7 @@ openssl rand -hex 32
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/yourusername/wedding-photo-sharing.git
|
||||
git clone https://github.com/the-luap/wedding-photo-sharing.git
|
||||
cd wedding-photo-sharing
|
||||
|
||||
# Create required directories
|
||||
|
||||
-130
@@ -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! 🎨
|
||||
@@ -1,32 +1,164 @@
|
||||
# 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.
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](https://nodejs.org/)
|
||||
[](https://reactjs.org/)
|
||||
|
||||
## 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
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
<details>
|
||||
<summary>View Gallery Examples</summary>
|
||||
|
||||
### Admin Dashboard
|
||||

|
||||
|
||||
### Client Gallery View
|
||||

|
||||
|
||||
### Mobile Experience
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## 🙏 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
@@ -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
@@ -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!)
|
||||
@@ -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
|
||||
@@ -1,280 +0,0 @@
|
||||
# Traefik Deployment Guide
|
||||
|
||||
This guide explains how to deploy the PicPeak application with Traefik as the reverse proxy.
|
||||
|
||||
## Overview
|
||||
|
||||
The application consists of:
|
||||
- **Frontend**: React app served by nginx (port 80)
|
||||
- **Backend**: Node.js API (port 3000)
|
||||
- **Database**: PostgreSQL (port 5432, internal only)
|
||||
|
||||
## Traefik Configuration
|
||||
|
||||
### 1. Docker Labels for Traefik
|
||||
|
||||
Add these labels to your `docker-compose.prod.yml` services:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
frontend:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.picpeak-frontend.rule=Host(`picpeak.yourdomain.com`)"
|
||||
- "traefik.http.routers.picpeak-frontend.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak-frontend.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak-frontend.loadbalancer.server.port=80"
|
||||
# Priority for catch-all route
|
||||
- "traefik.http.routers.picpeak-frontend.priority=1"
|
||||
|
||||
backend:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.picpeak-api.rule=Host(`picpeak.yourdomain.com`) && PathPrefix(`/api`)"
|
||||
- "traefik.http.routers.picpeak-api.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak-api.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak-api.loadbalancer.server.port=3000"
|
||||
# Higher priority for API routes
|
||||
- "traefik.http.routers.picpeak-api.priority=10"
|
||||
|
||||
# Additional routes for backend static files
|
||||
- "traefik.http.routers.picpeak-uploads.rule=Host(`picpeak.yourdomain.com`) && PathPrefix(`/uploads`)"
|
||||
- "traefik.http.routers.picpeak-uploads.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak-uploads.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.picpeak-uploads.service=picpeak-api"
|
||||
- "traefik.http.routers.picpeak-uploads.priority=10"
|
||||
|
||||
- "traefik.http.routers.picpeak-images.rule=Host(`picpeak.yourdomain.com`) && PathPrefix(`/images`)"
|
||||
- "traefik.http.routers.picpeak-images.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak-images.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.picpeak-images.service=picpeak-api"
|
||||
- "traefik.http.routers.picpeak-images.priority=10"
|
||||
```
|
||||
|
||||
### 2. Network Configuration
|
||||
|
||||
Ensure your services are on the Traefik network:
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
picpeak:
|
||||
external: false
|
||||
traefik:
|
||||
external: true
|
||||
|
||||
services:
|
||||
frontend:
|
||||
networks:
|
||||
- picpeak
|
||||
- traefik
|
||||
|
||||
backend:
|
||||
networks:
|
||||
- picpeak
|
||||
- traefik
|
||||
|
||||
db:
|
||||
networks:
|
||||
- picpeak # Don't expose to traefik
|
||||
```
|
||||
|
||||
### 3. Remove Nginx Service
|
||||
|
||||
Since you're using Traefik, remove the nginx service from `docker-compose.prod.yml`:
|
||||
|
||||
```yaml
|
||||
# Remove this entire service:
|
||||
# nginx:
|
||||
# image: nginx:alpine
|
||||
# ...
|
||||
```
|
||||
|
||||
## Frontend Configuration
|
||||
|
||||
The frontend is built with the API URL set to `/api`. This is important because:
|
||||
|
||||
1. All API calls will be relative to the same domain
|
||||
2. Traefik will route `/api/*` to the backend service
|
||||
3. No CORS issues since everything is on the same domain
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Ensure these are set correctly:
|
||||
|
||||
```bash
|
||||
# Backend needs to know the public URLs
|
||||
ADMIN_URL=https://picpeak.yourdomain.com
|
||||
FRONTEND_URL=https://picpeak.yourdomain.com
|
||||
|
||||
# Backend API is accessed via /api path
|
||||
API_URL=https://picpeak.yourdomain.com/api
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a complete `docker-compose.prod.yml` for Traefik:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
networks:
|
||||
picpeak:
|
||||
external: false
|
||||
traefik:
|
||||
external: true
|
||||
|
||||
services:
|
||||
backend:
|
||||
image: picpeak-backend:latest
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ADMIN_URL=https://picpeak.yourdomain.com
|
||||
- FRONTEND_URL=https://picpeak.yourdomain.com
|
||||
- DATABASE_CLIENT=pg
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
- DB_USER=${DB_USER:-picpeak}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_NAME=${DB_NAME:-picpeak}
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_SECURE=${SMTP_SECURE}
|
||||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASS=${SMTP_PASS}
|
||||
- EMAIL_FROM=${EMAIL_FROM}
|
||||
- STORAGE_PATH=/app/storage
|
||||
- EVENTS_PATH=/app/storage/events
|
||||
- ARCHIVE_PATH=/app/storage/events/archived
|
||||
volumes:
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- picpeak
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.picpeak-api.rule=Host(`picpeak.yourdomain.com`) && PathPrefix(`/api`)"
|
||||
- "traefik.http.routers.picpeak-api.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak-api.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak-api.loadbalancer.server.port=3000"
|
||||
- "traefik.http.routers.picpeak-api.priority=10"
|
||||
|
||||
- "traefik.http.routers.picpeak-uploads.rule=Host(`picpeak.yourdomain.com`) && PathPrefix(`/uploads`)"
|
||||
- "traefik.http.routers.picpeak-uploads.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak-uploads.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.picpeak-uploads.service=picpeak-api"
|
||||
- "traefik.http.routers.picpeak-uploads.priority=10"
|
||||
|
||||
- "traefik.http.routers.picpeak-images.rule=Host(`picpeak.yourdomain.com`) && PathPrefix(`/images`)"
|
||||
- "traefik.http.routers.picpeak-images.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak-images.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.picpeak-images.service=picpeak-api"
|
||||
- "traefik.http.routers.picpeak-images.priority=10"
|
||||
|
||||
frontend:
|
||||
image: picpeak-frontend:latest
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- VITE_API_URL=/api
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- picpeak
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.picpeak-frontend.rule=Host(`picpeak.yourdomain.com`)"
|
||||
- "traefik.http.routers.picpeak-frontend.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak-frontend.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak-frontend.loadbalancer.server.port=80"
|
||||
- "traefik.http.routers.picpeak-frontend.priority=1"
|
||||
|
||||
db:
|
||||
image: postgres:14-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=${DB_USER:-picpeak}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME:-picpeak}
|
||||
- POSTGRES_HOST_AUTH_METHOD=scram-sha-256
|
||||
- POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=trust
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- picpeak
|
||||
command: postgres -c ssl=off
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 502 Bad Gateway Errors
|
||||
|
||||
1. **Check if backend is running**:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
docker-compose -f docker-compose.prod.yml logs backend
|
||||
```
|
||||
|
||||
2. **Verify Traefik can reach the backend**:
|
||||
- Ensure both services are on the same Docker network
|
||||
- Check Traefik logs: `docker logs traefik`
|
||||
|
||||
3. **Check backend health**:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend curl http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
### Frontend Can't Reach API
|
||||
|
||||
1. **Verify API paths don't have double `/api`**:
|
||||
- Frontend should call `/auth/admin/login`, not `/api/auth/admin/login`
|
||||
- The base URL in axios should be `/api`
|
||||
|
||||
2. **Check browser console for actual URLs being called**
|
||||
|
||||
3. **Ensure Traefik routing rules are correct**:
|
||||
- API routes should have higher priority than frontend catch-all
|
||||
|
||||
### CORS Issues
|
||||
|
||||
Should not occur since everything is on the same domain. If you see CORS errors:
|
||||
1. Check that `FRONTEND_URL` and `ADMIN_URL` match your actual domain
|
||||
2. Ensure you're not mixing HTTP and HTTPS
|
||||
|
||||
## Testing the Setup
|
||||
|
||||
1. **Test API directly**:
|
||||
```bash
|
||||
curl https://picpeak.yourdomain.com/api/health
|
||||
```
|
||||
|
||||
2. **Test frontend**:
|
||||
```bash
|
||||
curl https://picpeak.yourdomain.com/
|
||||
```
|
||||
|
||||
3. **Test admin login**:
|
||||
- Navigate to https://picpeak.yourdomain.com/admin/login
|
||||
- Check browser console for any errors
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **SSL/TLS**: Traefik handles SSL termination, so the backend doesn't need SSL
|
||||
2. **Port Exposure**: Don't expose backend ports directly - let Traefik handle routing
|
||||
3. **Health Checks**: Configure Traefik health checks for better reliability
|
||||
4. **Rate Limiting**: Consider adding Traefik rate limiting middleware for API routes
|
||||
@@ -1,134 +0,0 @@
|
||||
# Traefik Troubleshooting Guide
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### 1. 404 Errors on API Routes
|
||||
|
||||
**Problem**: Getting 404 errors when accessing `/api/*` routes
|
||||
|
||||
**Causes**:
|
||||
- Traefik routing rules not properly configured
|
||||
- Backend container not healthy
|
||||
- Path stripping not working correctly
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check container health**:
|
||||
```bash
|
||||
docker ps # Check if backend is running
|
||||
docker logs picpeak-backend # Check for startup errors
|
||||
```
|
||||
|
||||
2. **Test backend directly**:
|
||||
```bash
|
||||
# Access backend container
|
||||
docker exec -it picpeak-backend sh
|
||||
|
||||
# Test health endpoint
|
||||
wget -O- http://localhost:3000/health
|
||||
|
||||
# Test public settings endpoint
|
||||
wget -O- http://localhost:3000/public/settings
|
||||
```
|
||||
|
||||
3. **Check Traefik routing**:
|
||||
```bash
|
||||
# Check if routes are registered in Traefik
|
||||
curl https://traefik.yourdomain.com/api/http/routers | jq '.[] | select(.rule | contains("picpeak"))'
|
||||
```
|
||||
|
||||
### 2. Backend Not Accessible Through Traefik
|
||||
|
||||
**Key Configuration Points**:
|
||||
|
||||
1. **Traefik Labels** (in deploy section):
|
||||
- `traefik.enable=true` - Enable Traefik for this container
|
||||
- `traefik.docker.network=proxy` - Specify which network Traefik should use
|
||||
- `traefik.http.routers.picpeak-backend.priority=100` - Higher priority for API routes
|
||||
|
||||
2. **Path Stripping**:
|
||||
- Frontend expects `/api/*` but backend serves routes without `/api` prefix
|
||||
- Middleware strips `/api` before forwarding to backend
|
||||
|
||||
3. **Network Configuration**:
|
||||
- Backend must be in both `picpeak` (internal) and `proxy` (Traefik) networks
|
||||
|
||||
### 3. Environment Variable Issues
|
||||
|
||||
**Critical Variables**:
|
||||
- `ADMIN_URL` and `FRONTEND_URL` must match your actual domain
|
||||
- These affect CORS configuration
|
||||
|
||||
**Example .env**:
|
||||
```env
|
||||
# URLs
|
||||
ADMIN_URL=https://picpeak.local.nothaft.cloud
|
||||
FRONTEND_URL=https://picpeak.local.nothaft.cloud
|
||||
|
||||
# Database
|
||||
DB_USER=picpeak
|
||||
DB_PASSWORD=your_secure_password
|
||||
DB_NAME=picpeak
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your_secure_jwt_secret
|
||||
|
||||
# Email (optional)
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=true
|
||||
SMTP_USER=noreply@example.com
|
||||
SMTP_PASS=smtp_password
|
||||
EMAIL_FROM=noreply@example.com
|
||||
```
|
||||
|
||||
### 4. Debugging Steps
|
||||
|
||||
1. **Check if backend is receiving requests**:
|
||||
```bash
|
||||
# Watch backend logs
|
||||
docker logs -f picpeak-backend
|
||||
|
||||
# Look for incoming requests when you try to access the admin page
|
||||
```
|
||||
|
||||
2. **Test API routes directly**:
|
||||
```bash
|
||||
# From outside
|
||||
curl -v https://picpeak.local.nothaft.cloud/api/public/settings
|
||||
|
||||
# Should see backend logs if request reaches container
|
||||
```
|
||||
|
||||
3. **Verify Traefik middleware**:
|
||||
```bash
|
||||
# Check if stripprefix middleware exists
|
||||
curl https://traefik.yourdomain.com/api/http/middlewares | jq '.[] | select(.name | contains("picpeak"))'
|
||||
```
|
||||
|
||||
### 5. Quick Fix Checklist
|
||||
|
||||
- [ ] Backend container is healthy (`docker ps`)
|
||||
- [ ] Backend is in both networks (`docker inspect picpeak-backend | grep -A 20 Networks`)
|
||||
- [ ] Traefik labels use correct network (`traefik.docker.network=proxy`)
|
||||
- [ ] Priority is set correctly (backend: 100, frontend: 10)
|
||||
- [ ] ADMIN_URL and FRONTEND_URL match your domain
|
||||
- [ ] Database is accessible from backend
|
||||
- [ ] Migrations have run successfully
|
||||
|
||||
### 6. Alternative Testing
|
||||
|
||||
If Traefik routing is problematic, test backend directly:
|
||||
|
||||
```bash
|
||||
# Port forward to test backend directly
|
||||
docker run --rm -it --network picpeak alpine/curl curl http://backend:3000/health
|
||||
|
||||
# Or expose backend port temporarily
|
||||
docker run -d --name picpeak-backend-test \
|
||||
--network picpeak \
|
||||
-p 3001:3000 \
|
||||
registry.local.nothaft.cloud/picpeak-backend:latest
|
||||
```
|
||||
|
||||
Then access http://localhost:3001/health to verify backend is working.
|
||||
Binary file not shown.
@@ -3,11 +3,20 @@ exports.up = async function(knex) {
|
||||
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',
|
||||
subject: 'Your Photo Gallery is Ready!',
|
||||
body_html: `<h2>Gallery Created Successfully</h2>
|
||||
[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>
|
||||
@@ -18,39 +27,39 @@ exports.up = async function(knex) {
|
||||
<li>Expires: {{expiry_date}}</li>
|
||||
</ul>
|
||||
<p>Share this link and password with your guests to allow them to view and download photos.</p>`,
|
||||
body_text: 'Gallery Created Successfully\n\nDear {{host_name}},\n\nYour photo gallery "{{event_name}}" has been created successfully!',
|
||||
[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',
|
||||
subject: 'Your Photo Gallery Expires Soon',
|
||||
body_html: `<h2>Gallery Expiring Soon</h2>
|
||||
[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>`,
|
||||
body_text: 'Gallery Expiring Soon\n\nDear {{host_name}},\n\nYour photo gallery "{{event_name}}" will expire in {{days_remaining}} days.',
|
||||
[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',
|
||||
subject: 'Your Photo Gallery Has Expired',
|
||||
body_html: `<h2>Gallery Expired</h2>
|
||||
[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>`,
|
||||
body_text: 'Gallery Expired\n\nDear {{host_name}},\n\nYour photo gallery "{{event_name}}" has expired and been archived.',
|
||||
[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',
|
||||
subject: 'Gallery Archive Complete',
|
||||
body_html: `<h2>Archive Complete</h2>
|
||||
[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>`,
|
||||
body_text: 'Archive Complete\n\nDear {{host_name}},\n\nYour photo gallery "{{event_name}}" has been successfully archived.',
|
||||
[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'])
|
||||
}
|
||||
];
|
||||
@@ -58,6 +67,20 @@ exports.up = async function(knex) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.26",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.26",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^5.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.26",
|
||||
"description": "Backend for PicPeak event photo sharing platform",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -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();
|
||||
@@ -0,0 +1,145 @@
|
||||
require('dotenv').config();
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function fixProductionIssues() {
|
||||
console.log('Fixing production database issues...\n');
|
||||
|
||||
try {
|
||||
// 1. Check and fix email_templates structure
|
||||
console.log('1. Checking email_templates structure:');
|
||||
const emailColumns = await db('email_templates').columnInfo();
|
||||
console.log('Current columns:', Object.keys(emailColumns));
|
||||
|
||||
// Check if we need to add basic columns back
|
||||
const hasSubject = 'subject' in emailColumns;
|
||||
const hasSubjectEn = 'subject_en' in emailColumns;
|
||||
|
||||
if (hasSubjectEn && !hasSubject) {
|
||||
console.log('Adding basic columns back to email_templates...');
|
||||
await db.schema.alterTable('email_templates', (table) => {
|
||||
table.string('subject');
|
||||
table.text('body_html');
|
||||
table.text('body_text');
|
||||
});
|
||||
|
||||
// Copy values from _en columns
|
||||
await db('email_templates').update({
|
||||
subject: db.raw('subject_en'),
|
||||
body_html: db.raw('body_html_en'),
|
||||
body_text: db.raw('body_text_en')
|
||||
});
|
||||
console.log('Basic columns added successfully');
|
||||
}
|
||||
|
||||
// 2. Ensure default templates exist
|
||||
console.log('\n2. Checking email templates:');
|
||||
const templateCount = await db('email_templates').count('* as count');
|
||||
console.log('Template count:', templateCount[0].count);
|
||||
|
||||
if (templateCount[0].count === 0) {
|
||||
console.log('No templates found, inserting defaults...');
|
||||
const defaultTemplates = [
|
||||
{
|
||||
template_key: 'gallery_created',
|
||||
subject: 'Your Photo Gallery is Ready!',
|
||||
body_html: '<h2>Gallery Created Successfully</h2>...',
|
||||
body_text: 'Gallery Created Successfully...',
|
||||
variables: JSON.stringify(['host_name', 'event_name', 'event_date', 'gallery_link', 'gallery_password', 'expiry_date'])
|
||||
},
|
||||
{
|
||||
template_key: 'expiration_warning',
|
||||
subject: 'Your Photo Gallery Expires Soon',
|
||||
body_html: '<h2>Gallery Expiring Soon</h2>...',
|
||||
body_text: 'Gallery Expiring Soon...',
|
||||
variables: JSON.stringify(['host_name', 'event_name', 'days_remaining', 'gallery_link'])
|
||||
},
|
||||
{
|
||||
template_key: 'gallery_expired',
|
||||
subject: 'Your Photo Gallery Has Expired',
|
||||
body_html: '<h2>Gallery Expired</h2>...',
|
||||
body_text: 'Gallery Expired...',
|
||||
variables: JSON.stringify(['host_name', 'event_name'])
|
||||
},
|
||||
{
|
||||
template_key: 'archive_complete',
|
||||
subject: 'Gallery Archive Complete',
|
||||
body_html: '<h2>Archive Complete</h2>...',
|
||||
body_text: 'Archive Complete...',
|
||||
variables: JSON.stringify(['host_name', 'event_name', 'archive_size'])
|
||||
}
|
||||
];
|
||||
|
||||
for (const template of defaultTemplates) {
|
||||
// Add language columns if they exist
|
||||
if (hasSubjectEn) {
|
||||
template.subject_en = template.subject;
|
||||
template.body_html_en = template.body_html;
|
||||
template.body_text_en = template.body_text;
|
||||
template.subject_de = template.subject;
|
||||
template.body_html_de = template.body_html;
|
||||
template.body_text_de = template.body_text;
|
||||
}
|
||||
|
||||
await db('email_templates').insert(template);
|
||||
}
|
||||
console.log('Default templates inserted');
|
||||
}
|
||||
|
||||
// 3. Check activity_logs structure
|
||||
console.log('\n3. Checking activity_logs structure:');
|
||||
const activityColumns = await db('activity_logs').columnInfo();
|
||||
console.log('Columns:', Object.keys(activityColumns));
|
||||
|
||||
// Check if read_at exists
|
||||
if (!('read_at' in activityColumns)) {
|
||||
console.log('Adding read_at column to activity_logs...');
|
||||
await db.schema.alterTable('activity_logs', (table) => {
|
||||
table.datetime('read_at').nullable();
|
||||
});
|
||||
console.log('read_at column added');
|
||||
}
|
||||
|
||||
// 4. Check and add CMS pages
|
||||
console.log('\n4. Checking CMS pages:');
|
||||
const cmsColumns = await db('cms_pages').columnInfo();
|
||||
console.log('CMS columns:', Object.keys(cmsColumns));
|
||||
|
||||
const impressum = await db('cms_pages').where('slug', 'impressum').first();
|
||||
const datenschutz = await db('cms_pages').where('slug', 'datenschutz').first();
|
||||
|
||||
if (!impressum) {
|
||||
console.log('Adding Impressum page...');
|
||||
await db('cms_pages').insert({
|
||||
slug: 'impressum',
|
||||
title_en: 'Legal Notice',
|
||||
title_de: 'Impressum',
|
||||
content_en: '<h1>Legal Notice</h1><p>Your legal information here...</p>',
|
||||
content_de: '<h1>Impressum</h1><p>Ihre rechtlichen Informationen hier...</p>',
|
||||
updated_at: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
if (!datenschutz) {
|
||||
console.log('Adding Datenschutz page...');
|
||||
await db('cms_pages').insert({
|
||||
slug: 'datenschutz',
|
||||
title_en: 'Privacy Policy',
|
||||
title_de: 'Datenschutzerklärung',
|
||||
content_en: '<h1>Privacy Policy</h1><p>Your privacy policy here...</p>',
|
||||
content_de: '<h1>Datenschutzerklärung</h1><p>Ihre Datenschutzerklärung hier...</p>',
|
||||
updated_at: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n✅ All fixes applied successfully!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fixing issues:', error);
|
||||
console.error('Stack:', error.stack);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
fixProductionIssues();
|
||||
@@ -28,6 +28,10 @@ const adminAuthRoutes = require('./src/routes/adminAuth');
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Trust proxy headers (required for Traefik/nginx)
|
||||
// Set to specific number of proxies or loopback to be more secure
|
||||
app.set('trust proxy', 'loopback, linklocal, uniquelocal');
|
||||
|
||||
// Security middleware with custom CSP
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
|
||||
@@ -7,6 +7,11 @@ const sessions = new Map();
|
||||
// Default session timeout (60 minutes)
|
||||
const DEFAULT_SESSION_TIMEOUT = 60 * 60 * 1000;
|
||||
|
||||
// Cache for session timeout setting
|
||||
let cachedTimeout = null;
|
||||
let cacheExpiry = 0;
|
||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// Clean up expired sessions every 5 minutes
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
@@ -18,20 +23,46 @@ setInterval(() => {
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
async function getSessionTimeout() {
|
||||
const now = Date.now();
|
||||
|
||||
// Return cached value if still valid
|
||||
if (cachedTimeout && now < cacheExpiry) {
|
||||
return cachedTimeout;
|
||||
}
|
||||
|
||||
try {
|
||||
const setting = await db('app_settings')
|
||||
.where('setting_key', 'security_session_timeout_minutes')
|
||||
.first();
|
||||
.first()
|
||||
.timeout(5000); // 5 second timeout
|
||||
|
||||
if (setting && setting.setting_value) {
|
||||
const minutes = parseInt(JSON.parse(setting.setting_value));
|
||||
return minutes * 60 * 1000; // Convert to milliseconds
|
||||
let value = setting.setting_value;
|
||||
// Handle both string and object values
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch (e) {
|
||||
// If it's not JSON, try to parse as number directly
|
||||
value = parseInt(value);
|
||||
}
|
||||
}
|
||||
const minutes = parseInt(value);
|
||||
if (!isNaN(minutes) && minutes > 0) {
|
||||
cachedTimeout = minutes * 60 * 1000; // Convert to milliseconds
|
||||
cacheExpiry = now + CACHE_DURATION;
|
||||
return cachedTimeout;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting session timeout:', error);
|
||||
// Only log if it's not a connection error (to avoid spam)
|
||||
if (error.code !== 'ECONNRESET' && !error.message?.includes('Connection terminated')) {
|
||||
console.error('Error getting session timeout:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_SESSION_TIMEOUT;
|
||||
// Use cached value if available, otherwise default
|
||||
return cachedTimeout || DEFAULT_SESSION_TIMEOUT;
|
||||
}
|
||||
|
||||
async function sessionTimeoutMiddleware(req, res, next) {
|
||||
|
||||
@@ -77,12 +77,14 @@ router.post('/', adminAuth, [
|
||||
}
|
||||
|
||||
// Create category
|
||||
const [categoryId] = await db('photo_categories').insert({
|
||||
const insertResult = await db('photo_categories').insert({
|
||||
name,
|
||||
slug: categorySlug,
|
||||
is_global,
|
||||
event_id: is_global ? null : event_id
|
||||
});
|
||||
}).returning('id');
|
||||
|
||||
const categoryId = insertResult[0]?.id || insertResult[0];
|
||||
|
||||
const category = await db('photo_categories').where('id', categoryId).first();
|
||||
|
||||
|
||||
@@ -122,7 +122,16 @@ router.get('/activity', adminAuth, async (req, res) => {
|
||||
actorType: activity.actor_type,
|
||||
actorName: activity.actor_name,
|
||||
eventName: activity.event_name,
|
||||
metadata: activity.metadata ? JSON.parse(activity.metadata) : {},
|
||||
metadata: (() => {
|
||||
try {
|
||||
if (!activity.metadata) return {};
|
||||
if (typeof activity.metadata === 'object') return activity.metadata;
|
||||
return JSON.parse(activity.metadata);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse metadata for activity:', activity.id, e.message);
|
||||
return {};
|
||||
}
|
||||
})(),
|
||||
createdAt: activity.created_at
|
||||
}));
|
||||
|
||||
|
||||
@@ -112,17 +112,44 @@ router.post('/test', adminAuth, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Email configuration not found. Please configure SMTP settings first.' });
|
||||
}
|
||||
|
||||
// Create transporter
|
||||
const transporter = nodemailer.createTransport({
|
||||
// Validate SMTP configuration
|
||||
if (!config.smtp_host || !config.smtp_port) {
|
||||
return res.status(400).json({
|
||||
error: 'Incomplete email configuration',
|
||||
details: 'SMTP host and port are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if password might be masked (this shouldn't happen when fetching from DB)
|
||||
if (config.smtp_pass === '********') {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid email configuration',
|
||||
details: 'SMTP password appears to be masked. Please reconfigure your email settings.'
|
||||
});
|
||||
}
|
||||
|
||||
// Create transporter with detailed logging
|
||||
const transportConfig = {
|
||||
host: config.smtp_host,
|
||||
port: config.smtp_port,
|
||||
secure: config.smtp_secure,
|
||||
auth: config.smtp_user ? {
|
||||
port: parseInt(config.smtp_port),
|
||||
secure: config.smtp_secure === true || config.smtp_secure === 1,
|
||||
auth: config.smtp_user && config.smtp_pass ? {
|
||||
user: config.smtp_user,
|
||||
pass: config.smtp_pass
|
||||
} : undefined
|
||||
} : undefined,
|
||||
logger: process.env.NODE_ENV === 'development',
|
||||
debug: process.env.NODE_ENV === 'development'
|
||||
};
|
||||
|
||||
console.log('Creating email transporter with config:', {
|
||||
host: transportConfig.host,
|
||||
port: transportConfig.port,
|
||||
secure: transportConfig.secure,
|
||||
auth: transportConfig.auth ? 'configured' : 'none'
|
||||
});
|
||||
|
||||
const transporter = nodemailer.createTransport(transportConfig);
|
||||
|
||||
// Send test email
|
||||
await transporter.sendMail({
|
||||
from: `${config.from_name} <${config.from_email}>`,
|
||||
@@ -145,9 +172,27 @@ router.post('/test', adminAuth, async (req, res) => {
|
||||
res.json({ message: 'Test email sent successfully' });
|
||||
} catch (error) {
|
||||
console.error('Test email error:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
|
||||
// Provide more specific error messages
|
||||
let errorMessage = 'Failed to send test email';
|
||||
let details = error.message;
|
||||
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
errorMessage = 'Failed to connect to SMTP server';
|
||||
details = 'Please check your SMTP host and port settings';
|
||||
} else if (error.code === 'EAUTH') {
|
||||
errorMessage = 'SMTP authentication failed';
|
||||
details = 'Please check your SMTP username and password';
|
||||
} else if (error.code === 'ESOCKET') {
|
||||
errorMessage = 'Network error';
|
||||
details = 'Could not establish connection to SMTP server';
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: 'Failed to send test email',
|
||||
details: error.message
|
||||
error: errorMessage,
|
||||
details: details,
|
||||
code: error.code
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -160,20 +205,44 @@ router.get('/templates', adminAuth, async (req, res) => {
|
||||
.orderBy('template_key');
|
||||
|
||||
// Parse variables JSON and format for multi-language support
|
||||
const formattedTemplates = templates.map(template => ({
|
||||
id: template.id,
|
||||
template_key: template.template_key,
|
||||
// English versions
|
||||
subject_en: template.subject_en || template.subject,
|
||||
body_html_en: template.body_html_en || template.body_html,
|
||||
body_text_en: template.body_text_en || template.body_text,
|
||||
// German versions
|
||||
subject_de: template.subject_de || template.subject_en || template.subject,
|
||||
body_html_de: template.body_html_de || template.body_html_en || template.body_html,
|
||||
body_text_de: template.body_text_de || template.body_text_en || template.body_text,
|
||||
variables: template.variables ? JSON.parse(template.variables) : [],
|
||||
updated_at: template.updated_at
|
||||
}));
|
||||
const formattedTemplates = templates.map(template => {
|
||||
const result = {
|
||||
id: template.id,
|
||||
template_key: template.template_key,
|
||||
variables: (() => {
|
||||
try {
|
||||
if (!template.variables) return [];
|
||||
if (typeof template.variables === 'object') return template.variables;
|
||||
return JSON.parse(template.variables);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse variables for template:', template.template_key, e.message);
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
updated_at: template.updated_at
|
||||
};
|
||||
|
||||
// Handle both old and new schema formats
|
||||
if (template.subject_en !== undefined) {
|
||||
// New schema with language columns
|
||||
result.subject_en = template.subject_en;
|
||||
result.body_html_en = template.body_html_en;
|
||||
result.body_text_en = template.body_text_en;
|
||||
result.subject_de = template.subject_de;
|
||||
result.body_html_de = template.body_html_de;
|
||||
result.body_text_de = template.body_text_de;
|
||||
} else {
|
||||
// Old schema - use basic columns for both languages
|
||||
result.subject_en = template.subject;
|
||||
result.body_html_en = template.body_html;
|
||||
result.body_text_en = template.body_text;
|
||||
result.subject_de = template.subject;
|
||||
result.body_html_de = template.body_html;
|
||||
result.body_text_de = template.body_text;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
res.json(formattedTemplates);
|
||||
} catch (error) {
|
||||
@@ -193,20 +262,43 @@ router.get('/templates/:key', adminAuth, async (req, res) => {
|
||||
return res.status(404).json({ error: 'Template not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
// Handle both old and new schema formats
|
||||
const response = {
|
||||
id: template.id,
|
||||
template_key: template.template_key,
|
||||
// English versions
|
||||
subject_en: template.subject_en || template.subject,
|
||||
body_html_en: template.body_html_en || template.body_html,
|
||||
body_text_en: template.body_text_en || template.body_text,
|
||||
// German versions
|
||||
subject_de: template.subject_de || template.subject_en || template.subject,
|
||||
body_html_de: template.body_html_de || template.body_html_en || template.body_html,
|
||||
body_text_de: template.body_text_de || template.body_text_en || template.body_text,
|
||||
variables: template.variables ? JSON.parse(template.variables) : [],
|
||||
variables: (() => {
|
||||
try {
|
||||
if (!template.variables) return [];
|
||||
if (typeof template.variables === 'object') return template.variables;
|
||||
return JSON.parse(template.variables);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse variables for template:', template.template_key, e.message);
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
updated_at: template.updated_at
|
||||
});
|
||||
};
|
||||
|
||||
// Check which columns exist and use them appropriately
|
||||
if (template.subject_en !== undefined) {
|
||||
// New schema with language columns
|
||||
response.subject_en = template.subject_en;
|
||||
response.body_html_en = template.body_html_en;
|
||||
response.body_text_en = template.body_text_en;
|
||||
response.subject_de = template.subject_de;
|
||||
response.body_html_de = template.body_html_de;
|
||||
response.body_text_de = template.body_text_de;
|
||||
} else {
|
||||
// Old schema - use basic columns for both languages
|
||||
response.subject_en = template.subject;
|
||||
response.body_html_en = template.body_html;
|
||||
response.body_text_en = template.body_text;
|
||||
response.subject_de = template.subject;
|
||||
response.body_html_de = template.body_html;
|
||||
response.body_text_de = template.body_text;
|
||||
}
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('Email template fetch error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch email template' });
|
||||
@@ -237,13 +329,39 @@ router.put('/templates/:key', [
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
// Only update provided fields
|
||||
if (subject_en !== undefined) updateData.subject_en = subject_en;
|
||||
if (subject_de !== undefined) updateData.subject_de = subject_de;
|
||||
if (body_html_en !== undefined) updateData.body_html_en = body_html_en;
|
||||
if (body_html_de !== undefined) updateData.body_html_de = body_html_de;
|
||||
if (body_text_en !== undefined) updateData.body_text_en = body_text_en || '';
|
||||
if (body_text_de !== undefined) updateData.body_text_de = body_text_de || '';
|
||||
// Check which columns exist in the database
|
||||
const template = await db('email_templates')
|
||||
.where('template_key', req.params.key)
|
||||
.first();
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({ error: 'Template not found' });
|
||||
}
|
||||
|
||||
// Determine schema type and update accordingly
|
||||
if (template.subject_en !== undefined) {
|
||||
// New schema with language columns
|
||||
if (subject_en !== undefined) updateData.subject_en = subject_en;
|
||||
if (subject_de !== undefined) updateData.subject_de = subject_de;
|
||||
if (body_html_en !== undefined) updateData.body_html_en = body_html_en;
|
||||
if (body_html_de !== undefined) updateData.body_html_de = body_html_de;
|
||||
if (body_text_en !== undefined) updateData.body_text_en = body_text_en || '';
|
||||
if (body_text_de !== undefined) updateData.body_text_de = body_text_de || '';
|
||||
|
||||
// Also update basic columns if they exist
|
||||
if (template.subject !== undefined) {
|
||||
updateData.subject = subject_en || updateData.subject_en;
|
||||
updateData.body_html = body_html_en || updateData.body_html_en;
|
||||
updateData.body_text = body_text_en || updateData.body_text_en || '';
|
||||
}
|
||||
} else {
|
||||
// Old schema - only update basic columns
|
||||
if (subject_en !== undefined) {
|
||||
updateData.subject = subject_en;
|
||||
updateData.body_html = body_html_en;
|
||||
updateData.body_text = body_text_en || '';
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await db('email_templates')
|
||||
.where('template_key', req.params.key)
|
||||
|
||||
@@ -92,7 +92,7 @@ router.post('/', adminAuth, [
|
||||
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
|
||||
|
||||
// Insert into database
|
||||
const [eventId] = await db('events').insert({
|
||||
const insertResult = await db('events').insert({
|
||||
slug,
|
||||
event_type,
|
||||
event_name,
|
||||
@@ -108,7 +108,10 @@ router.post('/', adminAuth, [
|
||||
created_at: new Date().toISOString(),
|
||||
allow_user_uploads,
|
||||
upload_category_id
|
||||
});
|
||||
}).returning('id');
|
||||
|
||||
// Handle both PostgreSQL (returns array of objects) and SQLite (returns array of IDs)
|
||||
const eventId = insertResult[0]?.id || insertResult[0];
|
||||
|
||||
// Log activity
|
||||
await logActivity('event_created',
|
||||
@@ -133,7 +136,9 @@ router.post('/', adminAuth, [
|
||||
gallery_password: password,
|
||||
expiry_date: await formatDate(expires_at, emailLang),
|
||||
welcome_message: welcome_message || ''
|
||||
})
|
||||
}),
|
||||
status: 'pending',
|
||||
created_at: new Date()
|
||||
// scheduled_at will use default value
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,16 @@ router.get('/', adminAuth, async (req, res) => {
|
||||
actorName: notification.actor_name,
|
||||
eventName: notification.event_name,
|
||||
eventId: notification.event_id,
|
||||
metadata: notification.metadata ? JSON.parse(notification.metadata) : {},
|
||||
metadata: (() => {
|
||||
try {
|
||||
if (!notification.metadata) return {};
|
||||
if (typeof notification.metadata === 'object') return notification.metadata;
|
||||
return JSON.parse(notification.metadata);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse metadata for notification:', notification.id, e.message);
|
||||
return {};
|
||||
}
|
||||
})(),
|
||||
createdAt: notification.created_at,
|
||||
readAt: notification.read_at,
|
||||
isRead: !!notification.read_at
|
||||
@@ -91,9 +100,13 @@ router.put('/read-all', adminAuth, async (req, res) => {
|
||||
// Delete old notifications (older than 30 days and read)
|
||||
router.delete('/clear-old', adminAuth, async (req, res) => {
|
||||
try {
|
||||
// Use database-agnostic date calculation
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const deletedCount = await db('activity_logs')
|
||||
.whereNotNull('read_at')
|
||||
.where('created_at', '<', db.raw("datetime('now', '-30 days')"))
|
||||
.where('created_at', '<', thirtyDaysAgo)
|
||||
.delete();
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -35,14 +35,30 @@ router.get('/version', adminAuth, async (req, res) => {
|
||||
// Get comprehensive system status
|
||||
router.get('/status', adminAuth, async (req, res) => {
|
||||
try {
|
||||
// Database size
|
||||
const dbPath = path.join(__dirname, '../../data/photo_sharing.db');
|
||||
// Database size - check if PostgreSQL or SQLite
|
||||
let dbSize = 0;
|
||||
try {
|
||||
const stats = await fs.stat(dbPath);
|
||||
dbSize = stats.size;
|
||||
} catch (error) {
|
||||
console.error('Error getting database size:', error);
|
||||
const dbClient = process.env.DATABASE_CLIENT || 'sqlite3';
|
||||
|
||||
if (dbClient === 'pg') {
|
||||
// PostgreSQL - query database size
|
||||
try {
|
||||
const dbName = process.env.DB_NAME || 'picpeak';
|
||||
const result = await db.raw(`
|
||||
SELECT pg_database_size(?) as size
|
||||
`, [dbName]);
|
||||
dbSize = result.rows[0]?.size || 0;
|
||||
} catch (error) {
|
||||
console.error('Error getting PostgreSQL database size:', error);
|
||||
}
|
||||
} else {
|
||||
// SQLite - check file size
|
||||
const dbPath = path.join(__dirname, '../../data/photo_sharing.db');
|
||||
try {
|
||||
const stats = await fs.stat(dbPath);
|
||||
dbSize = stats.size;
|
||||
} catch (error) {
|
||||
console.error('Error getting SQLite database size:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Count various entities
|
||||
|
||||
@@ -8,13 +8,13 @@ const logger = require('./logger');
|
||||
|
||||
// Configuration
|
||||
const PASSWORD_CONFIG = {
|
||||
minLength: 12,
|
||||
minLength: 8, // Reduced from 12 to 8 for better usability
|
||||
requireUppercase: true,
|
||||
requireLowercase: true,
|
||||
requireNumbers: true,
|
||||
requireSpecialChars: true,
|
||||
requireSpecialChars: false, // Made optional for gallery passwords
|
||||
preventCommonPasswords: true,
|
||||
minStrengthScore: 3, // zxcvbn score (0-4, where 3 is "good")
|
||||
minStrengthScore: 2, // Reduced from 3 to 2 (moderate strength)
|
||||
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS) || 12 // Configurable, default 12
|
||||
};
|
||||
|
||||
@@ -137,18 +137,15 @@ function validatePasswordInContext(password, context, userData = {}) {
|
||||
}
|
||||
}
|
||||
} else if (context === 'gallery') {
|
||||
// Gallery passwords can be slightly less strict
|
||||
// but still need to be secure
|
||||
if (result.score < 2) {
|
||||
// Gallery passwords can be more lenient for user convenience
|
||||
// Allow passwords with score >= 1 (weak but acceptable)
|
||||
if (result.score < 1) {
|
||||
result.valid = false;
|
||||
result.errors.push('Gallery passwords must have moderate strength or better');
|
||||
result.errors.push('Password is too simple. Please add more complexity');
|
||||
}
|
||||
|
||||
// Check password doesn't contain event name
|
||||
if (userData.eventName && password.toLowerCase().includes(userData.eventName.toLowerCase())) {
|
||||
result.valid = false;
|
||||
result.errors.push('Password must not contain the event name');
|
||||
}
|
||||
// Don't check for event name in password - allow date-based passwords
|
||||
// This allows passwords like "Sommer2025!" which users prefer
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,862 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Complete setup script to create ALL remaining files
|
||||
|
||||
echo "========================================="
|
||||
echo "PicPeak Platform Setup"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# Function to create directory if it doesn't exist
|
||||
create_dir() {
|
||||
if [ ! -d "$1" ]; then
|
||||
mkdir -p "$1"
|
||||
echo "Created directory: $1"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create all necessary directories
|
||||
echo "Creating directory structure..."
|
||||
create_dir "backend/src/services"
|
||||
create_dir "backend/src/utils"
|
||||
create_dir "backend/src/routes"
|
||||
create_dir "backend/migrations"
|
||||
create_dir "backend/scripts"
|
||||
create_dir "backend/__tests__"
|
||||
create_dir "frontend/public"
|
||||
create_dir "frontend/src/components"
|
||||
create_dir "frontend/src/contexts"
|
||||
create_dir "frontend/src/hooks"
|
||||
create_dir "frontend/src/pages/admin"
|
||||
create_dir "frontend/src/services"
|
||||
create_dir "frontend/src/config"
|
||||
create_dir "nginx/sites-enabled"
|
||||
create_dir "scripts"
|
||||
create_dir "storage/events/active"
|
||||
create_dir "storage/events/archived"
|
||||
create_dir "storage/thumbnails"
|
||||
create_dir "data"
|
||||
create_dir "logs"
|
||||
create_dir "certbot/conf"
|
||||
create_dir "certbot/www"
|
||||
|
||||
# Create .gitkeep files to preserve empty directories
|
||||
touch storage/events/active/.gitkeep
|
||||
touch storage/events/archived/.gitkeep
|
||||
touch storage/thumbnails/.gitkeep
|
||||
touch data/.gitkeep
|
||||
touch logs/.gitkeep
|
||||
|
||||
echo ""
|
||||
echo "Creating backend utilities..."
|
||||
|
||||
# Create helpers utility
|
||||
cat > backend/src/utils/helpers.js << 'EOF'
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
|
||||
function generateToken(length = 32) {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
function sanitizeFilename(filename) {
|
||||
const basename = path.basename(filename);
|
||||
return basename.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
}
|
||||
|
||||
function formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function generateSlug(text) {
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/[\s_-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
function daysBetween(date1, date2) {
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
const firstDate = new Date(date1);
|
||||
const secondDate = new Date(date2);
|
||||
const diffDays = Math.round(Math.abs((firstDate - secondDate) / oneDay));
|
||||
return diffDays;
|
||||
}
|
||||
|
||||
function isValidEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
function paginate(totalItems, currentPage = 1, pageSize = 20) {
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
const offset = (currentPage - 1) * pageSize;
|
||||
return {
|
||||
totalItems,
|
||||
currentPage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
offset,
|
||||
hasNext: currentPage < totalPages,
|
||||
hasPrev: currentPage > 1
|
||||
};
|
||||
}
|
||||
|
||||
function asyncHandler(fn) {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
}
|
||||
|
||||
function getClientIp(req) {
|
||||
return req.headers['x-forwarded-for']?.split(',')[0] ||
|
||||
req.headers['x-real-ip'] ||
|
||||
req.connection.remoteAddress;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateToken,
|
||||
sanitizeFilename,
|
||||
formatBytes,
|
||||
generateSlug,
|
||||
daysBetween,
|
||||
isValidEmail,
|
||||
paginate,
|
||||
asyncHandler,
|
||||
getClientIp
|
||||
};
|
||||
EOF
|
||||
|
||||
echo "Creating remaining backend routes..."
|
||||
|
||||
# Create admin routes
|
||||
cat > backend/src/routes/admin.js << 'EOF'
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const { db } = require('../database/db');
|
||||
const router = express.Router();
|
||||
|
||||
// Dashboard stats
|
||||
router.get('/stats', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const totalEvents = await db('events').count('id as count').first();
|
||||
const activeEvents = await db('events').where('is_active', true).count('id as count').first();
|
||||
const archivedEvents = await db('events').where('is_archived', true).count('id as count').first();
|
||||
const totalPhotos = await db('photos').count('id as count').first();
|
||||
|
||||
const upcomingExpirations = await db('events')
|
||||
.where('is_active', true)
|
||||
.where('expires_at', '<=', new Date(Date.now() + 7 * 24 * 60 * 60 * 1000))
|
||||
.orderBy('expires_at', 'asc')
|
||||
.limit(5);
|
||||
|
||||
const recentActivity = await db('access_logs')
|
||||
.join('events', 'access_logs.event_id', 'events.id')
|
||||
.select('access_logs.*', 'events.event_name')
|
||||
.orderBy('access_logs.timestamp', 'desc')
|
||||
.limit(10);
|
||||
|
||||
res.json({
|
||||
total_events: totalEvents.count,
|
||||
active_events: activeEvents.count,
|
||||
archived_events: archivedEvents.count,
|
||||
total_photos: totalPhotos.count,
|
||||
upcoming_expirations: upcomingExpirations,
|
||||
recent_activity: recentActivity
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch stats' });
|
||||
}
|
||||
});
|
||||
|
||||
// Email queue management
|
||||
router.get('/emails', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const emails = await db('email_queue')
|
||||
.join('events', 'email_queue.event_id', 'events.id')
|
||||
.select('email_queue.*', 'events.event_name')
|
||||
.orderBy('email_queue.scheduled_at', 'desc')
|
||||
.limit(50);
|
||||
|
||||
res.json(emails);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch emails' });
|
||||
}
|
||||
});
|
||||
|
||||
// Retry failed email
|
||||
router.post('/emails/:id/retry', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
await db('email_queue').where('id', id).update({
|
||||
status: 'pending',
|
||||
retry_count: 0,
|
||||
error_message: null
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to retry email' });
|
||||
}
|
||||
});
|
||||
|
||||
// Archive management
|
||||
router.get('/archives', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const archives = await db('events')
|
||||
.where('is_archived', true)
|
||||
.select('id', 'event_name', 'event_date', 'archive_path', 'archived_at')
|
||||
.orderBy('archived_at', 'desc');
|
||||
|
||||
res.json(archives);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch archives' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create admin user
|
||||
router.post('/users', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { username, email, password } = req.body;
|
||||
|
||||
const existing = await db('admin_users')
|
||||
.where('username', username)
|
||||
.orWhere('email', email)
|
||||
.first();
|
||||
|
||||
if (existing) {
|
||||
return res.status(400).json({ error: 'User already exists' });
|
||||
}
|
||||
|
||||
const password_hash = await bcrypt.hash(password, 10);
|
||||
|
||||
const [userId] = await db('admin_users').insert({
|
||||
username,
|
||||
email,
|
||||
password_hash
|
||||
});
|
||||
|
||||
res.json({ id: userId, username, email });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to create user' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
EOF
|
||||
|
||||
echo "Creating deployment scripts..."
|
||||
|
||||
# Create backup script
|
||||
cat > scripts/backup.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
BACKUP_DIR="/backup/photo-sharing"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_NAME="backup_${TIMESTAMP}"
|
||||
|
||||
mkdir -p "${BACKUP_DIR}/${BACKUP_NAME}"
|
||||
|
||||
echo "Starting backup..."
|
||||
|
||||
if [ -f data/photo_sharing.db ]; then
|
||||
echo "Backing up SQLite database..."
|
||||
cp data/photo_sharing.db "${BACKUP_DIR}/${BACKUP_NAME}/"
|
||||
else
|
||||
echo "Backing up PostgreSQL database..."
|
||||
docker-compose -f docker-compose.prod.yml exec -T db pg_dump -U photoapp photo_sharing > "${BACKUP_DIR}/${BACKUP_NAME}/database.sql"
|
||||
fi
|
||||
|
||||
echo "Backing up active events..."
|
||||
tar -czf "${BACKUP_DIR}/${BACKUP_NAME}/active_events.tar.gz" -C storage/events active/
|
||||
|
||||
cp .env "${BACKUP_DIR}/${BACKUP_NAME}/"
|
||||
|
||||
cat > "${BACKUP_DIR}/${BACKUP_NAME}/backup_info.txt" << EOFINFO
|
||||
Backup created: $(date)
|
||||
Database: $([ -f data/photo_sharing.db ] && echo "photo_sharing.db" || echo "database.sql")
|
||||
Active events: active_events.tar.gz
|
||||
Configuration: .env
|
||||
EOFINFO
|
||||
|
||||
cd "${BACKUP_DIR}"
|
||||
tar -czf "${BACKUP_NAME}.tar.gz" "${BACKUP_NAME}/"
|
||||
rm -rf "${BACKUP_NAME}/"
|
||||
|
||||
find "${BACKUP_DIR}" -name "backup_*.tar.gz" -mtime +30 -delete
|
||||
|
||||
echo "Backup completed: ${BACKUP_DIR}/${BACKUP_NAME}.tar.gz"
|
||||
EOF
|
||||
|
||||
chmod +x scripts/backup.sh
|
||||
|
||||
# Create monitoring script
|
||||
cat > scripts/monitoring.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
check_service() {
|
||||
SERVICE=$1
|
||||
if docker-compose -f docker-compose.prod.yml ps | grep -q "${SERVICE}.*Up"; then
|
||||
echo "✓ ${SERVICE} is running"
|
||||
return 0
|
||||
else
|
||||
echo "✗ ${SERVICE} is down!"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Service Health Check"
|
||||
echo "==================="
|
||||
|
||||
SERVICES_OK=true
|
||||
|
||||
check_service "backend" || SERVICES_OK=false
|
||||
check_service "frontend" || SERVICES_OK=false
|
||||
check_service "nginx" || SERVICES_OK=false
|
||||
|
||||
echo ""
|
||||
echo "Disk Usage:"
|
||||
df -h | grep -E '^/dev/' | awk '{print $6 ": " $5 " used"}'
|
||||
|
||||
FAILED_EMAILS=$(docker-compose -f docker-compose.prod.yml exec -T backend sqlite3 data/photo_sharing.db "SELECT COUNT(*) FROM email_queue WHERE status='failed' AND retry_count >= 3;" 2>/dev/null || echo "0")
|
||||
if [ "$FAILED_EMAILS" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "⚠️ Warning: $FAILED_EMAILS failed emails in queue"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Upcoming Expirations:"
|
||||
docker-compose -f docker-compose.prod.yml exec -T backend sqlite3 data/photo_sharing.db "SELECT event_name, date(expires_at) as expires FROM events WHERE is_active=1 AND expires_at <= datetime('now', '+7 days') ORDER BY expires_at;" 2>/dev/null || echo "No database connection"
|
||||
|
||||
if [ "$SERVICES_OK" = false ]; then
|
||||
echo ""
|
||||
echo "⚠️ Some services are down! Run 'docker-compose -f docker-compose.prod.yml up -d' to restart."
|
||||
exit 1
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x scripts/monitoring.sh
|
||||
|
||||
# Create SSL setup script
|
||||
cat > scripts/setup-ssl.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "SSL Certificate Setup"
|
||||
echo "===================="
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Please run install.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source .env
|
||||
|
||||
ADMIN_DOMAIN=$(echo $ADMIN_URL | sed 's|https://||')
|
||||
FRONTEND_DOMAIN=$(echo $FRONTEND_URL | sed 's|https://||')
|
||||
|
||||
if [ -z "$ADMIN_DOMAIN" ] || [ -z "$FRONTEND_DOMAIN" ]; then
|
||||
echo "Error: Please set ADMIN_URL and FRONTEND_URL in .env file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed -i "s/admin.photos.yourdomain.com/$ADMIN_DOMAIN/g" nginx/sites-enabled/default.conf
|
||||
sed -i "s/photos.yourdomain.com/$FRONTEND_DOMAIN/g" nginx/sites-enabled/default.conf
|
||||
|
||||
read -p "Enter email for Let's Encrypt notifications: " EMAIL
|
||||
|
||||
docker-compose -f docker-compose.prod.yml up -d nginx
|
||||
|
||||
sleep 5
|
||||
|
||||
echo "Obtaining SSL certificates for $ADMIN_DOMAIN and $FRONTEND_DOMAIN..."
|
||||
|
||||
docker-compose -f docker-compose.prod.yml run --rm certbot certonly \
|
||||
--webroot \
|
||||
--webroot-path=/var/www/certbot \
|
||||
--email $EMAIL \
|
||||
--agree-tos \
|
||||
--no-eff-email \
|
||||
-d $ADMIN_DOMAIN \
|
||||
-d $FRONTEND_DOMAIN
|
||||
|
||||
echo "SSL certificates obtained successfully!"
|
||||
EOF
|
||||
|
||||
chmod +x scripts/setup-ssl.sh
|
||||
|
||||
# Create update script
|
||||
cat > scripts/update.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
echo "Photo Sharing Platform - Update"
|
||||
echo "=============================="
|
||||
|
||||
echo "Creating backup before update..."
|
||||
./scripts/backup.sh
|
||||
|
||||
echo "Pulling latest changes..."
|
||||
git pull origin main
|
||||
|
||||
echo "Rebuilding services..."
|
||||
docker-compose -f docker-compose.prod.yml build
|
||||
|
||||
echo "Restarting services..."
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
echo "Running database migrations..."
|
||||
docker-compose -f docker-compose.prod.yml exec backend npm run migrate
|
||||
|
||||
echo "Update completed successfully!"
|
||||
EOF
|
||||
|
||||
chmod +x scripts/update.sh
|
||||
|
||||
echo ""
|
||||
echo "Creating frontend files..."
|
||||
|
||||
# Create frontend Dockerfile
|
||||
cat > frontend/Dockerfile << 'EOF'
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /app/build /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
EOF
|
||||
|
||||
# Create frontend nginx.conf
|
||||
cat > frontend/nginx.conf << 'EOF'
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /photos {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create minimal frontend files to get started
|
||||
cat > frontend/public/index.html << 'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Share your event photos securely" />
|
||||
<title>Photo Gallery</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
# Create basic frontend files
|
||||
cat > frontend/src/index.js << 'EOF'
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
EOF
|
||||
|
||||
cat > frontend/src/App.js << 'EOF'
|
||||
import React from 'react';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<h1>Photo Sharing Platform</h1>
|
||||
<p>Setup in progress. Please complete the frontend implementation.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
EOF
|
||||
|
||||
cat > frontend/src/index.css << 'EOF'
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create Tailwind config
|
||||
cat > frontend/tailwind.config.js << 'EOF'
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
wedding: {
|
||||
primary: '#d4a574',
|
||||
secondary: '#f3e5d0',
|
||||
accent: '#8b7355'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create postcss config
|
||||
cat > frontend/postcss.config.js << 'EOF'
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create nginx site config
|
||||
cat > nginx/sites-enabled/default.conf << 'EOF'
|
||||
# Redirect HTTP to HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name admin.photos.yourdomain.com photos.yourdomain.com;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# Admin backend
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name admin.photos.yourdomain.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/admin.photos.yourdomain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/admin.photos.yourdomain.com/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
client_max_body_size 100M;
|
||||
|
||||
location / {
|
||||
limit_req zone=general burst=20 nodelay;
|
||||
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /api/auth {
|
||||
limit_req zone=auth burst=5 nodelay;
|
||||
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# Public frontend
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name photos.yourdomain.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/photos.yourdomain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/photos.yourdomain.com/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
location / {
|
||||
limit_req zone=general burst=20 nodelay;
|
||||
proxy_pass http://frontend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /api {
|
||||
limit_req zone=general burst=20 nodelay;
|
||||
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /photos {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Cache images
|
||||
proxy_cache_valid 200 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create DEPLOYMENT.md
|
||||
cat > DEPLOYMENT.md << 'EOF'
|
||||
# Production Deployment Guide
|
||||
|
||||
## System Requirements
|
||||
|
||||
- Ubuntu 20.04+ or similar Linux distribution
|
||||
- 2GB RAM minimum (4GB recommended)
|
||||
- 20GB storage minimum
|
||||
- Docker and Docker Compose
|
||||
- Valid domain names with DNS configured
|
||||
|
||||
## Step-by-Step Deployment
|
||||
|
||||
### 1. Server Preparation
|
||||
|
||||
```bash
|
||||
# Update system
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# Install required packages
|
||||
sudo apt install -y git curl ufw
|
||||
|
||||
# Configure firewall
|
||||
sudo ufw allow 22/tcp
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 443/tcp
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
### 2. Clone and Install
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
cd /opt
|
||||
sudo git clone https://github.com/yourusername/photo-sharing-platform.git
|
||||
cd photo-sharing-platform
|
||||
|
||||
# Run installation script
|
||||
sudo ./scripts/install.sh
|
||||
```
|
||||
|
||||
### 3. Configuration
|
||||
|
||||
Edit `.env` file:
|
||||
```bash
|
||||
sudo nano .env
|
||||
```
|
||||
|
||||
Required settings:
|
||||
```env
|
||||
# URLs (use your actual domains)
|
||||
ADMIN_URL=https://admin.photos.yourdomain.com
|
||||
FRONTEND_URL=https://photos.yourdomain.com
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
### 4. SSL Certificate Setup
|
||||
|
||||
```bash
|
||||
# Configure SSL
|
||||
sudo ./scripts/setup-ssl.sh
|
||||
```
|
||||
|
||||
### 5. Start Services
|
||||
|
||||
```bash
|
||||
# Build and start all services
|
||||
docker-compose -f docker-compose.prod.yml build
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# Initialize database
|
||||
docker-compose -f docker-compose.prod.yml exec backend npm run migrate
|
||||
```
|
||||
|
||||
### 6. Verify Deployment
|
||||
|
||||
1. Check service status:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
```
|
||||
|
||||
2. View logs:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
```
|
||||
|
||||
3. Access sites:
|
||||
- Admin panel: https://admin.photos.yourdomain.com
|
||||
- Public gallery: https://photos.yourdomain.com
|
||||
|
||||
## Post-Deployment
|
||||
|
||||
### Configure Automatic Backups
|
||||
|
||||
```bash
|
||||
# Add to crontab
|
||||
sudo crontab -e
|
||||
|
||||
# Add this line for daily backups at 2 AM
|
||||
0 2 * * * /opt/photo-sharing-platform/scripts/backup.sh
|
||||
```
|
||||
|
||||
### Set Up Monitoring
|
||||
|
||||
```bash
|
||||
# Add health check to crontab
|
||||
*/5 * * * * /opt/photo-sharing-platform/scripts/monitoring.sh
|
||||
```
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
1. Change default admin password immediately
|
||||
2. Configure firewall rules
|
||||
3. Enable automatic security updates
|
||||
4. Monitor access logs regularly
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Services won't start
|
||||
```bash
|
||||
# Check logs
|
||||
docker-compose -f docker-compose.prod.yml logs backend
|
||||
docker-compose -f docker-compose.prod.yml logs frontend
|
||||
|
||||
# Restart services
|
||||
docker-compose -f docker-compose.prod.yml restart
|
||||
```
|
||||
|
||||
### Email not sending
|
||||
1. Check SMTP settings in `.env`
|
||||
2. View email queue in admin panel
|
||||
3. Check logs: `docker-compose logs backend | grep email`
|
||||
|
||||
### SSL certificate issues
|
||||
```bash
|
||||
# Renew certificates
|
||||
docker-compose -f docker-compose.prod.yml run --rm certbot renew
|
||||
```
|
||||
EOF
|
||||
|
||||
# Set all script permissions
|
||||
chmod +x scripts/*.sh
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "✅ Setup Complete!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "All core files have been created. The platform structure is ready."
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Install dependencies:"
|
||||
echo " cd backend && npm install"
|
||||
echo " cd ../frontend && npm install"
|
||||
echo ""
|
||||
echo "2. Create a .env file from .env.example:"
|
||||
echo " cp .env.example .env"
|
||||
echo " nano .env # Edit with your settings"
|
||||
echo ""
|
||||
echo "3. Start development environment:"
|
||||
echo " docker-compose up"
|
||||
echo ""
|
||||
echo "4. For production deployment:"
|
||||
echo " Follow the instructions in DEPLOYMENT.md"
|
||||
echo ""
|
||||
echo "Note: The frontend is a basic skeleton. You'll need to implement:"
|
||||
echo "- Authentication context (AuthContext.js)"
|
||||
echo "- Page components (Login, Gallery, Admin pages)"
|
||||
echo "- API service layer"
|
||||
echo "- UI components"
|
||||
echo ""
|
||||
echo "All backend functionality is complete and ready to use!"
|
||||
echo ""
|
||||
echo "Default admin credentials: admin / admin123 (change immediately!)"
|
||||
@@ -1,265 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
image: ${REGISTRY_URL}/photo-sharing-backend:${VERSION:-latest}
|
||||
networks:
|
||||
- photo-sharing
|
||||
- traefik-public
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- JWT_SECRET_FILE=/run/secrets/jwt_secret
|
||||
- ADMIN_URL=${ADMIN_URL}
|
||||
- FRONTEND_URL=${FRONTEND_URL}
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_SECURE=${SMTP_SECURE}
|
||||
- SMTP_USER_FILE=/run/secrets/smtp_user
|
||||
- SMTP_PASS_FILE=/run/secrets/smtp_pass
|
||||
- EMAIL_FROM=${EMAIL_FROM}
|
||||
- UMAMI_URL=${UMAMI_URL}
|
||||
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=${DB_NAME:-photo_sharing}
|
||||
- DB_USER_FILE=/run/secrets/db_user
|
||||
- DB_PASSWORD_FILE=/run/secrets/db_password
|
||||
secrets:
|
||||
- jwt_secret
|
||||
- smtp_user
|
||||
- smtp_pass
|
||||
- db_user
|
||||
- db_password
|
||||
volumes:
|
||||
- photo-storage:/app/storage
|
||||
- app-data:/app/data
|
||||
- app-logs:/app/logs
|
||||
deploy:
|
||||
replicas: 3
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
failure_action: rollback
|
||||
max_failure_ratio: 0.3
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1'
|
||||
memory: 512M
|
||||
reservations:
|
||||
cpus: '0.25'
|
||||
memory: 128M
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.constraint-label=traefik-public"
|
||||
- "traefik.http.routers.backend.rule=Host(`${BACKEND_HOST}`) && PathPrefix(`/api`)"
|
||||
- "traefik.http.routers.backend.entrypoints=https"
|
||||
- "traefik.http.routers.backend.tls=true"
|
||||
- "traefik.http.routers.backend.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.backend.loadbalancer.server.port=3000"
|
||||
- "traefik.http.services.backend.loadbalancer.healthcheck.path=/api/health"
|
||||
- "traefik.http.services.backend.loadbalancer.healthcheck.interval=10s"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
frontend:
|
||||
image: ${REGISTRY_URL}/photo-sharing-frontend:${VERSION:-latest}
|
||||
networks:
|
||||
- photo-sharing
|
||||
- traefik-public
|
||||
deploy:
|
||||
replicas: 2
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
failure_action: rollback
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.5'
|
||||
memory: 256M
|
||||
reservations:
|
||||
cpus: '0.1'
|
||||
memory: 64M
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.constraint-label=traefik-public"
|
||||
- "traefik.http.routers.frontend.rule=Host(`${FRONTEND_HOST}`)"
|
||||
- "traefik.http.routers.frontend.entrypoints=https"
|
||||
- "traefik.http.routers.frontend.tls=true"
|
||||
- "traefik.http.routers.frontend.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.frontend.loadbalancer.server.port=80"
|
||||
- "traefik.http.middlewares.frontend-compress.compress=true"
|
||||
- "traefik.http.routers.frontend.middlewares=frontend-compress"
|
||||
|
||||
db:
|
||||
image: postgres:14-alpine
|
||||
networks:
|
||||
- photo-sharing
|
||||
environment:
|
||||
- POSTGRES_USER_FILE=/run/secrets/db_user
|
||||
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
|
||||
- POSTGRES_DB=${DB_NAME:-photo_sharing}
|
||||
secrets:
|
||||
- db_user
|
||||
- db_password
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.db == true
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 1G
|
||||
reservations:
|
||||
cpus: '0.5'
|
||||
memory: 256M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Background workers as separate services for better control
|
||||
email-worker:
|
||||
image: ${REGISTRY_URL}/photo-sharing-backend:${VERSION:-latest}
|
||||
command: ["node", "src/services/emailService.js"]
|
||||
networks:
|
||||
- photo-sharing
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- JWT_SECRET_FILE=/run/secrets/jwt_secret
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_SECURE=${SMTP_SECURE}
|
||||
- SMTP_USER_FILE=/run/secrets/smtp_user
|
||||
- SMTP_PASS_FILE=/run/secrets/smtp_pass
|
||||
- EMAIL_FROM=${EMAIL_FROM}
|
||||
secrets:
|
||||
- jwt_secret
|
||||
- smtp_user
|
||||
- smtp_pass
|
||||
volumes:
|
||||
- app-data:/app/data
|
||||
- app-logs:/app/logs
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.5'
|
||||
memory: 256M
|
||||
|
||||
expiration-checker:
|
||||
image: ${REGISTRY_URL}/photo-sharing-backend:${VERSION:-latest}
|
||||
command: ["node", "src/services/expirationChecker.js"]
|
||||
networks:
|
||||
- photo-sharing
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
volumes:
|
||||
- photo-storage:/app/storage
|
||||
- app-data:/app/data
|
||||
- app-logs:/app/logs
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.5'
|
||||
memory: 256M
|
||||
|
||||
archive-worker:
|
||||
image: ${REGISTRY_URL}/photo-sharing-backend:${VERSION:-latest}
|
||||
command: ["node", "src/services/archiveService.js"]
|
||||
networks:
|
||||
- photo-sharing
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
volumes:
|
||||
- photo-storage:/app/storage
|
||||
- app-data:/app/data
|
||||
- app-logs:/app/logs
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1'
|
||||
memory: 512M
|
||||
|
||||
# Umami Analytics
|
||||
umami:
|
||||
image: ghcr.io/umami-software/umami:postgresql-latest
|
||||
networks:
|
||||
- photo-sharing
|
||||
- traefik-public
|
||||
environment:
|
||||
DATABASE_URL: postgresql://umami:${UMAMI_DB_PASSWORD}@db:5432/umami
|
||||
DATABASE_TYPE: postgresql
|
||||
HASH_SALT: ${UMAMI_HASH_SALT}
|
||||
depends_on:
|
||||
- db
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.constraint-label=traefik-public"
|
||||
- "traefik.http.routers.umami.rule=Host(`${UMAMI_HOST}`)"
|
||||
- "traefik.http.routers.umami.entrypoints=https"
|
||||
- "traefik.http.routers.umami.tls=true"
|
||||
- "traefik.http.routers.umami.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.umami.loadbalancer.server.port=3000"
|
||||
|
||||
networks:
|
||||
photo-sharing:
|
||||
driver: overlay
|
||||
attachable: true
|
||||
traefik-public:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
driver: local
|
||||
photo-storage:
|
||||
driver: local
|
||||
app-data:
|
||||
driver: local
|
||||
app-logs:
|
||||
driver: local
|
||||
|
||||
secrets:
|
||||
jwt_secret:
|
||||
external: true
|
||||
smtp_user:
|
||||
external: true
|
||||
smtp_pass:
|
||||
external: true
|
||||
db_user:
|
||||
external: true
|
||||
db_password:
|
||||
external: true
|
||||
@@ -1,189 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
networks:
|
||||
- monitoring
|
||||
- traefik-public
|
||||
volumes:
|
||||
- prometheus-data:/prometheus
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||||
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||||
- '--web.enable-lifecycle'
|
||||
- '--storage.tsdb.retention.time=30d'
|
||||
deploy:
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.monitoring == true
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
cpus: '1'
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.http.routers.prometheus.rule=Host(`prometheus.${DOMAIN}`)"
|
||||
- "traefik.http.routers.prometheus.entrypoints=https"
|
||||
- "traefik.http.routers.prometheus.tls=true"
|
||||
- "traefik.http.routers.prometheus.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.prometheus.middlewares=admin-auth"
|
||||
- "traefik.http.services.prometheus.loadbalancer.server.port=9090"
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
networks:
|
||||
- monitoring
|
||||
- traefik-public
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=${GRAFANA_USER:-admin}
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- GF_SERVER_ROOT_URL=https://grafana.${DOMAIN}
|
||||
- GF_SMTP_ENABLED=true
|
||||
- GF_SMTP_HOST=${SMTP_HOST}:${SMTP_PORT}
|
||||
- GF_SMTP_USER=${SMTP_USER}
|
||||
- GF_SMTP_PASSWORD=${SMTP_PASS}
|
||||
- GF_SMTP_FROM_ADDRESS=${EMAIL_FROM}
|
||||
deploy:
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.monitoring == true
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: '0.5'
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.http.routers.grafana.rule=Host(`grafana.${DOMAIN}`)"
|
||||
- "traefik.http.routers.grafana.entrypoints=https"
|
||||
- "traefik.http.routers.grafana.tls=true"
|
||||
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
|
||||
|
||||
loki:
|
||||
image: grafana/loki:latest
|
||||
networks:
|
||||
- monitoring
|
||||
volumes:
|
||||
- loki-data:/loki
|
||||
- ./loki-config.yml:/etc/loki/config.yml:ro
|
||||
command: -config.file=/etc/loki/config.yml
|
||||
deploy:
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.monitoring == true
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
cpus: '1'
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:latest
|
||||
networks:
|
||||
- monitoring
|
||||
volumes:
|
||||
- /var/log:/var/log:ro
|
||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||
- ./promtail-config.yml:/etc/promtail/config.yml:ro
|
||||
command: -config.file=/etc/promtail/config.yml
|
||||
deploy:
|
||||
mode: global
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
|
||||
node-exporter:
|
||||
image: prom/node-exporter:latest
|
||||
networks:
|
||||
- monitoring
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/rootfs:ro
|
||||
command:
|
||||
- '--path.procfs=/host/proc'
|
||||
- '--path.sysfs=/host/sys'
|
||||
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
|
||||
deploy:
|
||||
mode: global
|
||||
resources:
|
||||
limits:
|
||||
memory: 128M
|
||||
cpus: '0.1'
|
||||
|
||||
cadvisor:
|
||||
image: gcr.io/cadvisor/cadvisor:latest
|
||||
networks:
|
||||
- monitoring
|
||||
volumes:
|
||||
- /:/rootfs:ro
|
||||
- /var/run:/var/run:ro
|
||||
- /sys:/sys:ro
|
||||
- /var/lib/docker/:/var/lib/docker:ro
|
||||
- /dev/disk/:/dev/disk:ro
|
||||
privileged: true
|
||||
deploy:
|
||||
mode: global
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
|
||||
alertmanager:
|
||||
image: prom/alertmanager:latest
|
||||
networks:
|
||||
- monitoring
|
||||
- traefik-public
|
||||
volumes:
|
||||
- alertmanager-data:/alertmanager
|
||||
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
|
||||
command:
|
||||
- '--config.file=/etc/alertmanager/alertmanager.yml'
|
||||
- '--storage.path=/alertmanager'
|
||||
deploy:
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.monitoring == true
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.http.routers.alertmanager.rule=Host(`alerts.${DOMAIN}`)"
|
||||
- "traefik.http.routers.alertmanager.entrypoints=https"
|
||||
- "traefik.http.routers.alertmanager.tls=true"
|
||||
- "traefik.http.routers.alertmanager.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.alertmanager.middlewares=admin-auth"
|
||||
- "traefik.http.services.alertmanager.loadbalancer.server.port=9093"
|
||||
|
||||
networks:
|
||||
monitoring:
|
||||
external: true
|
||||
traefik-public:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
prometheus-data:
|
||||
driver: local
|
||||
grafana-data:
|
||||
driver: local
|
||||
loki-data:
|
||||
driver: local
|
||||
alertmanager-data:
|
||||
driver: local
|
||||
@@ -1,65 +0,0 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
external_labels:
|
||||
monitor: 'photo-sharing'
|
||||
environment: 'production'
|
||||
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets: ['alertmanager:9093']
|
||||
|
||||
rule_files:
|
||||
- '/etc/prometheus/alerts/*.yml'
|
||||
|
||||
scrape_configs:
|
||||
# Prometheus itself
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
# Node Exporter
|
||||
- job_name: 'node-exporter'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'tasks.node-exporter'
|
||||
type: 'A'
|
||||
port: 9100
|
||||
|
||||
# Docker containers
|
||||
- job_name: 'cadvisor'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'tasks.cadvisor'
|
||||
type: 'A'
|
||||
port: 8080
|
||||
|
||||
# Traefik
|
||||
- job_name: 'traefik'
|
||||
static_configs:
|
||||
- targets: ['traefik:8082']
|
||||
|
||||
# Photo Sharing Backend
|
||||
- job_name: 'photo-sharing-backend'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'tasks.photo-sharing_backend'
|
||||
type: 'A'
|
||||
port: 3000
|
||||
metrics_path: '/api/metrics'
|
||||
|
||||
# PostgreSQL
|
||||
- job_name: 'postgres'
|
||||
static_configs:
|
||||
- targets: ['photo-sharing_db:9187']
|
||||
|
||||
# Loki
|
||||
- job_name: 'loki'
|
||||
static_configs:
|
||||
- targets: ['loki:3100']
|
||||
|
||||
# Grafana
|
||||
- job_name: 'grafana'
|
||||
static_configs:
|
||||
- targets: ['grafana:3000']
|
||||
@@ -1,134 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}Photo Sharing Platform Backup Script${NC}"
|
||||
echo "===================================="
|
||||
|
||||
# Configuration
|
||||
BACKUP_DIR="/opt/photo-sharing/backup"
|
||||
STACK_NAME="photo-sharing"
|
||||
RETENTION_DAYS=30
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
BACKUP_NAME="backup-${TIMESTAMP}"
|
||||
|
||||
# Create backup directory
|
||||
mkdir -p $BACKUP_DIR/$BACKUP_NAME
|
||||
|
||||
# Function to check if service is running
|
||||
check_service() {
|
||||
local service=$1
|
||||
if docker service ps ${STACK_NAME}_${service} --format "{{.CurrentState}}" | grep -q "Running"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Backup database
|
||||
echo -e "${GREEN}Backing up database...${NC}"
|
||||
if check_service "db"; then
|
||||
DB_CONTAINER=$(docker ps -q -f name=${STACK_NAME}_db -f status=running | head -1)
|
||||
if [ ! -z "$DB_CONTAINER" ]; then
|
||||
docker exec $DB_CONTAINER pg_dumpall -U postgres > $BACKUP_DIR/$BACKUP_NAME/database.sql
|
||||
echo -e "${GREEN}Database backup completed${NC}"
|
||||
else
|
||||
echo -e "${RED}Database container not found${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}Database service not running, skipping...${NC}"
|
||||
fi
|
||||
|
||||
# Backup photos
|
||||
echo -e "${GREEN}Backing up photos...${NC}"
|
||||
if [ -d "/opt/photo-sharing/storage" ]; then
|
||||
tar -czf $BACKUP_DIR/$BACKUP_NAME/photos.tar.gz -C /opt/photo-sharing storage/
|
||||
echo -e "${GREEN}Photos backup completed${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Photos directory not found, skipping...${NC}"
|
||||
fi
|
||||
|
||||
# Backup application data
|
||||
echo -e "${GREEN}Backing up application data...${NC}"
|
||||
if [ -d "/opt/photo-sharing/data" ]; then
|
||||
tar -czf $BACKUP_DIR/$BACKUP_NAME/app-data.tar.gz -C /opt/photo-sharing data/
|
||||
echo -e "${GREEN}Application data backup completed${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Application data directory not found, skipping...${NC}"
|
||||
fi
|
||||
|
||||
# Backup Docker volumes
|
||||
echo -e "${GREEN}Backing up Docker volumes...${NC}"
|
||||
for volume in $(docker volume ls -q | grep ${STACK_NAME}); do
|
||||
echo "Backing up volume: $volume"
|
||||
docker run --rm \
|
||||
-v $volume:/data \
|
||||
-v $BACKUP_DIR/$BACKUP_NAME:/backup \
|
||||
alpine tar -czf /backup/volume-${volume}.tar.gz -C /data .
|
||||
done
|
||||
|
||||
# Backup configurations
|
||||
echo -e "${GREEN}Backing up configurations...${NC}"
|
||||
if [ -f "../../.env.production" ]; then
|
||||
cp ../../.env.production $BACKUP_DIR/$BACKUP_NAME/
|
||||
fi
|
||||
|
||||
# Export Docker secrets (encrypted)
|
||||
echo -e "${GREEN}Exporting Docker secrets info...${NC}"
|
||||
docker secret ls --filter "label=com.docker.stack.namespace=$STACK_NAME" > $BACKUP_DIR/$BACKUP_NAME/secrets-list.txt
|
||||
|
||||
# Create backup manifest
|
||||
echo -e "${GREEN}Creating backup manifest...${NC}"
|
||||
cat > $BACKUP_DIR/$BACKUP_NAME/manifest.json << EOF
|
||||
{
|
||||
"timestamp": "$TIMESTAMP",
|
||||
"stack_name": "$STACK_NAME",
|
||||
"hostname": "$(hostname)",
|
||||
"docker_version": "$(docker version --format '{{.Server.Version}}')",
|
||||
"services": $(docker service ls --filter "label=com.docker.stack.namespace=$STACK_NAME" --format '{{json .}}' | jq -s .),
|
||||
"backup_contents": [
|
||||
"database.sql",
|
||||
"photos.tar.gz",
|
||||
"app-data.tar.gz",
|
||||
"volume-*.tar.gz",
|
||||
".env.production",
|
||||
"secrets-list.txt"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Compress entire backup
|
||||
echo -e "${GREEN}Compressing backup...${NC}"
|
||||
cd $BACKUP_DIR
|
||||
tar -czf ${BACKUP_NAME}.tar.gz $BACKUP_NAME/
|
||||
rm -rf $BACKUP_NAME/
|
||||
|
||||
# Upload to S3 (optional)
|
||||
if [ ! -z "$S3_BACKUP_BUCKET" ] && command -v aws &> /dev/null; then
|
||||
echo -e "${GREEN}Uploading to S3...${NC}"
|
||||
aws s3 cp ${BACKUP_NAME}.tar.gz s3://${S3_BACKUP_BUCKET}/photo-sharing/
|
||||
fi
|
||||
|
||||
# Clean up old backups
|
||||
echo -e "${GREEN}Cleaning up old backups...${NC}"
|
||||
find $BACKUP_DIR -name "backup-*.tar.gz" -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
# Show backup summary
|
||||
BACKUP_SIZE=$(du -h $BACKUP_DIR/${BACKUP_NAME}.tar.gz | cut -f1)
|
||||
echo ""
|
||||
echo -e "${GREEN}Backup completed successfully!${NC}"
|
||||
echo -e "Backup file: $BACKUP_DIR/${BACKUP_NAME}.tar.gz"
|
||||
echo -e "Backup size: $BACKUP_SIZE"
|
||||
echo -e "Retention: $RETENTION_DAYS days"
|
||||
|
||||
# Verify backup
|
||||
echo ""
|
||||
echo -e "${GREEN}Verifying backup...${NC}"
|
||||
tar -tzf $BACKUP_DIR/${BACKUP_NAME}.tar.gz | head -10
|
||||
echo "..."
|
||||
echo -e "${GREEN}Backup verification complete${NC}"
|
||||
@@ -1,111 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}Docker Secrets Creation Script${NC}"
|
||||
echo "==============================="
|
||||
|
||||
# Function to create or update a secret
|
||||
create_secret() {
|
||||
local secret_name=$1
|
||||
local secret_value=$2
|
||||
|
||||
# Check if secret exists
|
||||
if docker secret ls | grep -q $secret_name; then
|
||||
echo -e "${YELLOW}Secret '$secret_name' already exists. Skipping...${NC}"
|
||||
else
|
||||
echo "$secret_value" | docker secret create $secret_name -
|
||||
echo -e "${GREEN}Created secret: $secret_name${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to generate random password
|
||||
generate_password() {
|
||||
openssl rand -base64 32 | tr -d "=+/" | cut -c1-25
|
||||
}
|
||||
|
||||
# Check if in swarm mode
|
||||
if ! docker info | grep -q "Swarm: active"; then
|
||||
echo -e "${RED}Docker is not in swarm mode. Run init-swarm.sh first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load environment variables if .env.production exists
|
||||
if [ -f "../../.env.production" ]; then
|
||||
echo -e "${GREEN}Loading environment variables from .env.production${NC}"
|
||||
source ../../.env.production
|
||||
fi
|
||||
|
||||
# JWT Secret
|
||||
if [ -z "$JWT_SECRET" ]; then
|
||||
JWT_SECRET=$(generate_password)
|
||||
echo -e "${YELLOW}Generated JWT_SECRET: $JWT_SECRET${NC}"
|
||||
fi
|
||||
create_secret "jwt_secret" "$JWT_SECRET"
|
||||
|
||||
# Database credentials
|
||||
if [ -z "$DB_USER" ]; then
|
||||
DB_USER="photoapp"
|
||||
fi
|
||||
if [ -z "$DB_PASSWORD" ]; then
|
||||
DB_PASSWORD=$(generate_password)
|
||||
echo -e "${YELLOW}Generated DB_PASSWORD: $DB_PASSWORD${NC}"
|
||||
fi
|
||||
create_secret "db_user" "$DB_USER"
|
||||
create_secret "db_password" "$DB_PASSWORD"
|
||||
|
||||
# SMTP credentials
|
||||
if [ -z "$SMTP_USER" ]; then
|
||||
read -p "Enter SMTP username: " SMTP_USER
|
||||
fi
|
||||
if [ -z "$SMTP_PASS" ]; then
|
||||
read -sp "Enter SMTP password: " SMTP_PASS
|
||||
echo
|
||||
fi
|
||||
create_secret "smtp_user" "$SMTP_USER"
|
||||
create_secret "smtp_pass" "$SMTP_PASS"
|
||||
|
||||
# Traefik dashboard auth (username:password)
|
||||
if [ -z "$TRAEFIK_USER" ]; then
|
||||
TRAEFIK_USER="admin"
|
||||
fi
|
||||
if [ -z "$TRAEFIK_PASSWORD" ]; then
|
||||
TRAEFIK_PASSWORD=$(generate_password)
|
||||
echo -e "${YELLOW}Generated TRAEFIK_PASSWORD: $TRAEFIK_PASSWORD${NC}"
|
||||
fi
|
||||
# Generate htpasswd format
|
||||
TRAEFIK_AUTH=$(docker run --rm httpd:alpine htpasswd -nb $TRAEFIK_USER $TRAEFIK_PASSWORD)
|
||||
create_secret "traefik_dashboard_auth" "$TRAEFIK_AUTH"
|
||||
|
||||
# OAuth secrets (optional)
|
||||
if [ ! -z "$OAUTH_CLIENT_SECRET" ]; then
|
||||
create_secret "oauth_client_secret" "$OAUTH_CLIENT_SECRET"
|
||||
fi
|
||||
|
||||
if [ ! -z "$OAUTH_SECRET" ]; then
|
||||
create_secret "oauth_secret" "$OAUTH_SECRET"
|
||||
fi
|
||||
|
||||
# Drone CI secrets
|
||||
if [ ! -z "$DRONE_RPC_SECRET" ]; then
|
||||
create_secret "drone_rpc_secret" "$DRONE_RPC_SECRET"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Secrets creation complete!${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Important: Save these generated values in a secure location:${NC}"
|
||||
echo "JWT_SECRET=$JWT_SECRET"
|
||||
echo "DB_PASSWORD=$DB_PASSWORD"
|
||||
echo "TRAEFIK_USER=$TRAEFIK_USER"
|
||||
echo "TRAEFIK_PASSWORD=$TRAEFIK_PASSWORD"
|
||||
echo ""
|
||||
echo -e "${GREEN}Next steps:${NC}"
|
||||
echo "1. Update .env.production with the generated values"
|
||||
echo "2. Deploy Traefik: ./deploy-traefik.sh"
|
||||
echo "3. Deploy the application: ./deploy.sh"
|
||||
@@ -1,196 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}PicPeak Deployment Script${NC}"
|
||||
echo "========================================"
|
||||
|
||||
# Default values
|
||||
STACK_NAME="picpeak"
|
||||
ENV_FILE="../../.env.production"
|
||||
REGISTRY_URL="${REGISTRY_URL:-}"
|
||||
VERSION="${VERSION:-latest}"
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--env)
|
||||
ENV_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--registry)
|
||||
REGISTRY_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--stack-name)
|
||||
STACK_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: $0 [options]"
|
||||
echo "Options:"
|
||||
echo " --env FILE Path to environment file (default: ../../.env.production)"
|
||||
echo " --registry URL Docker registry URL"
|
||||
echo " --version VERSION Image version to deploy (default: latest)"
|
||||
echo " --stack-name NAME Stack name (default: picpeak)"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $1${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check if Docker is in swarm mode
|
||||
if ! docker info | grep -q "Swarm: active"; then
|
||||
echo -e "${RED}Docker is not in swarm mode. Run init-swarm.sh first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if environment file exists
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo -e "${RED}Environment file not found: $ENV_FILE${NC}"
|
||||
echo "Please create it from .env.production.example"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load environment variables
|
||||
echo -e "${GREEN}Loading environment variables...${NC}"
|
||||
set -a
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
# Export deployment variables
|
||||
export REGISTRY_URL
|
||||
export VERSION
|
||||
|
||||
# Validate required environment variables
|
||||
required_vars=(
|
||||
"FRONTEND_HOST"
|
||||
"BACKEND_HOST"
|
||||
"ADMIN_URL"
|
||||
"FRONTEND_URL"
|
||||
"ACME_EMAIL"
|
||||
"DB_NAME"
|
||||
"EMAIL_FROM"
|
||||
)
|
||||
|
||||
echo -e "${GREEN}Validating configuration...${NC}"
|
||||
for var in "${required_vars[@]}"; do
|
||||
if [ -z "${!var}" ]; then
|
||||
echo -e "${RED}Missing required environment variable: $var${NC}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if Traefik is running
|
||||
if ! docker service ls | grep -q "traefik_traefik"; then
|
||||
echo -e "${YELLOW}Traefik is not running. Deploy it first with:${NC}"
|
||||
echo "cd ../traefik && docker stack deploy -c docker-compose.traefik.yml traefik"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if secrets exist
|
||||
echo -e "${GREEN}Checking Docker secrets...${NC}"
|
||||
required_secrets=(
|
||||
"jwt_secret"
|
||||
"db_user"
|
||||
"db_password"
|
||||
"smtp_user"
|
||||
"smtp_pass"
|
||||
)
|
||||
|
||||
for secret in "${required_secrets[@]}"; do
|
||||
if ! docker secret ls | grep -q $secret; then
|
||||
echo -e "${RED}Missing required secret: $secret${NC}"
|
||||
echo "Run create-secrets.sh first"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Pull latest images if registry is specified
|
||||
if [ ! -z "$REGISTRY_URL" ]; then
|
||||
echo -e "${GREEN}Pulling latest images...${NC}"
|
||||
docker pull ${REGISTRY_URL}/photo-sharing-backend:${VERSION} || true
|
||||
docker pull ${REGISTRY_URL}/photo-sharing-frontend:${VERSION} || true
|
||||
fi
|
||||
|
||||
# Deploy the stack
|
||||
echo -e "${GREEN}Deploying stack: $STACK_NAME${NC}"
|
||||
echo -e "${BLUE}Version: $VERSION${NC}"
|
||||
echo -e "${BLUE}Registry: ${REGISTRY_URL:-local}${NC}"
|
||||
|
||||
cd ..
|
||||
docker stack deploy \
|
||||
-c docker-stack.yml \
|
||||
--with-registry-auth \
|
||||
$STACK_NAME
|
||||
|
||||
# Wait for services to start
|
||||
echo -e "${GREEN}Waiting for services to start...${NC}"
|
||||
sleep 10
|
||||
|
||||
# Check service status
|
||||
echo -e "${GREEN}Service status:${NC}"
|
||||
docker service ls --filter "label=com.docker.stack.namespace=$STACK_NAME"
|
||||
|
||||
# Wait for database to be ready
|
||||
echo -e "${GREEN}Waiting for database to be ready...${NC}"
|
||||
max_attempts=30
|
||||
attempt=1
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if docker exec $(docker ps -q -f name=${STACK_NAME}_db) pg_isready -U postgres > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}Database is ready!${NC}"
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
if [ $attempt -gt $max_attempts ]; then
|
||||
echo -e "${RED}Database failed to start in time${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run database migrations
|
||||
echo -e "${GREEN}Running database migrations...${NC}"
|
||||
sleep 5
|
||||
docker exec $(docker ps -q -f name=${STACK_NAME}_backend -f status=running | head -1) npm run migrate || {
|
||||
echo -e "${YELLOW}Migration failed. This might be normal if migrations already ran.${NC}"
|
||||
}
|
||||
|
||||
# Show deployment information
|
||||
echo ""
|
||||
echo -e "${GREEN}Deployment complete!${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}Access URLs:${NC}"
|
||||
echo "Frontend: https://${FRONTEND_HOST}"
|
||||
echo "Backend API: https://${BACKEND_HOST}/api"
|
||||
if [ ! -z "$UMAMI_HOST" ]; then
|
||||
echo "Analytics: https://${UMAMI_HOST}"
|
||||
fi
|
||||
if [ ! -z "$TRAEFIK_HOST" ]; then
|
||||
echo "Traefik Dashboard: https://${TRAEFIK_HOST}/dashboard/"
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${BLUE}Useful commands:${NC}"
|
||||
echo "View logs: docker service logs ${STACK_NAME}_backend"
|
||||
echo "Scale service: docker service scale ${STACK_NAME}_backend=5"
|
||||
echo "Update service: docker service update ${STACK_NAME}_backend"
|
||||
echo "Remove stack: docker stack rm $STACK_NAME"
|
||||
echo ""
|
||||
echo -e "${GREEN}Health check:${NC}"
|
||||
curl -s -o /dev/null -w "Frontend: %{http_code}\n" https://${FRONTEND_HOST}/health || true
|
||||
curl -s -o /dev/null -w "Backend: %{http_code}\n" https://${BACKEND_HOST}/api/health || true
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}Docker Swarm Initialization Script${NC}"
|
||||
echo "======================================"
|
||||
|
||||
# Check if running as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo -e "${RED}This script must be run as root${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker is installed
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo -e "${RED}Docker is not installed. Please install Docker first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if already in swarm mode
|
||||
if docker info | grep -q "Swarm: active"; then
|
||||
echo -e "${YELLOW}This node is already part of a swarm.${NC}"
|
||||
docker node ls
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Initialize swarm
|
||||
echo -e "${GREEN}Initializing Docker Swarm...${NC}"
|
||||
ADVERTISE_ADDR=${1:-$(hostname -I | awk '{print $1}')}
|
||||
docker swarm init --advertise-addr $ADVERTISE_ADDR
|
||||
|
||||
# Create overlay networks
|
||||
echo -e "${GREEN}Creating overlay networks...${NC}"
|
||||
docker network create --driver overlay --attachable traefik-public || true
|
||||
docker network create --driver overlay --attachable monitoring || true
|
||||
|
||||
# Label the node
|
||||
echo -e "${GREEN}Labeling manager node...${NC}"
|
||||
NODE_ID=$(docker info -f '{{.Swarm.NodeID}}')
|
||||
docker node update --label-add db=true $NODE_ID
|
||||
docker node update --label-add monitoring=true $NODE_ID
|
||||
|
||||
# Create required directories
|
||||
echo -e "${GREEN}Creating required directories...${NC}"
|
||||
mkdir -p /opt/photo-sharing/{storage,data,logs,backup}
|
||||
mkdir -p /opt/traefik/letsencrypt
|
||||
mkdir -p /opt/monitoring/{prometheus,grafana,loki}
|
||||
|
||||
# Set permissions
|
||||
chown -R 1000:1000 /opt/photo-sharing
|
||||
chmod -R 755 /opt/photo-sharing
|
||||
|
||||
echo -e "${GREEN}Swarm initialization complete!${NC}"
|
||||
echo ""
|
||||
echo "Manager join token:"
|
||||
docker swarm join-token manager
|
||||
echo ""
|
||||
echo "Worker join token:"
|
||||
docker swarm join-token worker
|
||||
echo ""
|
||||
echo -e "${GREEN}Next steps:${NC}"
|
||||
echo "1. Join worker nodes using the token above"
|
||||
echo "2. Create secrets using create-secrets.sh"
|
||||
echo "3. Deploy Traefik using deploy-traefik.sh"
|
||||
echo "4. Deploy the application stack using deploy.sh"
|
||||
@@ -1,124 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v2.10
|
||||
ports:
|
||||
- target: 80
|
||||
published: 80
|
||||
mode: host
|
||||
- target: 443
|
||||
published: 443
|
||||
mode: host
|
||||
networks:
|
||||
- traefik-public
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- traefik-certificates:/letsencrypt
|
||||
environment:
|
||||
- TRAEFIK_API=true
|
||||
- TRAEFIK_API_DASHBOARD=true
|
||||
- TRAEFIK_API_DEBUG=false
|
||||
- TRAEFIK_LOG_LEVEL=INFO
|
||||
- TRAEFIK_PROVIDERS_DOCKER=true
|
||||
- TRAEFIK_PROVIDERS_DOCKER_SWARMMODE=true
|
||||
- TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT=false
|
||||
- TRAEFIK_PROVIDERS_DOCKER_NETWORK=traefik-public
|
||||
- TRAEFIK_ENTRYPOINTS_HTTP_ADDRESS=:80
|
||||
- TRAEFIK_ENTRYPOINTS_HTTPS_ADDRESS=:443
|
||||
# Redirect HTTP to HTTPS
|
||||
- TRAEFIK_ENTRYPOINTS_HTTP_HTTP_REDIRECTIONS_ENTRYPOINT_TO=https
|
||||
- TRAEFIK_ENTRYPOINTS_HTTP_HTTP_REDIRECTIONS_ENTRYPOINT_SCHEME=https
|
||||
# Let's Encrypt
|
||||
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=${ACME_EMAIL}
|
||||
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE=/letsencrypt/acme.json
|
||||
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE=true
|
||||
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE_ENTRYPOINT=http
|
||||
# Enable metrics
|
||||
- TRAEFIK_METRICS_PROMETHEUS=true
|
||||
- TRAEFIK_METRICS_PROMETHEUS_ENTRYPOINT=metrics
|
||||
- TRAEFIK_ENTRYPOINTS_METRICS_ADDRESS=:8082
|
||||
deploy:
|
||||
mode: global
|
||||
placement:
|
||||
constraints:
|
||||
- node.role == manager
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.constraint-label=traefik-public"
|
||||
# Dashboard
|
||||
- "traefik.http.routers.traefik-dashboard.rule=Host(`${TRAEFIK_HOST}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
|
||||
- "traefik.http.routers.traefik-dashboard.entrypoints=https"
|
||||
- "traefik.http.routers.traefik-dashboard.tls=true"
|
||||
- "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.traefik-dashboard.service=api@internal"
|
||||
- "traefik.http.routers.traefik-dashboard.middlewares=admin-auth"
|
||||
# Basic auth for dashboard
|
||||
- "traefik.http.middlewares.admin-auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}"
|
||||
# Global redirect to https
|
||||
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
|
||||
# Security headers
|
||||
- "traefik.http.middlewares.security-headers.headers.frameDeny=true"
|
||||
- "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
|
||||
- "traefik.http.middlewares.security-headers.headers.browserXssFilter=true"
|
||||
- "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000"
|
||||
- "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true"
|
||||
- "traefik.http.middlewares.security-headers.headers.stsPreload=true"
|
||||
# Rate limiting
|
||||
- "traefik.http.middlewares.rate-limit.ratelimit.average=100"
|
||||
- "traefik.http.middlewares.rate-limit.ratelimit.burst=50"
|
||||
# API service
|
||||
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
|
||||
healthcheck:
|
||||
test: ["CMD", "traefik", "healthcheck"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
|
||||
# Traefik Forward Auth for advanced authentication (optional)
|
||||
traefik-forward-auth:
|
||||
image: thomseddon/traefik-forward-auth:latest
|
||||
networks:
|
||||
- traefik-public
|
||||
environment:
|
||||
- DEFAULT_PROVIDER=generic-oauth
|
||||
- PROVIDERS_GENERIC_OAUTH_AUTH_URL=${OAUTH_AUTH_URL}
|
||||
- PROVIDERS_GENERIC_OAUTH_TOKEN_URL=${OAUTH_TOKEN_URL}
|
||||
- PROVIDERS_GENERIC_OAUTH_USER_URL=${OAUTH_USER_URL}
|
||||
- PROVIDERS_GENERIC_OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
|
||||
- PROVIDERS_GENERIC_OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
|
||||
- SECRET=${OAUTH_SECRET}
|
||||
- COOKIE_DOMAIN=${COOKIE_DOMAIN}
|
||||
- INSECURE_COOKIE=false
|
||||
- LOG_LEVEL=info
|
||||
- URL_PATH=/_oauth
|
||||
- WHITELIST=${OAUTH_WHITELIST}
|
||||
deploy:
|
||||
replicas: 2
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.http.routers.traefik-forward-auth.rule=Host(`${FRONTEND_HOST}`) && PathPrefix(`/_oauth`)"
|
||||
- "traefik.http.routers.traefik-forward-auth.entrypoints=https"
|
||||
- "traefik.http.routers.traefik-forward-auth.tls=true"
|
||||
- "traefik.http.routers.traefik-forward-auth.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.traefik-forward-auth.middlewares=auth-verify"
|
||||
- "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"
|
||||
- "traefik.http.middlewares.auth-verify.forwardauth.address=http://traefik-forward-auth:4181"
|
||||
- "traefik.http.middlewares.auth-verify.forwardauth.authResponseHeaders=X-Forwarded-User"
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
traefik-certificates:
|
||||
driver: local
|
||||
@@ -1,93 +0,0 @@
|
||||
# Static configuration
|
||||
global:
|
||||
checkNewVersion: true
|
||||
sendAnonymousUsage: false
|
||||
|
||||
api:
|
||||
dashboard: true
|
||||
debug: false
|
||||
|
||||
# Entry Points
|
||||
entryPoints:
|
||||
http:
|
||||
address: ":80"
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: https
|
||||
scheme: https
|
||||
priority: 1000
|
||||
https:
|
||||
address: ":443"
|
||||
http:
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
domains:
|
||||
- main: "${FRONTEND_HOST}"
|
||||
- main: "${BACKEND_HOST}"
|
||||
- main: "${UMAMI_HOST}"
|
||||
forwardedHeaders:
|
||||
trustedIPs:
|
||||
- "127.0.0.1/32"
|
||||
- "10.0.0.0/8"
|
||||
- "172.16.0.0/12"
|
||||
- "192.168.0.0/16"
|
||||
metrics:
|
||||
address: ":8082"
|
||||
|
||||
# Providers
|
||||
providers:
|
||||
docker:
|
||||
swarmMode: true
|
||||
exposedByDefault: false
|
||||
network: traefik-public
|
||||
watch: true
|
||||
file:
|
||||
directory: /etc/traefik/dynamic
|
||||
watch: true
|
||||
|
||||
# Certificate Resolvers
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
email: ${ACME_EMAIL}
|
||||
storage: /letsencrypt/acme.json
|
||||
httpChallenge:
|
||||
entryPoint: http
|
||||
# Staging server for testing
|
||||
# caServer: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
|
||||
# Logs
|
||||
log:
|
||||
level: INFO
|
||||
format: json
|
||||
|
||||
accessLog:
|
||||
format: json
|
||||
filters:
|
||||
statusCodes:
|
||||
- "200-299"
|
||||
- "400-499"
|
||||
- "500-599"
|
||||
retryAttempts: true
|
||||
minDuration: "10ms"
|
||||
|
||||
# Metrics
|
||||
metrics:
|
||||
prometheus:
|
||||
entryPoint: metrics
|
||||
addEntryPointsLabels: true
|
||||
addServicesLabels: true
|
||||
buckets:
|
||||
- 0.1
|
||||
- 0.3
|
||||
- 1.2
|
||||
- 5.0
|
||||
|
||||
# Ping
|
||||
ping:
|
||||
entryPoint: traefik
|
||||
|
||||
# Pilot
|
||||
pilot:
|
||||
enabled: false
|
||||
@@ -1,61 +0,0 @@
|
||||
# Development Docker Compose Configuration
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=3000
|
||||
- JWT_SECRET=dev-secret-change-in-production
|
||||
- ADMIN_URL=http://localhost:3005
|
||||
- FRONTEND_URL=http://localhost:3005
|
||||
- DATABASE_CLIENT=sqlite3
|
||||
- DATABASE_PATH=./data/photo_sharing.db
|
||||
# Email - uses Mailhog
|
||||
- SMTP_HOST=mailhog
|
||||
- SMTP_PORT=1025
|
||||
- SMTP_SECURE=false
|
||||
- EMAIL_FROM=noreply@photo-sharing.local
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
depends_on:
|
||||
- mailhog
|
||||
command: sh -c "npm install && npm run dev"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3005:80"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
- ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
mailhog:
|
||||
image: mailhog/mailhog:latest
|
||||
ports:
|
||||
- "1025:1025" # SMTP
|
||||
- "8025:8025" # Web UI
|
||||
@@ -1,110 +0,0 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
backend:
|
||||
image: 'registry.local.nothaft.cloud/picpeak-backend:latest'
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_CLIENT=pg
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
- DB_USER=${DB_USER:-picpeak}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_NAME=${DB_NAME:-picpeak}
|
||||
- PORT=3000
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ADMIN_URL=https://picpeak.local.nothaft.cloud
|
||||
- FRONTEND_URL=https://picpeak.local.nothaft.cloud
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_SECURE=${SMTP_SECURE}
|
||||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASS=${SMTP_PASS}
|
||||
- EMAIL_FROM=${EMAIL_FROM}
|
||||
- UMAMI_URL=${UMAMI_URL}
|
||||
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
|
||||
- STORAGE_PATH=/app/storage
|
||||
- EVENTS_PATH=/app/storage/events
|
||||
- ARCHIVE_PATH=/app/storage/events/archived
|
||||
volumes:
|
||||
- '/mnt/DockerMount/picpeak/storage:/app/storage'
|
||||
- '/mnt/DockerMount/picpeak/data:/app/data'
|
||||
- '/mnt/DockerMount/picpeak/logs:/app/logs'
|
||||
networks:
|
||||
- picpeak
|
||||
- proxy
|
||||
deploy:
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=proxy
|
||||
# Backend API routing WITHOUT path stripping
|
||||
- 'traefik.http.routers.picpeak-backend.rule=(Host(`picpeak.nothaft.cloud`) || Host(`picpeak.local.nothaft.cloud`)) && PathPrefix(`/api`)'
|
||||
- traefik.http.routers.picpeak-backend.entrypoints=https
|
||||
- traefik.http.routers.picpeak-backend.tls.certresolver=dns
|
||||
- traefik.http.services.picpeak-backend.loadbalancer.server.port=3000
|
||||
# Remove the stripprefix middleware - backend expects /api prefix
|
||||
# - traefik.http.middlewares.picpeak-stripprefix.stripprefix.prefixes=/api
|
||||
# - traefik.http.routers.picpeak-backend.middlewares=picpeak-stripprefix
|
||||
- traefik.http.routers.picpeak-backend.priority=100
|
||||
- homepage.group=Public Services
|
||||
- homepage.name=PicPeak Backend
|
||||
- homepage.icon=mdi-api
|
||||
- 'homepage.href=https://picpeak.local.nothaft.cloud/api/health'
|
||||
- homepage.description=PicPeak API Backend
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
frontend:
|
||||
image: 'registry.local.nothaft.cloud/picpeak-frontend:latest'
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- picpeak
|
||||
- proxy
|
||||
deploy:
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=proxy
|
||||
- traefik.http.routers.picpeak.rule=Host(`picpeak.nothaft.cloud`) || Host(`picpeak.local.nothaft.cloud`)
|
||||
- traefik.http.routers.picpeak.entrypoints=https
|
||||
- traefik.http.routers.picpeak.tls.certresolver=dns
|
||||
- traefik.http.services.picpeak.loadbalancer.server.port=80
|
||||
- traefik.http.routers.picpeak.priority=10
|
||||
- homepage.group=Public Services
|
||||
- homepage.name=PicPeak
|
||||
- homepage.icon=mdi-photo
|
||||
- 'homepage.href=https://picpeak.local.nothaft.cloud/'
|
||||
- homepage.description=Photo Sharing System
|
||||
|
||||
db:
|
||||
image: 'postgres:14-alpine'
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=${DB_USER:-picpeak}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME:-picpeak}
|
||||
- POSTGRES_HOST_AUTH_METHOD=${PG_AUTH_METHOD:-scram-sha-256}
|
||||
- POSTGRES_INITDB_ARGS=${PG_INIT_ARGS:---auth-host=scram-sha-256 --auth-local=trust}
|
||||
volumes:
|
||||
- '/mnt/DockerMount/picpeak/db:/var/lib/postgresql/data'
|
||||
networks:
|
||||
- picpeak
|
||||
command: ${PG_COMMANDS:-postgres -c ssl=off}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-picpeak}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
picpeak:
|
||||
driver: bridge
|
||||
@@ -1,131 +0,0 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
backend:
|
||||
image: 'registry.local.nothaft.cloud/picpeak-backend:latest'
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_CLIENT=pg
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
- DB_USER=${DB_USER:-picpeak}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_NAME=${DB_NAME:-picpeak}
|
||||
- PORT=3000
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ADMIN_URL=https://picpeak.local.nothaft.cloud
|
||||
- FRONTEND_URL=https://picpeak.local.nothaft.cloud
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_SECURE=${SMTP_SECURE}
|
||||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASS=${SMTP_PASS}
|
||||
- EMAIL_FROM=${EMAIL_FROM}
|
||||
- UMAMI_URL=${UMAMI_URL}
|
||||
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
|
||||
- STORAGE_PATH=/app/storage
|
||||
- EVENTS_PATH=/app/storage/events
|
||||
- ARCHIVE_PATH=/app/storage/events/archived
|
||||
volumes:
|
||||
- '/mnt/DockerMount/picpeak/storage:/app/storage'
|
||||
- '/mnt/DockerMount/picpeak/data:/app/data'
|
||||
- '/mnt/DockerMount/picpeak/logs:/app/logs'
|
||||
networks:
|
||||
- picpeak
|
||||
- proxy
|
||||
deploy:
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=proxy
|
||||
# Backend API routing
|
||||
- 'traefik.http.routers.picpeak-backend.rule=(Host(`picpeak.nothaft.cloud`) || Host(`picpeak.local.nothaft.cloud`)) && PathPrefix(`/api`)'
|
||||
- traefik.http.routers.picpeak-backend.entrypoints=https
|
||||
- traefik.http.routers.picpeak-backend.tls=true
|
||||
- traefik.http.routers.picpeak-backend.tls.certresolver=dns
|
||||
- traefik.http.services.picpeak-backend.loadbalancer.server.port=3000
|
||||
# Strip /api prefix when forwarding to backend
|
||||
- traefik.http.middlewares.picpeak-stripprefix.stripprefix.prefixes=/api
|
||||
- traefik.http.routers.picpeak-backend.middlewares=picpeak-stripprefix
|
||||
# Higher priority for API routes
|
||||
- traefik.http.routers.picpeak-backend.priority=100
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
frontend:
|
||||
image: 'registry.local.nothaft.cloud/picpeak-frontend:latest'
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- picpeak
|
||||
- proxy
|
||||
deploy:
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=proxy
|
||||
# Frontend routing (catch-all for non-API routes)
|
||||
- traefik.http.routers.picpeak.rule=Host(`picpeak.nothaft.cloud`) || Host(`picpeak.local.nothaft.cloud`)
|
||||
- traefik.http.routers.picpeak.entrypoints=https
|
||||
- traefik.http.routers.picpeak.tls=true
|
||||
- traefik.http.routers.picpeak.tls.certresolver=dns
|
||||
- traefik.http.services.picpeak.loadbalancer.server.port=80
|
||||
# Lower priority than backend to ensure /api routes go to backend
|
||||
- traefik.http.routers.picpeak.priority=10
|
||||
|
||||
db:
|
||||
image: 'postgres:14-alpine'
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=${DB_USER:-picpeak}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME:-picpeak}
|
||||
- POSTGRES_HOST_AUTH_METHOD=scram-sha-256
|
||||
- POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=trust
|
||||
volumes:
|
||||
- '/mnt/DockerMount/picpeak/db:/var/lib/postgresql/data'
|
||||
# Mount init script to create umami database
|
||||
- ./docker/postgres-init:/docker-entrypoint-initdb.d:ro
|
||||
networks:
|
||||
- picpeak
|
||||
command: postgres -c ssl=off
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-picpeak}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Optional: Umami analytics
|
||||
umami:
|
||||
image: ghcr.io/umami-software/umami:postgresql-latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${DB_USER:-picpeak}:${DB_PASSWORD}@db:5432/umami
|
||||
DATABASE_TYPE: postgresql
|
||||
HASH_SALT: ${UMAMI_HASH_SALT}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- picpeak
|
||||
- proxy
|
||||
deploy:
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=proxy
|
||||
- traefik.http.routers.picpeak-umami.rule=Host(`analytics.picpeak.local.nothaft.cloud`)
|
||||
- traefik.http.routers.picpeak-umami.entrypoints=https
|
||||
- traefik.http.routers.picpeak-umami.tls=true
|
||||
- traefik.http.routers.picpeak-umami.tls.certresolver=dns
|
||||
- traefik.http.services.picpeak-umami.loadbalancer.server.port=3000
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
picpeak:
|
||||
driver: bridge
|
||||
@@ -1,30 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
gitea-runner:
|
||||
image: gitea/act_runner:latest
|
||||
container_name: gitea-runner-picpeak
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# IMPORTANT: Replace this with your actual registration token from Gitea
|
||||
- GITEA_RUNNER_REGISTRATION_TOKEN=YOUR_REGISTRATION_TOKEN_HERE
|
||||
- GITEA_INSTANCE_URL=https://gitea.nothaft.cloud
|
||||
- GITEA_RUNNER_NAME=picpeak-docker-runner
|
||||
# Runner labels - what this runner can handle
|
||||
- GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:16-bullseye,ubuntu-22.04:docker://node:16-bullseye,ubuntu-20.04:docker://node:16-bullseye
|
||||
volumes:
|
||||
# Mount Docker socket to allow runner to create containers
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# Persist runner data
|
||||
- ./runner-data:/data
|
||||
# Cache directory
|
||||
- ./runner-cache:/root/.cache
|
||||
# Optional: Use host network for better performance
|
||||
# network_mode: host
|
||||
|
||||
# Optional: Watchtower to auto-update the runner
|
||||
# watchtower:
|
||||
# image: containrrr/watchtower
|
||||
# volumes:
|
||||
# - /var/run/docker.sock:/var/run/docker.sock
|
||||
# command: --interval 86400 gitea-runner-picpeak
|
||||
@@ -1,147 +0,0 @@
|
||||
# docker-compose.traefik.yml - Production configuration for external Traefik
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
image: picpeak-backend:latest
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ADMIN_URL=https://picpeak.nothaft.cloud
|
||||
- FRONTEND_URL=https://picpeak.nothaft.cloud
|
||||
# Database
|
||||
- DATABASE_CLIENT=pg
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
- DB_USER=${DB_USER:-picpeak}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_NAME=${DB_NAME:-picpeak}
|
||||
# Email
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_SECURE=${SMTP_SECURE}
|
||||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASS=${SMTP_PASS}
|
||||
- EMAIL_FROM=${EMAIL_FROM}
|
||||
# Analytics
|
||||
- UMAMI_URL=${UMAMI_URL}
|
||||
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
|
||||
# Storage paths
|
||||
- STORAGE_PATH=/app/storage
|
||||
- EVENTS_PATH=/app/storage/events
|
||||
- ARCHIVE_PATH=/app/storage/events/archived
|
||||
volumes:
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- picpeak
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik"
|
||||
# Backend API routing
|
||||
- "traefik.http.routers.picpeak-backend.rule=Host(`picpeak.nothaft.cloud`) && PathPrefix(`/api`)"
|
||||
- "traefik.http.routers.picpeak-backend.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak-backend.tls=true"
|
||||
- "traefik.http.routers.picpeak-backend.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak-backend.loadbalancer.server.port=3000"
|
||||
# Strip /api prefix when forwarding to backend
|
||||
- "traefik.http.middlewares.picpeak-stripprefix.stripprefix.prefixes=/api"
|
||||
- "traefik.http.routers.picpeak-backend.middlewares=picpeak-stripprefix"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
frontend:
|
||||
image: picpeak-frontend:latest
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- VITE_API_URL=/api
|
||||
- VITE_UMAMI_URL=${VITE_UMAMI_URL}
|
||||
- VITE_UMAMI_WEBSITE_ID=${VITE_UMAMI_WEBSITE_ID}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- picpeak
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik"
|
||||
# Frontend routing (catch-all for non-API routes)
|
||||
- "traefik.http.routers.picpeak-frontend.rule=Host(`picpeak.nothaft.cloud`)"
|
||||
- "traefik.http.routers.picpeak-frontend.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak-frontend.tls=true"
|
||||
- "traefik.http.routers.picpeak-frontend.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak-frontend.loadbalancer.server.port=80"
|
||||
# Lower priority than backend to ensure /api routes go to backend
|
||||
- "traefik.http.routers.picpeak-frontend.priority=1"
|
||||
|
||||
db:
|
||||
image: postgres:14-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=${DB_USER:-picpeak}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME:-picpeak}
|
||||
# Allow connections from any host with password authentication
|
||||
- POSTGRES_HOST_AUTH_METHOD=scram-sha-256
|
||||
- POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=trust
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
# Mount init script to create umami database
|
||||
- ./docker/postgres-init:/docker-entrypoint-initdb.d
|
||||
networks:
|
||||
- picpeak
|
||||
# Allow connections without SSL requirement from Docker network
|
||||
command: postgres -c ssl=off
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-picpeak}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
umami:
|
||||
image: ghcr.io/umami-software/umami:postgresql-latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${DB_USER:-picpeak}:${DB_PASSWORD}@db:5432/umami
|
||||
DATABASE_TYPE: postgresql
|
||||
HASH_SALT: ${UMAMI_HASH_SALT}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- picpeak
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik"
|
||||
# Umami analytics routing
|
||||
- "traefik.http.routers.picpeak-umami.rule=Host(`analytics.picpeak.nothaft.cloud`)"
|
||||
- "traefik.http.routers.picpeak-umami.entrypoints=websecure"
|
||||
- "traefik.http.routers.picpeak-umami.tls=true"
|
||||
- "traefik.http.routers.picpeak-umami.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.picpeak-umami.loadbalancer.server.port=3000"
|
||||
|
||||
networks:
|
||||
picpeak:
|
||||
driver: bridge
|
||||
traefik:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -0,0 +1,92 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: picpeak-postgres
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-picpeak}
|
||||
POSTGRES_USER: ${DB_USER:-picpeak}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-picpeak}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-picpeak}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Backend API
|
||||
backend:
|
||||
build: ./backend
|
||||
container_name: picpeak-backend
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
DATABASE_CLIENT: pg
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_NAME: ${DB_NAME:-picpeak}
|
||||
DB_USER: ${DB_USER:-picpeak}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-picpeak}
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# Frontend
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
args:
|
||||
VITE_API_URL: ${VITE_API_URL:-/api}
|
||||
VITE_UMAMI_URL: ${VITE_UMAMI_URL}
|
||||
VITE_UMAMI_WEBSITE_ID: ${VITE_UMAMI_WEBSITE_ID}
|
||||
container_name: picpeak-frontend
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
|
||||
# Nginx Reverse Proxy
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: picpeak-nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
|
||||
|
||||
# Certbot for SSL
|
||||
certbot:
|
||||
image: certbot/certbot
|
||||
container_name: picpeak-certbot
|
||||
volumes:
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: picpeak-network
|
||||
@@ -1,151 +0,0 @@
|
||||
# Gitea Actions Setup Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Gitea Version**: Ensure you're running Gitea 1.19.0 or later
|
||||
2. **Gitea Actions Enabled**: Check your Gitea configuration
|
||||
|
||||
## Step 1: Enable Gitea Actions in app.ini
|
||||
|
||||
Add or modify these settings in your Gitea `app.ini`:
|
||||
|
||||
```ini
|
||||
[actions]
|
||||
ENABLED = true
|
||||
DEFAULT_ACTIONS_URL = https://gitea.com
|
||||
```
|
||||
|
||||
## Step 2: Install Gitea Act Runner
|
||||
|
||||
### Option A: Using Docker
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name gitea-runner \
|
||||
--restart unless-stopped \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v gitea-runner-data:/data \
|
||||
-e GITEA_INSTANCE_URL=https://gitea.nothaft.cloud \
|
||||
-e GITEA_RUNNER_REGISTRATION_TOKEN=<your-registration-token> \
|
||||
-e GITEA_RUNNER_NAME=docker-runner \
|
||||
gitea/act_runner:latest
|
||||
```
|
||||
|
||||
### Option B: Using Binary
|
||||
|
||||
1. Download the act_runner:
|
||||
```bash
|
||||
wget https://gitea.com/gitea/act_runner/releases/download/v0.2.5/act_runner-0.2.5-linux-amd64
|
||||
chmod +x act_runner-0.2.5-linux-amd64
|
||||
sudo mv act_runner-0.2.5-linux-amd64 /usr/local/bin/act_runner
|
||||
```
|
||||
|
||||
2. Register the runner:
|
||||
```bash
|
||||
act_runner register \
|
||||
--instance https://gitea.nothaft.cloud \
|
||||
--token <your-registration-token> \
|
||||
--name "my-runner" \
|
||||
--labels "ubuntu-latest:docker://node:16-bullseye,ubuntu-22.04:docker://node:16-bullseye"
|
||||
```
|
||||
|
||||
3. Start the runner:
|
||||
```bash
|
||||
act_runner daemon
|
||||
```
|
||||
|
||||
## Step 3: Get Registration Token
|
||||
|
||||
1. Go to your Gitea instance admin panel
|
||||
2. Navigate to Site Administration → Actions → Runners
|
||||
3. Click "Create new Runner"
|
||||
4. Copy the registration token
|
||||
|
||||
## Step 4: Repository Settings
|
||||
|
||||
1. Go to your repository settings in Gitea
|
||||
2. Navigate to Settings → Actions → General
|
||||
3. Ensure Actions are enabled for the repository
|
||||
|
||||
## Step 5: Convert GitHub Actions to Gitea Actions
|
||||
|
||||
While Gitea Actions is mostly compatible with GitHub Actions, there are some differences:
|
||||
|
||||
### Workflow Location
|
||||
- GitHub Actions: `.github/workflows/`
|
||||
- Gitea Actions: `.gitea/workflows/` (preferred) or `.github/workflows/`
|
||||
|
||||
### Supported Features
|
||||
✅ Supported:
|
||||
- Basic workflow syntax
|
||||
- Common actions like `actions/checkout`
|
||||
- Environment variables
|
||||
- Secrets
|
||||
- Artifacts
|
||||
- Matrix builds
|
||||
|
||||
❌ Not Supported:
|
||||
- Some GitHub-specific actions
|
||||
- GitHub Packages
|
||||
- Some advanced features
|
||||
|
||||
## Step 6: Debug Workflow Issues
|
||||
|
||||
If workflows are stuck in "waiting":
|
||||
|
||||
1. **Check Runner Status**:
|
||||
```bash
|
||||
# If using Docker
|
||||
docker logs gitea-runner
|
||||
|
||||
# If using binary
|
||||
journalctl -u act_runner -f
|
||||
```
|
||||
|
||||
2. **Check Gitea Logs**:
|
||||
```bash
|
||||
# Check Gitea logs for action-related errors
|
||||
tail -f /path/to/gitea/log/gitea.log | grep -i action
|
||||
```
|
||||
|
||||
3. **Verify Runner Labels**:
|
||||
- Ensure your runner has the labels that match your workflow's `runs-on`
|
||||
- Common labels: `ubuntu-latest`, `ubuntu-22.04`, `ubuntu-20.04`
|
||||
|
||||
4. **Check Repository Permissions**:
|
||||
- Ensure the repository has Actions enabled
|
||||
- Check if there are any branch protection rules blocking Actions
|
||||
|
||||
## Step 7: Alternative - Use Drone CI
|
||||
|
||||
Since you already have Drone CI configured (`.drone.yml`), you might want to use that instead:
|
||||
|
||||
```yaml
|
||||
# Your existing .drone.yml is already set up for CI/CD
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
# ... rest of your Drone configuration
|
||||
```
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue: Workflows stuck in "waiting"
|
||||
**Solution**: No runners available. Register and start a runner.
|
||||
|
||||
### Issue: Runner can't connect
|
||||
**Solution**: Check firewall rules and ensure runner can reach Gitea instance.
|
||||
|
||||
### Issue: Docker-in-Docker errors
|
||||
**Solution**: Mount Docker socket or use privileged mode for runner.
|
||||
|
||||
### Issue: Actions not showing in UI
|
||||
**Solution**: Enable Actions in both Gitea config and repository settings.
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Check your Gitea version and configuration
|
||||
2. Install and register a runner
|
||||
3. Enable Actions for your repository
|
||||
4. Test with the simple workflow created in `.gitea/workflows/test.yml`
|
||||
5. Once working, migrate your GitHub Actions workflows if needed
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.26",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.26",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@tiptap/extension-link": "^2.25.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.26",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -872,6 +872,7 @@
|
||||
"passwordRequired": "Passwort ist erforderlich",
|
||||
"passwordMinLength": "Passwort muss mindestens 6 Zeichen lang sein",
|
||||
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
|
||||
"passwordSecurityRequirements": "Passwort erfüllt nicht die Sicherheitsanforderungen",
|
||||
"expirationRange": "Ablauf muss zwischen 1 und 365 Tagen liegen"
|
||||
},
|
||||
"maintenance": {
|
||||
|
||||
@@ -792,6 +792,7 @@
|
||||
"passwordRequired": "Password is required",
|
||||
"passwordMinLength": "Password must be at least 6 characters",
|
||||
"passwordsDoNotMatch": "Passwords do not match",
|
||||
"passwordSecurityRequirements": "Password does not meet security requirements",
|
||||
"expirationRange": "Expiration must be between 1 and 365 days"
|
||||
},
|
||||
"legal": {
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
Clock,
|
||||
ArrowLeft,
|
||||
Info,
|
||||
Upload
|
||||
Upload,
|
||||
Eye,
|
||||
EyeOff
|
||||
} from 'lucide-react';
|
||||
import { format, addDays } from 'date-fns';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -168,7 +170,13 @@ export const CreateEventPage: React.FC = () => {
|
||||
toast.error(t('errors.sessionExpired'));
|
||||
navigate('/admin/login');
|
||||
} else {
|
||||
toast.error(error.response?.data?.error || t('errors.failedToCreateEvent'));
|
||||
const errorMessage = error.response?.data?.error;
|
||||
// Check if it's the password security requirements error
|
||||
if (errorMessage === 'Password does not meet security requirements') {
|
||||
toast.error(t('validation.passwordSecurityRequirements'));
|
||||
} else {
|
||||
toast.error(errorMessage || t('errors.failedToCreateEvent'));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -405,7 +413,20 @@ export const CreateEventPage: React.FC = () => {
|
||||
error={errors.password}
|
||||
placeholder={t('events.enterPassword')}
|
||||
leftIcon={<Lock className="w-5 h-5 text-neutral-400" />}
|
||||
className="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
style={{ top: errors.password ? '0' : '0' }}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="w-5 h-5 text-neutral-400 hover:text-neutral-600" />
|
||||
) : (
|
||||
<Eye className="w-5 h-5 text-neutral-400 hover:text-neutral-600" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -414,29 +435,32 @@ export const CreateEventPage: React.FC = () => {
|
||||
<label htmlFor="confirm_password" className="block text-sm font-medium text-neutral-700 mb-1">
|
||||
{t('events.confirmPassword')}
|
||||
</label>
|
||||
<Input
|
||||
id="confirm_password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={formData.confirm_password}
|
||||
onChange={handleInputChange('confirm_password')}
|
||||
error={errors.confirm_password}
|
||||
placeholder={t('events.confirmPasswordPlaceholder')}
|
||||
leftIcon={<Lock className="w-5 h-5 text-neutral-400" />}
|
||||
/>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="confirm_password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={formData.confirm_password}
|
||||
onChange={handleInputChange('confirm_password')}
|
||||
error={errors.confirm_password}
|
||||
placeholder={t('events.confirmPasswordPlaceholder')}
|
||||
leftIcon={<Lock className="w-5 h-5 text-neutral-400" />}
|
||||
className="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
style={{ top: errors.confirm_password ? '0' : '0' }}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="w-5 h-5 text-neutral-400 hover:text-neutral-600" />
|
||||
) : (
|
||||
<Eye className="w-5 h-5 text-neutral-400 hover:text-neutral-600" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showPassword}
|
||||
onChange={(e) => setShowPassword(e.target.checked)}
|
||||
className="w-4 h-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500"
|
||||
/>
|
||||
<span className="ml-2 text-sm text-neutral-700">{t('events.showPasswords')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Gallery Settings */}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json;
|
||||
|
||||
# Rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com www.your-domain.com;
|
||||
|
||||
# Let's Encrypt challenge
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# Redirect to HTTPS
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-domain.com www.your-domain.com;
|
||||
|
||||
# SSL configuration
|
||||
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
|
||||
|
||||
# Modern SSL configuration
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# API proxy
|
||||
location /api {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# Rate limiting for API
|
||||
limit_req zone=general burst=20 nodelay;
|
||||
}
|
||||
|
||||
# Auth endpoints with stricter rate limiting
|
||||
location /api/auth {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Strict rate limiting for auth
|
||||
limit_req zone=auth burst=5 nodelay;
|
||||
}
|
||||
|
||||
# Static files
|
||||
location / {
|
||||
proxy_pass http://frontend:80;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# Security headers for static content
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
|
||||
proxy_pass http://frontend:80;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
|
||||
# PicPeak Backup Script
|
||||
# Creates backups of database and storage
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
BACKUP_DIR="./backups"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_NAME="picpeak_backup_${TIMESTAMP}"
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo "🔄 Starting PicPeak backup..."
|
||||
|
||||
# Create backup directory
|
||||
mkdir -p "${BACKUP_DIR}/${BACKUP_NAME}"
|
||||
|
||||
# Backup database
|
||||
echo "📊 Backing up database..."
|
||||
if [ -f "./data/photo_sharing.db" ]; then
|
||||
cp ./data/photo_sharing.db "${BACKUP_DIR}/${BACKUP_NAME}/"
|
||||
echo -e "${GREEN}✓ SQLite database backed up${NC}"
|
||||
else
|
||||
# PostgreSQL backup
|
||||
docker exec picpeak-postgres pg_dump -U picpeak picpeak > "${BACKUP_DIR}/${BACKUP_NAME}/database.sql" 2>/dev/null || {
|
||||
echo -e "${RED}⚠ Database backup failed - is PostgreSQL running?${NC}"
|
||||
}
|
||||
fi
|
||||
|
||||
# Backup storage
|
||||
echo "📸 Backing up photos..."
|
||||
if [ -d "./storage" ]; then
|
||||
tar -czf "${BACKUP_DIR}/${BACKUP_NAME}/storage.tar.gz" ./storage 2>/dev/null || {
|
||||
echo -e "${RED}⚠ Storage backup failed${NC}"
|
||||
exit 1
|
||||
}
|
||||
echo -e "${GREEN}✓ Storage backed up${NC}"
|
||||
fi
|
||||
|
||||
# Backup environment files
|
||||
echo "⚙️ Backing up configuration..."
|
||||
cp .env "${BACKUP_DIR}/${BACKUP_NAME}/.env.backup" 2>/dev/null || true
|
||||
|
||||
# Create backup info
|
||||
echo "📝 Creating backup info..."
|
||||
cat > "${BACKUP_DIR}/${BACKUP_NAME}/backup_info.txt" << EOF
|
||||
PicPeak Backup
|
||||
Created: $(date)
|
||||
Version: $(grep version backend/package.json | head -1 | awk -F'"' '{print $4}')
|
||||
Storage Size: $(du -sh ./storage 2>/dev/null | cut -f1 || echo "N/A")
|
||||
EOF
|
||||
|
||||
# Compress entire backup
|
||||
echo "📦 Compressing backup..."
|
||||
cd "${BACKUP_DIR}"
|
||||
tar -czf "${BACKUP_NAME}.tar.gz" "${BACKUP_NAME}"
|
||||
rm -rf "${BACKUP_NAME}"
|
||||
|
||||
# Cleanup old backups (keep last 7)
|
||||
echo "🧹 Cleaning up old backups..."
|
||||
ls -t *.tar.gz | tail -n +8 | xargs -r rm
|
||||
|
||||
echo -e "${GREEN}✅ Backup completed: ${BACKUP_DIR}/${BACKUP_NAME}.tar.gz${NC}"
|
||||
echo "💡 To restore: tar -xzf ${BACKUP_NAME}.tar.gz && follow restore instructions"
|
||||
@@ -1,380 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Setup script to create remaining files
|
||||
|
||||
echo "Creating remaining project files..."
|
||||
|
||||
# Create directories
|
||||
mkdir -p backend/src/services
|
||||
mkdir -p backend/src/utils
|
||||
mkdir -p backend/src/routes
|
||||
mkdir -p backend/migrations
|
||||
mkdir -p backend/scripts
|
||||
mkdir -p backend/__tests__
|
||||
mkdir -p frontend/public
|
||||
mkdir -p frontend/src/components
|
||||
mkdir -p frontend/src/contexts
|
||||
mkdir -p frontend/src/hooks
|
||||
mkdir -p frontend/src/pages/admin
|
||||
mkdir -p frontend/src/services
|
||||
mkdir -p frontend/src/config
|
||||
mkdir -p nginx/sites-enabled
|
||||
mkdir -p scripts
|
||||
mkdir -p storage/events/active
|
||||
mkdir -p storage/events/archived
|
||||
mkdir -p storage/thumbnails
|
||||
mkdir -p data
|
||||
mkdir -p logs
|
||||
mkdir -p certbot/conf
|
||||
mkdir -p certbot/www
|
||||
|
||||
# Create .gitkeep files
|
||||
touch storage/events/active/.gitkeep
|
||||
touch storage/events/archived/.gitkeep
|
||||
touch storage/thumbnails/.gitkeep
|
||||
touch data/.gitkeep
|
||||
touch logs/.gitkeep
|
||||
|
||||
# Create remaining backend services
|
||||
cat > backend/src/services/imageProcessor.js << 'EOF'
|
||||
const sharp = require('sharp');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const THUMBNAIL_WIDTH = 300;
|
||||
const THUMBNAIL_PATH = path.join(__dirname, '../../../storage/thumbnails');
|
||||
|
||||
async function generateThumbnail(imagePath) {
|
||||
const filename = path.basename(imagePath);
|
||||
const thumbnailFilename = `thumb_${filename}`;
|
||||
const thumbnailPath = path.join(THUMBNAIL_PATH, thumbnailFilename);
|
||||
|
||||
// Ensure thumbnail directory exists
|
||||
await fs.mkdir(THUMBNAIL_PATH, { recursive: true });
|
||||
|
||||
// Generate thumbnail
|
||||
await sharp(imagePath)
|
||||
.resize(THUMBNAIL_WIDTH, null, {
|
||||
withoutEnlargement: true,
|
||||
fit: 'inside'
|
||||
})
|
||||
.jpeg({ quality: 80 })
|
||||
.toFile(thumbnailPath);
|
||||
|
||||
return path.relative(path.join(__dirname, '../../../storage'), thumbnailPath);
|
||||
}
|
||||
|
||||
module.exports = { generateThumbnail };
|
||||
EOF
|
||||
|
||||
# Create logger utility
|
||||
cat > backend/src/utils/logger.js << 'EOF'
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.File({
|
||||
filename: path.join(__dirname, '../../../logs/error.log'),
|
||||
level: 'error'
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: path.join(__dirname, '../../../logs/combined.log')
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = logger;
|
||||
EOF
|
||||
|
||||
echo "Backend services created."
|
||||
|
||||
# Create migration init script
|
||||
cat > backend/migrations/init.js << 'EOF'
|
||||
const bcrypt = require('bcrypt');
|
||||
const { db, initializeDatabase } = require('../src/database/db');
|
||||
|
||||
async function runMigrations() {
|
||||
console.log('Running database migrations...');
|
||||
|
||||
try {
|
||||
// Initialize tables
|
||||
await initializeDatabase();
|
||||
|
||||
// Create default admin user if none exists
|
||||
const adminExists = await db('admin_users').first();
|
||||
if (!adminExists) {
|
||||
const defaultPassword = 'admin123'; // Change this!
|
||||
const passwordHash = await bcrypt.hash(defaultPassword, 10);
|
||||
|
||||
await db('admin_users').insert({
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
password_hash: passwordHash
|
||||
});
|
||||
|
||||
console.log('Default admin user created:');
|
||||
console.log('Username: admin');
|
||||
console.log('Password: admin123');
|
||||
console.log('⚠️ Please change this password immediately!');
|
||||
}
|
||||
|
||||
console.log('Migrations completed successfully');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runMigrations();
|
||||
EOF
|
||||
|
||||
echo "Migration script created."
|
||||
|
||||
# Create README
|
||||
cat > README.md << 'EOF'
|
||||
# Photo Sharing Platform
|
||||
|
||||
A secure, self-hosted photo sharing platform designed for weddings and events. Features automatic expiration, email notifications, and simple file-based management.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔒 Password Protected Galleries
|
||||
- ⏰ Automatic Expiration
|
||||
- 📧 Email Notifications
|
||||
- 📁 Simple File Management
|
||||
- 📊 Analytics Integration
|
||||
- 🎨 Customizable Themes
|
||||
- 📱 Mobile Responsive
|
||||
- ⚡ Docker Ready
|
||||
|
||||
## Quick Start
|
||||
|
||||
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`
|
||||
|
||||
Default credentials: admin / admin123 (change immediately!)
|
||||
|
||||
## Documentation
|
||||
|
||||
See DEPLOYMENT.md for detailed deployment instructions.
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
EOF
|
||||
|
||||
echo "README created."
|
||||
|
||||
# Create main installation script
|
||||
cat > scripts/install.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Photo Sharing Platform - Docker Installation"
|
||||
echo "==========================================="
|
||||
|
||||
# Check if running as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This script must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to check if command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
echo "Checking prerequisites..."
|
||||
|
||||
# Install Docker if not present
|
||||
if ! command_exists docker; then
|
||||
echo "Installing Docker..."
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sh get-docker.sh
|
||||
rm get-docker.sh
|
||||
fi
|
||||
|
||||
# Install Docker Compose if not present
|
||||
if ! command_exists docker-compose; then
|
||||
echo "Installing Docker Compose..."
|
||||
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
chmod +x /usr/local/bin/docker-compose
|
||||
fi
|
||||
|
||||
# Create necessary directories
|
||||
echo "Creating directory structure..."
|
||||
mkdir -p storage/events/{active,archived}
|
||||
mkdir -p storage/thumbnails
|
||||
mkdir -p data
|
||||
mkdir -p logs
|
||||
mkdir -p nginx/sites-enabled
|
||||
mkdir -p certbot/{conf,www}
|
||||
|
||||
# Set permissions
|
||||
chmod -R 755 storage
|
||||
chmod -R 755 data
|
||||
chmod -R 755 logs
|
||||
|
||||
# Copy environment file
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
echo "Created .env file. Please edit it with your configuration."
|
||||
fi
|
||||
|
||||
# Generate secure passwords
|
||||
echo "Generating secure passwords..."
|
||||
JWT_SECRET=$(openssl rand -base64 32)
|
||||
DB_PASSWORD=$(openssl rand -base64 32)
|
||||
UMAMI_HASH_SALT=$(openssl rand -base64 32)
|
||||
|
||||
# Update .env file with generated values
|
||||
sed -i "s/JWT_SECRET=.*/JWT_SECRET=$JWT_SECRET/" .env
|
||||
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=$DB_PASSWORD/" .env
|
||||
sed -i "s/UMAMI_HASH_SALT=.*/UMAMI_HASH_SALT=$UMAMI_HASH_SALT/" .env
|
||||
|
||||
echo ""
|
||||
echo "Installation complete!"
|
||||
echo "Next steps:"
|
||||
echo "1. Edit .env file with your domain names and SMTP settings"
|
||||
echo "2. Run: ./scripts/setup-ssl.sh to configure SSL certificates"
|
||||
echo "3. Run: docker-compose -f docker-compose.prod.yml up -d"
|
||||
echo "4. Run: docker-compose -f docker-compose.prod.yml exec backend npm run migrate"
|
||||
EOF
|
||||
|
||||
chmod +x scripts/install.sh
|
||||
|
||||
echo "Installation script created."
|
||||
|
||||
# Create nginx config
|
||||
cat > nginx/nginx.conf << 'EOF'
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
|
||||
# Rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;
|
||||
|
||||
include /etc/nginx/sites-enabled/*.conf;
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "Nginx config created."
|
||||
|
||||
# Create frontend package.json
|
||||
cat > frontend/package.json << 'EOF'
|
||||
{
|
||||
"name": "photo-sharing-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.8.0",
|
||||
"axios": "^1.3.2",
|
||||
"react-query": "^3.39.3",
|
||||
"date-fns": "^2.29.3",
|
||||
"react-toastify": "^9.1.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-image-gallery": "^1.2.11",
|
||||
"react-countdown": "^2.3.5",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"postcss": "^8.4.21",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"proxy": "http://localhost:3000"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "Frontend package.json created."
|
||||
|
||||
echo ""
|
||||
echo "Setup script complete!"
|
||||
echo "Most important files have been created."
|
||||
echo ""
|
||||
echo "To complete the setup:"
|
||||
echo "1. Run this script: chmod +x setup-remaining-files.sh && ./setup-remaining-files.sh"
|
||||
echo "2. Review and update the created files as needed"
|
||||
echo "3. Install dependencies: cd backend && npm install && cd ../frontend && npm install"
|
||||
echo "4. Follow the deployment instructions in the README"
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}🚀 Photo Sharing Platform - Local Development Setup${NC}"
|
||||
echo "=================================================="
|
||||
|
||||
# Check if Docker is installed
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo -e "${RED}❌ Docker is not installed. Please install Docker Desktop first.${NC}"
|
||||
echo " Visit: https://www.docker.com/products/docker-desktop"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker is running
|
||||
if ! docker info &> /dev/null; then
|
||||
echo -e "${RED}❌ Docker is not running. Please start Docker Desktop.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create necessary directories
|
||||
echo -e "${YELLOW}📁 Creating directories...${NC}"
|
||||
mkdir -p storage/events/{active,archived}
|
||||
mkdir -p storage/thumbnails
|
||||
mkdir -p data
|
||||
mkdir -p logs
|
||||
mkdir -p backend/node_modules
|
||||
mkdir -p frontend/node_modules
|
||||
|
||||
# Copy local environment file if it doesn't exist
|
||||
if [ ! -f .env ]; then
|
||||
echo -e "${YELLOW}📋 Setting up environment...${NC}"
|
||||
cp .env.local .env
|
||||
fi
|
||||
|
||||
# Stop any existing containers
|
||||
echo -e "${YELLOW}🛑 Stopping existing containers...${NC}"
|
||||
docker-compose -f docker-compose.local.yml down 2>/dev/null || true
|
||||
|
||||
# Build images
|
||||
echo -e "${YELLOW}🔨 Building Docker images...${NC}"
|
||||
docker-compose -f docker-compose.local.yml build
|
||||
|
||||
# Start services
|
||||
echo -e "${YELLOW}🚀 Starting services...${NC}"
|
||||
docker-compose -f docker-compose.local.yml up -d
|
||||
|
||||
# Wait for backend to be ready
|
||||
echo -e "${YELLOW}⏳ Waiting for backend to start...${NC}"
|
||||
max_attempts=30
|
||||
attempt=1
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if curl -s http://localhost:3001/api/health > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ Backend is ready!${NC}"
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
if [ $attempt -gt $max_attempts ]; then
|
||||
echo -e "${RED}❌ Backend failed to start. Check logs with: docker-compose -f docker-compose.local.yml logs backend${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build frontend for production-like testing
|
||||
echo -e "${YELLOW}📦 Building frontend...${NC}"
|
||||
docker-compose -f docker-compose.local.yml exec frontend-dev npm run build
|
||||
|
||||
# Show status
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Local development environment is ready!${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}🌐 Access Points:${NC}"
|
||||
echo " Frontend (Production Build): http://localhost:3000"
|
||||
echo " Frontend (Dev with Hot Reload): http://localhost:3002"
|
||||
echo " Backend API: http://localhost:3001/api"
|
||||
echo " Mailhog (Email Testing): http://localhost:8025"
|
||||
echo ""
|
||||
echo -e "${GREEN}🔑 Default Admin Credentials:${NC}"
|
||||
echo " Username: admin"
|
||||
echo " Password: admin123"
|
||||
echo ""
|
||||
echo -e "${GREEN}📝 Useful Commands:${NC}"
|
||||
echo " View logs: docker-compose -f docker-compose.local.yml logs -f"
|
||||
echo " Stop all: ./stop-local.sh"
|
||||
echo " Backend shell: docker-compose -f docker-compose.local.yml exec backend sh"
|
||||
echo " Reset database: docker-compose -f docker-compose.local.yml exec backend npm run migrate"
|
||||
echo ""
|
||||
echo -e "${GREEN}💡 Tips:${NC}"
|
||||
echo " - Frontend dev server (port 3002) has hot reload enabled"
|
||||
echo " - All emails are caught by Mailhog - check http://localhost:8025"
|
||||
echo " - SQLite database is stored in ./data/photo_sharing.db"
|
||||
echo " - Upload photos to ./storage/events/active/{event-name}/"
|
||||
echo ""
|
||||
|
||||
# Open browser
|
||||
if command -v xdg-open &> /dev/null; then
|
||||
xdg-open http://localhost:3002
|
||||
elif command -v open &> /dev/null; then
|
||||
open http://localhost:3002
|
||||
fi
|
||||
|
||||
# Show logs
|
||||
echo -e "${YELLOW}📋 Showing logs (Ctrl+C to exit)...${NC}"
|
||||
docker-compose -f docker-compose.local.yml logs -f
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${YELLOW}🛑 Stopping Photo Sharing Platform...${NC}"
|
||||
|
||||
# Stop all containers
|
||||
docker-compose -f docker-compose.local.yml down
|
||||
|
||||
# Optional: Remove volumes (uncomment if you want to reset data)
|
||||
# docker-compose -f docker-compose.local.yml down -v
|
||||
|
||||
echo -e "${GREEN}✅ All services stopped${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}💡 Tips:${NC}"
|
||||
echo " - Your data is preserved in ./data and ./storage"
|
||||
echo " - To completely reset, run: docker-compose -f docker-compose.local.yml down -v"
|
||||
echo " - To restart, run: ./start-local.sh"
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}🔄 Updating Photo Sharing Platform - Local Development${NC}"
|
||||
echo "===================================================="
|
||||
|
||||
# Check if Docker is running
|
||||
if ! docker info &> /dev/null; then
|
||||
echo -e "${RED}❌ Docker is not running. Please start Docker Desktop.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop all containers
|
||||
echo -e "${YELLOW}🛑 Stopping all containers...${NC}"
|
||||
docker-compose -f docker-compose.local.yml down
|
||||
|
||||
# Remove old images to force rebuild
|
||||
echo -e "${YELLOW}🗑️ Removing old images...${NC}"
|
||||
docker-compose -f docker-compose.local.yml rm -f
|
||||
|
||||
# Pull latest base images
|
||||
echo -e "${YELLOW}📥 Pulling latest base images...${NC}"
|
||||
docker-compose -f docker-compose.local.yml pull
|
||||
|
||||
# Build frontend production files
|
||||
echo -e "${YELLOW}📦 Building frontend production files...${NC}"
|
||||
cd frontend
|
||||
npm install --legacy-peer-deps
|
||||
npm run build
|
||||
cd ..
|
||||
|
||||
# Rebuild all images with no cache
|
||||
echo -e "${YELLOW}🔨 Rebuilding Docker images (no cache)...${NC}"
|
||||
docker-compose -f docker-compose.local.yml build --no-cache
|
||||
|
||||
# Start all services
|
||||
echo -e "${YELLOW}🚀 Starting services...${NC}"
|
||||
docker-compose -f docker-compose.local.yml up -d
|
||||
|
||||
# Wait for backend to be ready
|
||||
echo -e "${YELLOW}⏳ Waiting for backend to start...${NC}"
|
||||
max_attempts=30
|
||||
attempt=1
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if curl -s http://localhost:3001/api/health > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ Backend is ready!${NC}"
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
if [ $attempt -gt $max_attempts ]; then
|
||||
echo -e "${RED}❌ Backend failed to start. Check logs with: docker-compose -f docker-compose.local.yml logs backend${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Wait a bit more for frontend to be ready
|
||||
echo -e "${YELLOW}⏳ Waiting for frontend to be ready...${NC}"
|
||||
sleep 5
|
||||
|
||||
# Show status
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Local development environment has been updated!${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}🌐 Access Points:${NC}"
|
||||
echo " Frontend (Nginx): http://localhost:3005"
|
||||
echo " Frontend (Dev): http://localhost:3002"
|
||||
echo " Backend API: http://localhost:3001"
|
||||
echo " Mailhog: http://localhost:8025"
|
||||
echo ""
|
||||
echo -e "${GREEN}📝 Container Status:${NC}"
|
||||
docker-compose -f docker-compose.local.yml ps
|
||||
echo ""
|
||||
echo -e "${GREEN}💡 Tips:${NC}"
|
||||
echo " - View logs: docker-compose -f docker-compose.local.yml logs -f"
|
||||
echo " - View specific service logs: docker-compose -f docker-compose.local.yml logs -f [service-name]"
|
||||
echo " - Stop all: ./stop-local.sh"
|
||||
echo ""
|
||||
|
||||
# Open browser
|
||||
if command -v xdg-open &> /dev/null; then
|
||||
xdg-open http://localhost:3005
|
||||
elif command -v open &> /dev/null; then
|
||||
open http://localhost:3005
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✨ Update complete! The browser should open automatically.${NC}"
|
||||
Reference in New Issue
Block a user