#!/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'
Setup in progress. Please complete the frontend implementation.