From 85e7fbe73f478b6aae5a30444f2d6e5ab157934e Mon Sep 17 00:00:00 2001 From: paul Date: Mon, 14 Jul 2025 20:22:52 +0200 Subject: [PATCH] chore: clean up unnecessary deployment files - Remove deploy/ folder with complex Docker Swarm configs - Remove local development scripts referencing non-existent docker-compose.local.yml - Remove setup scripts for files that already exist - Remove docker-compose.dev.yml in favor of unified approach - Keep certbot/, CHANGELOG.md, docker-compose.prod.yml, and production guide This simplifies the repository structure and removes confusing duplicate deployment approaches, making it clearer for new users. --- .gitea/workflows/mirror-to-github.yml | 1 + README-LOCAL.md | 130 --- complete-setup.sh | 862 ------------------ deploy/docker-stack.yml | 265 ------ .../monitoring/docker-compose.monitoring.yml | 189 ---- deploy/monitoring/prometheus.yml | 65 -- deploy/scripts/backup.sh | 134 --- deploy/scripts/create-secrets.sh | 111 --- deploy/scripts/deploy.sh | 196 ---- deploy/scripts/init-swarm.sh | 70 -- deploy/traefik/docker-compose.traefik.yml | 124 --- deploy/traefik/traefik.yml | 93 -- docker-compose.dev.yml | 61 -- setup-remaining-files.sh | 380 -------- start-local.sh | 111 --- stop-local.sh | 22 - update-local.sh | 94 -- 17 files changed, 1 insertion(+), 2907 deletions(-) delete mode 100644 README-LOCAL.md delete mode 100644 complete-setup.sh delete mode 100644 deploy/docker-stack.yml delete mode 100644 deploy/monitoring/docker-compose.monitoring.yml delete mode 100644 deploy/monitoring/prometheus.yml delete mode 100755 deploy/scripts/backup.sh delete mode 100755 deploy/scripts/create-secrets.sh delete mode 100755 deploy/scripts/deploy.sh delete mode 100755 deploy/scripts/init-swarm.sh delete mode 100644 deploy/traefik/docker-compose.traefik.yml delete mode 100644 deploy/traefik/traefik.yml delete mode 100644 docker-compose.dev.yml delete mode 100755 setup-remaining-files.sh delete mode 100755 start-local.sh delete mode 100755 stop-local.sh delete mode 100755 update-local.sh diff --git a/.gitea/workflows/mirror-to-github.yml b/.gitea/workflows/mirror-to-github.yml index 16702d7..ac4ef95 100644 --- a/.gitea/workflows/mirror-to-github.yml +++ b/.gitea/workflows/mirror-to-github.yml @@ -41,6 +41,7 @@ jobs: 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 diff --git a/README-LOCAL.md b/README-LOCAL.md deleted file mode 100644 index ae9c9b7..0000000 --- a/README-LOCAL.md +++ /dev/null @@ -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 -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! 🎨 \ No newline at end of file diff --git a/complete-setup.sh b/complete-setup.sh deleted file mode 100644 index 57a2a77..0000000 --- a/complete-setup.sh +++ /dev/null @@ -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' - - - - - - - - - Photo Gallery - - - -
- - -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( - - - -); -EOF - -cat > frontend/src/App.js << 'EOF' -import React from 'react'; - -function App() { - return ( -
-

Photo Sharing Platform

-

Setup in progress. Please complete the frontend implementation.

-
- ); -} - -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!)" diff --git a/deploy/docker-stack.yml b/deploy/docker-stack.yml deleted file mode 100644 index 69ebb71..0000000 --- a/deploy/docker-stack.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/deploy/monitoring/docker-compose.monitoring.yml b/deploy/monitoring/docker-compose.monitoring.yml deleted file mode 100644 index 83dc70c..0000000 --- a/deploy/monitoring/docker-compose.monitoring.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/deploy/monitoring/prometheus.yml b/deploy/monitoring/prometheus.yml deleted file mode 100644 index df78b22..0000000 --- a/deploy/monitoring/prometheus.yml +++ /dev/null @@ -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'] \ No newline at end of file diff --git a/deploy/scripts/backup.sh b/deploy/scripts/backup.sh deleted file mode 100755 index 20abbf2..0000000 --- a/deploy/scripts/backup.sh +++ /dev/null @@ -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}" \ No newline at end of file diff --git a/deploy/scripts/create-secrets.sh b/deploy/scripts/create-secrets.sh deleted file mode 100755 index 4bf0c56..0000000 --- a/deploy/scripts/create-secrets.sh +++ /dev/null @@ -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" \ No newline at end of file diff --git a/deploy/scripts/deploy.sh b/deploy/scripts/deploy.sh deleted file mode 100755 index 799edfb..0000000 --- a/deploy/scripts/deploy.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/deploy/scripts/init-swarm.sh b/deploy/scripts/init-swarm.sh deleted file mode 100755 index 36fa359..0000000 --- a/deploy/scripts/init-swarm.sh +++ /dev/null @@ -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" \ No newline at end of file diff --git a/deploy/traefik/docker-compose.traefik.yml b/deploy/traefik/docker-compose.traefik.yml deleted file mode 100644 index 660e38e..0000000 --- a/deploy/traefik/docker-compose.traefik.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/deploy/traefik/traefik.yml b/deploy/traefik/traefik.yml deleted file mode 100644 index ceeabe0..0000000 --- a/deploy/traefik/traefik.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 51a4c40..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/setup-remaining-files.sh b/setup-remaining-files.sh deleted file mode 100755 index 45f0bae..0000000 --- a/setup-remaining-files.sh +++ /dev/null @@ -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" diff --git a/start-local.sh b/start-local.sh deleted file mode 100755 index 5cf1283..0000000 --- a/start-local.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/stop-local.sh b/stop-local.sh deleted file mode 100755 index edc95d3..0000000 --- a/stop-local.sh +++ /dev/null @@ -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" \ No newline at end of file diff --git a/update-local.sh b/update-local.sh deleted file mode 100755 index 8280f89..0000000 --- a/update-local.sh +++ /dev/null @@ -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}" \ No newline at end of file