Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc17e7d59d | |||
| f05ad87602 | |||
| 2efc74a687 | |||
| 85e7fbe73f | |||
| 0a21856a8d | |||
| 349e7c7eb1 | |||
| be07438915 |
-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
|
# PicPeak Production Configuration
|
||||||
# Copy this file to .env and fill in your values
|
# Copy this file to .env and update with your values
|
||||||
|
|
||||||
# Application URLs
|
# Required: Security
|
||||||
ADMIN_URL=https://yourdomain.com
|
JWT_SECRET=CHANGE_THIS_TO_RANDOM_32_CHAR_STRING
|
||||||
FRONTEND_URL=https://yourdomain.com
|
|
||||||
|
|
||||||
# Security - CRITICAL: Generate a secure random JWT secret
|
# Required: URLs (update with your domain)
|
||||||
# You can generate one with: openssl rand -base64 32
|
FRONTEND_URL=https://your-domain.com
|
||||||
JWT_SECRET=your-secure-random-jwt-secret-here
|
BACKEND_URL=https://your-domain.com
|
||||||
|
ADMIN_URL=https://your-domain.com
|
||||||
|
|
||||||
# Database Configuration (PostgreSQL)
|
# Required: Email Settings
|
||||||
DB_USER=picpeak
|
|
||||||
DB_PASSWORD=your-secure-database-password
|
|
||||||
DB_NAME=picpeak
|
|
||||||
|
|
||||||
# Email Configuration
|
|
||||||
SMTP_HOST=smtp.gmail.com
|
SMTP_HOST=smtp.gmail.com
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=true
|
|
||||||
SMTP_USER=your-email@gmail.com
|
SMTP_USER=your-email@gmail.com
|
||||||
SMTP_PASS=your-app-password
|
SMTP_PASS=your-app-password
|
||||||
EMAIL_FROM=noreply@yourdomain.com
|
SMTP_FROM=your-email@gmail.com
|
||||||
|
|
||||||
# Umami Analytics (Optional)
|
# Required: Initial Admin Account
|
||||||
UMAMI_URL=https://analytics.yourdomain.com
|
ADMIN_EMAIL=admin@your-domain.com
|
||||||
UMAMI_WEBSITE_ID=your-website-id
|
ADMIN_PASSWORD=change-this-password
|
||||||
UMAMI_HASH_SALT=your-random-hash-salt
|
|
||||||
|
|
||||||
# First Admin User (for initial setup)
|
# Database (PostgreSQL recommended for production)
|
||||||
# Run: docker-compose exec backend node scripts/create-admin.js --email admin@yourdomain.com
|
DATABASE_CLIENT=pg
|
||||||
ADMIN_EMAIL=admin@yourdomain.com
|
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
|
||||||
@@ -16,8 +16,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Git
|
- name: Setup Git
|
||||||
run: |
|
run: |
|
||||||
git config --global user.name "Gitea Mirror Bot"
|
git config --global user.name "the-luap"
|
||||||
git config --global user.email "bot@noreply.gitea.local"
|
git config --global user.email "paul-nothaft@hotmail.de"
|
||||||
|
|
||||||
- name: Create filtered branch
|
- name: Create filtered branch
|
||||||
run: |
|
run: |
|
||||||
@@ -33,16 +33,26 @@ jobs:
|
|||||||
git rm -r --cached .claudedocs/ || true
|
git rm -r --cached .claudedocs/ || true
|
||||||
git rm -r --cached backend/data/ || true
|
git rm -r --cached backend/data/ || true
|
||||||
git rm -r --cached backend/storage/ || 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
|
# Commit the changes
|
||||||
git commit -m "Remove sensitive files for GitHub mirror" || true
|
git commit -m "Remove sensitive files for GitHub mirror" || true
|
||||||
|
|
||||||
- name: Push to GitHub
|
- name: Push to GitHub
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }}
|
||||||
run: |
|
run: |
|
||||||
# Add GitHub remote
|
# Add GitHub remote
|
||||||
git remote add github https://x-access-token:${GITHUB_TOKEN}@github.com/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME.git
|
git remote add github https://x-access-token:${GITHUBTOKEN}@github.com/the-luap/picpeak.git
|
||||||
|
|
||||||
# Force push the filtered branch to GitHub main
|
# Force push the filtered branch to GitHub main
|
||||||
git push github github-mirror:main --force
|
git push github github-mirror:main --force
|
||||||
@@ -18,3 +18,7 @@ dist/
|
|||||||
build/
|
build/
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.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
|
## 📋 Prerequisites
|
||||||
- [Quick Start (Development)](#quick-start-development)
|
|
||||||
- [Production Deployment](#production-deployment)
|
|
||||||
- [Admin User Setup](#admin-user-setup)
|
|
||||||
- [Configuration Reference](#configuration-reference)
|
|
||||||
- [Troubleshooting](#troubleshooting)
|
|
||||||
|
|
||||||
## 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
|
```bash
|
||||||
git clone https://github.com/yourusername/picpeak.git
|
# Clone the repository
|
||||||
|
git clone https://github.com/the-luap/picpeak.git
|
||||||
cd picpeak
|
cd picpeak
|
||||||
|
|
||||||
# Copy environment template
|
# 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
|
cp .env.production.example .env
|
||||||
|
|
||||||
# Generate secure secrets
|
# Generate a secure JWT secret
|
||||||
echo "JWT_SECRET=$(openssl rand -base64 32)" >> .env
|
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
|
```env
|
||||||
# Your domain
|
# Application URLs
|
||||||
ADMIN_URL=https://yourdomain.com
|
FRONTEND_URL=https://your-domain.com
|
||||||
FRONTEND_URL=https://yourdomain.com
|
BACKEND_URL=https://your-domain.com
|
||||||
|
|
||||||
# Database (PostgreSQL)
|
# Email Configuration (Required for notifications)
|
||||||
DB_USER=picpeak
|
|
||||||
DB_NAME=picpeak
|
|
||||||
# DB_PASSWORD already generated above
|
|
||||||
|
|
||||||
# Email
|
|
||||||
SMTP_HOST=smtp.gmail.com
|
SMTP_HOST=smtp.gmail.com
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=true
|
|
||||||
SMTP_USER=your-email@gmail.com
|
SMTP_USER=your-email@gmail.com
|
||||||
SMTP_PASS=your-app-password
|
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
|
# Database (PostgreSQL for production)
|
||||||
# Configure frontend for production
|
DATABASE_CLIENT=pg
|
||||||
echo "VITE_API_URL=/api" > frontend/.env.production
|
DB_HOST=postgres
|
||||||
|
DB_NAME=picpeak
|
||||||
|
DB_USER=picpeak
|
||||||
|
DB_PASSWORD=secure-db-password
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Deploy with Docker Compose
|
### 3. Deploy with Docker Compose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build and start services
|
# Start all services
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
# Check status
|
# Check logs
|
||||||
docker-compose -f docker-compose.prod.yml ps
|
docker-compose logs -f
|
||||||
|
|
||||||
# View logs
|
# Access your site at https://your-domain.com
|
||||||
docker-compose -f docker-compose.prod.yml logs -f
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Deploy with Traefik
|
## 🔧 Configuration Options
|
||||||
|
|
||||||
If using Traefik, create `docker-compose.override.yml`:
|
### Storage Settings
|
||||||
|
|
||||||
```yaml
|
```env
|
||||||
version: '3.8'
|
# Storage paths (default: ./storage)
|
||||||
|
STORAGE_PATH=./storage
|
||||||
|
ARCHIVE_PATH=./storage/archives
|
||||||
|
|
||||||
services:
|
# Gallery expiration (days)
|
||||||
frontend:
|
DEFAULT_EXPIRATION_DAYS=30
|
||||||
labels:
|
WARNING_DAYS_BEFORE_EXPIRY=7
|
||||||
- "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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
```bash
|
||||||
# Production
|
# Backup database and photos
|
||||||
docker-compose -f docker-compose.prod.yml exec backend node scripts/create-admin.js \
|
./scripts/backup.sh
|
||||||
--email admin@yourdomain.com \
|
|
||||||
--username admin \
|
|
||||||
--password yourSecurePassword
|
|
||||||
|
|
||||||
# Auto-generate password
|
# Backups are stored in ./backups/
|
||||||
docker-compose -f docker-compose.prod.yml exec backend node scripts/create-admin.js \
|
|
||||||
--email admin@yourdomain.com
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The script will display:
|
### Update
|
||||||
- ✅ 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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Pull latest changes
|
# Pull latest changes
|
||||||
git pull
|
git pull
|
||||||
|
|
||||||
# Rebuild images
|
# Rebuild and restart
|
||||||
docker-compose -f docker-compose.prod.yml build
|
docker-compose -f docker-compose.prod.yml up -d --build
|
||||||
|
|
||||||
# Restart services
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Clean Up
|
### Logs
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Remove unused images
|
# View all logs
|
||||||
docker image prune -a
|
docker-compose logs
|
||||||
|
|
||||||
# Clean up logs
|
# View specific service
|
||||||
docker-compose -f docker-compose.prod.yml logs --tail=0 -f
|
docker-compose logs backend
|
||||||
|
docker-compose logs frontend
|
||||||
# Remove old archives
|
|
||||||
find ./storage/events/archived -name "*.zip" -mtime +90 -delete
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security Checklist
|
## 🚨 Troubleshooting
|
||||||
|
|
||||||
- [ ] Generated secure `JWT_SECRET`
|
### Common Issues
|
||||||
- [ ] Generated secure `DB_PASSWORD`
|
|
||||||
- [ ] HTTPS enabled via reverse proxy
|
**Photos not appearing:**
|
||||||
- [ ] Changed default admin credentials
|
- Check storage permissions: `chmod -R 755 storage/`
|
||||||
- [ ] Configured real SMTP server
|
- Verify file watcher is running: `docker-compose logs backend | grep watcher`
|
||||||
- [ ] Set file permissions: `chmod 600 .env`
|
|
||||||
- [ ] Firewall configured
|
**Email not sending:**
|
||||||
- [ ] Regular backups scheduled
|
- Test SMTP settings: Admin Panel → Settings → Email → Send Test
|
||||||
- [ ] Monitoring enabled
|
- 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
|
```bash
|
||||||
# Clone repository
|
# 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
|
cd wedding-photo-sharing
|
||||||
|
|
||||||
# Create required directories
|
# 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
|
Unlike expensive SaaS solutions, PicPeak gives you:
|
||||||
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: 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.
|
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.23",
|
"version": "1.0.26",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.23",
|
"version": "1.0.26",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.23",
|
"version": "1.0.26",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -77,12 +77,14 @@ router.post('/', adminAuth, [
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create category
|
// Create category
|
||||||
const [categoryId] = await db('photo_categories').insert({
|
const insertResult = await db('photo_categories').insert({
|
||||||
name,
|
name,
|
||||||
slug: categorySlug,
|
slug: categorySlug,
|
||||||
is_global,
|
is_global,
|
||||||
event_id: is_global ? null : event_id
|
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();
|
const category = await db('photo_categories').where('id', categoryId).first();
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ router.post('/', adminAuth, [
|
|||||||
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
|
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
|
||||||
|
|
||||||
// Insert into database
|
// Insert into database
|
||||||
const [eventId] = await db('events').insert({
|
const insertResult = await db('events').insert({
|
||||||
slug,
|
slug,
|
||||||
event_type,
|
event_type,
|
||||||
event_name,
|
event_name,
|
||||||
@@ -108,7 +108,10 @@ router.post('/', adminAuth, [
|
|||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
allow_user_uploads,
|
allow_user_uploads,
|
||||||
upload_category_id
|
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
|
// Log activity
|
||||||
await logActivity('event_created',
|
await logActivity('event_created',
|
||||||
@@ -133,7 +136,9 @@ router.post('/', adminAuth, [
|
|||||||
gallery_password: password,
|
gallery_password: password,
|
||||||
expiry_date: await formatDate(expires_at, emailLang),
|
expiry_date: await formatDate(expires_at, emailLang),
|
||||||
welcome_message: welcome_message || ''
|
welcome_message: welcome_message || ''
|
||||||
})
|
}),
|
||||||
|
status: 'pending',
|
||||||
|
created_at: new Date()
|
||||||
// scheduled_at will use default value
|
// scheduled_at will use default value
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.23",
|
"version": "1.0.26",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.23",
|
"version": "1.0.26",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tiptap/extension-link": "^2.25.0",
|
"@tiptap/extension-link": "^2.25.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.23",
|
"version": "1.0.26",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -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