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' + + + + + + + + + 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!)"