diff --git a/complete-setup.sh b/complete-setup.sh new file mode 100644 index 0000000..47b5ba7 --- /dev/null +++ b/complete-setup.sh @@ -0,0 +1,862 @@ +#!/bin/bash +# Complete setup script to create ALL remaining files + +echo "=========================================" +echo "Wedding Photo Sharing 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.
+