288b0c25e6
- Update Docker image names and network configurations - Rename package.json project names to picpeak-backend/frontend - Update CI/CD configurations (Drone CI and GitHub Actions) - Update documentation and setup scripts - Update application branding in source code - Change default database name to picpeak - Update PM2 ecosystem config 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
863 lines
22 KiB
Bash
863 lines
22 KiB
Bash
#!/bin/bash
|
|
# Complete setup script to create ALL remaining files
|
|
|
|
echo "========================================="
|
|
echo "PicPeak Platform Setup"
|
|
echo "========================================="
|
|
echo ""
|
|
|
|
# Function to create directory if it doesn't exist
|
|
create_dir() {
|
|
if [ ! -d "$1" ]; then
|
|
mkdir -p "$1"
|
|
echo "Created directory: $1"
|
|
fi
|
|
}
|
|
|
|
# Create all necessary directories
|
|
echo "Creating directory structure..."
|
|
create_dir "backend/src/services"
|
|
create_dir "backend/src/utils"
|
|
create_dir "backend/src/routes"
|
|
create_dir "backend/migrations"
|
|
create_dir "backend/scripts"
|
|
create_dir "backend/__tests__"
|
|
create_dir "frontend/public"
|
|
create_dir "frontend/src/components"
|
|
create_dir "frontend/src/contexts"
|
|
create_dir "frontend/src/hooks"
|
|
create_dir "frontend/src/pages/admin"
|
|
create_dir "frontend/src/services"
|
|
create_dir "frontend/src/config"
|
|
create_dir "nginx/sites-enabled"
|
|
create_dir "scripts"
|
|
create_dir "storage/events/active"
|
|
create_dir "storage/events/archived"
|
|
create_dir "storage/thumbnails"
|
|
create_dir "data"
|
|
create_dir "logs"
|
|
create_dir "certbot/conf"
|
|
create_dir "certbot/www"
|
|
|
|
# Create .gitkeep files to preserve empty directories
|
|
touch storage/events/active/.gitkeep
|
|
touch storage/events/archived/.gitkeep
|
|
touch storage/thumbnails/.gitkeep
|
|
touch data/.gitkeep
|
|
touch logs/.gitkeep
|
|
|
|
echo ""
|
|
echo "Creating backend utilities..."
|
|
|
|
# Create helpers utility
|
|
cat > backend/src/utils/helpers.js << 'EOF'
|
|
const crypto = require('crypto');
|
|
const path = require('path');
|
|
|
|
function generateToken(length = 32) {
|
|
return crypto.randomBytes(length).toString('hex');
|
|
}
|
|
|
|
function sanitizeFilename(filename) {
|
|
const basename = path.basename(filename);
|
|
return basename.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
}
|
|
|
|
function formatBytes(bytes, decimals = 2) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function generateSlug(text) {
|
|
return text
|
|
.toString()
|
|
.toLowerCase()
|
|
.trim()
|
|
.replace(/[^\w\s-]/g, '')
|
|
.replace(/[\s_-]+/g, '-')
|
|
.replace(/^-+|-+$/g, '');
|
|
}
|
|
|
|
function daysBetween(date1, date2) {
|
|
const oneDay = 24 * 60 * 60 * 1000;
|
|
const firstDate = new Date(date1);
|
|
const secondDate = new Date(date2);
|
|
const diffDays = Math.round(Math.abs((firstDate - secondDate) / oneDay));
|
|
return diffDays;
|
|
}
|
|
|
|
function isValidEmail(email) {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
}
|
|
|
|
function paginate(totalItems, currentPage = 1, pageSize = 20) {
|
|
const totalPages = Math.ceil(totalItems / pageSize);
|
|
const offset = (currentPage - 1) * pageSize;
|
|
return {
|
|
totalItems,
|
|
currentPage,
|
|
pageSize,
|
|
totalPages,
|
|
offset,
|
|
hasNext: currentPage < totalPages,
|
|
hasPrev: currentPage > 1
|
|
};
|
|
}
|
|
|
|
function asyncHandler(fn) {
|
|
return (req, res, next) => {
|
|
Promise.resolve(fn(req, res, next)).catch(next);
|
|
};
|
|
}
|
|
|
|
function getClientIp(req) {
|
|
return req.headers['x-forwarded-for']?.split(',')[0] ||
|
|
req.headers['x-real-ip'] ||
|
|
req.connection.remoteAddress;
|
|
}
|
|
|
|
module.exports = {
|
|
generateToken,
|
|
sanitizeFilename,
|
|
formatBytes,
|
|
generateSlug,
|
|
daysBetween,
|
|
isValidEmail,
|
|
paginate,
|
|
asyncHandler,
|
|
getClientIp
|
|
};
|
|
EOF
|
|
|
|
echo "Creating remaining backend routes..."
|
|
|
|
# Create admin routes
|
|
cat > backend/src/routes/admin.js << 'EOF'
|
|
const express = require('express');
|
|
const bcrypt = require('bcrypt');
|
|
const { adminAuth } = require('../middleware/auth');
|
|
const { db } = require('../database/db');
|
|
const router = express.Router();
|
|
|
|
// Dashboard stats
|
|
router.get('/stats', adminAuth, async (req, res) => {
|
|
try {
|
|
const totalEvents = await db('events').count('id as count').first();
|
|
const activeEvents = await db('events').where('is_active', true).count('id as count').first();
|
|
const archivedEvents = await db('events').where('is_archived', true).count('id as count').first();
|
|
const totalPhotos = await db('photos').count('id as count').first();
|
|
|
|
const upcomingExpirations = await db('events')
|
|
.where('is_active', true)
|
|
.where('expires_at', '<=', new Date(Date.now() + 7 * 24 * 60 * 60 * 1000))
|
|
.orderBy('expires_at', 'asc')
|
|
.limit(5);
|
|
|
|
const recentActivity = await db('access_logs')
|
|
.join('events', 'access_logs.event_id', 'events.id')
|
|
.select('access_logs.*', 'events.event_name')
|
|
.orderBy('access_logs.timestamp', 'desc')
|
|
.limit(10);
|
|
|
|
res.json({
|
|
total_events: totalEvents.count,
|
|
active_events: activeEvents.count,
|
|
archived_events: archivedEvents.count,
|
|
total_photos: totalPhotos.count,
|
|
upcoming_expirations: upcomingExpirations,
|
|
recent_activity: recentActivity
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch stats' });
|
|
}
|
|
});
|
|
|
|
// Email queue management
|
|
router.get('/emails', adminAuth, async (req, res) => {
|
|
try {
|
|
const emails = await db('email_queue')
|
|
.join('events', 'email_queue.event_id', 'events.id')
|
|
.select('email_queue.*', 'events.event_name')
|
|
.orderBy('email_queue.scheduled_at', 'desc')
|
|
.limit(50);
|
|
|
|
res.json(emails);
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch emails' });
|
|
}
|
|
});
|
|
|
|
// Retry failed email
|
|
router.post('/emails/:id/retry', adminAuth, async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
await db('email_queue').where('id', id).update({
|
|
status: 'pending',
|
|
retry_count: 0,
|
|
error_message: null
|
|
});
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to retry email' });
|
|
}
|
|
});
|
|
|
|
// Archive management
|
|
router.get('/archives', adminAuth, async (req, res) => {
|
|
try {
|
|
const archives = await db('events')
|
|
.where('is_archived', true)
|
|
.select('id', 'event_name', 'event_date', 'archive_path', 'archived_at')
|
|
.orderBy('archived_at', 'desc');
|
|
|
|
res.json(archives);
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch archives' });
|
|
}
|
|
});
|
|
|
|
// Create admin user
|
|
router.post('/users', adminAuth, async (req, res) => {
|
|
try {
|
|
const { username, email, password } = req.body;
|
|
|
|
const existing = await db('admin_users')
|
|
.where('username', username)
|
|
.orWhere('email', email)
|
|
.first();
|
|
|
|
if (existing) {
|
|
return res.status(400).json({ error: 'User already exists' });
|
|
}
|
|
|
|
const password_hash = await bcrypt.hash(password, 10);
|
|
|
|
const [userId] = await db('admin_users').insert({
|
|
username,
|
|
email,
|
|
password_hash
|
|
});
|
|
|
|
res.json({ id: userId, username, email });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to create user' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|
|
EOF
|
|
|
|
echo "Creating deployment scripts..."
|
|
|
|
# Create backup script
|
|
cat > scripts/backup.sh << 'EOF'
|
|
#!/bin/bash
|
|
BACKUP_DIR="/backup/photo-sharing"
|
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
BACKUP_NAME="backup_${TIMESTAMP}"
|
|
|
|
mkdir -p "${BACKUP_DIR}/${BACKUP_NAME}"
|
|
|
|
echo "Starting backup..."
|
|
|
|
if [ -f data/photo_sharing.db ]; then
|
|
echo "Backing up SQLite database..."
|
|
cp data/photo_sharing.db "${BACKUP_DIR}/${BACKUP_NAME}/"
|
|
else
|
|
echo "Backing up PostgreSQL database..."
|
|
docker-compose -f docker-compose.prod.yml exec -T db pg_dump -U photoapp photo_sharing > "${BACKUP_DIR}/${BACKUP_NAME}/database.sql"
|
|
fi
|
|
|
|
echo "Backing up active events..."
|
|
tar -czf "${BACKUP_DIR}/${BACKUP_NAME}/active_events.tar.gz" -C storage/events active/
|
|
|
|
cp .env "${BACKUP_DIR}/${BACKUP_NAME}/"
|
|
|
|
cat > "${BACKUP_DIR}/${BACKUP_NAME}/backup_info.txt" << EOFINFO
|
|
Backup created: $(date)
|
|
Database: $([ -f data/photo_sharing.db ] && echo "photo_sharing.db" || echo "database.sql")
|
|
Active events: active_events.tar.gz
|
|
Configuration: .env
|
|
EOFINFO
|
|
|
|
cd "${BACKUP_DIR}"
|
|
tar -czf "${BACKUP_NAME}.tar.gz" "${BACKUP_NAME}/"
|
|
rm -rf "${BACKUP_NAME}/"
|
|
|
|
find "${BACKUP_DIR}" -name "backup_*.tar.gz" -mtime +30 -delete
|
|
|
|
echo "Backup completed: ${BACKUP_DIR}/${BACKUP_NAME}.tar.gz"
|
|
EOF
|
|
|
|
chmod +x scripts/backup.sh
|
|
|
|
# Create monitoring script
|
|
cat > scripts/monitoring.sh << 'EOF'
|
|
#!/bin/bash
|
|
check_service() {
|
|
SERVICE=$1
|
|
if docker-compose -f docker-compose.prod.yml ps | grep -q "${SERVICE}.*Up"; then
|
|
echo "✓ ${SERVICE} is running"
|
|
return 0
|
|
else
|
|
echo "✗ ${SERVICE} is down!"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
echo "Service Health Check"
|
|
echo "==================="
|
|
|
|
SERVICES_OK=true
|
|
|
|
check_service "backend" || SERVICES_OK=false
|
|
check_service "frontend" || SERVICES_OK=false
|
|
check_service "nginx" || SERVICES_OK=false
|
|
|
|
echo ""
|
|
echo "Disk Usage:"
|
|
df -h | grep -E '^/dev/' | awk '{print $6 ": " $5 " used"}'
|
|
|
|
FAILED_EMAILS=$(docker-compose -f docker-compose.prod.yml exec -T backend sqlite3 data/photo_sharing.db "SELECT COUNT(*) FROM email_queue WHERE status='failed' AND retry_count >= 3;" 2>/dev/null || echo "0")
|
|
if [ "$FAILED_EMAILS" -gt 0 ]; then
|
|
echo ""
|
|
echo "⚠️ Warning: $FAILED_EMAILS failed emails in queue"
|
|
fi
|
|
|
|
echo ""
|
|
echo "Upcoming Expirations:"
|
|
docker-compose -f docker-compose.prod.yml exec -T backend sqlite3 data/photo_sharing.db "SELECT event_name, date(expires_at) as expires FROM events WHERE is_active=1 AND expires_at <= datetime('now', '+7 days') ORDER BY expires_at;" 2>/dev/null || echo "No database connection"
|
|
|
|
if [ "$SERVICES_OK" = false ]; then
|
|
echo ""
|
|
echo "⚠️ Some services are down! Run 'docker-compose -f docker-compose.prod.yml up -d' to restart."
|
|
exit 1
|
|
fi
|
|
EOF
|
|
|
|
chmod +x scripts/monitoring.sh
|
|
|
|
# Create SSL setup script
|
|
cat > scripts/setup-ssl.sh << 'EOF'
|
|
#!/bin/bash
|
|
set -e
|
|
|
|
echo "SSL Certificate Setup"
|
|
echo "===================="
|
|
|
|
if [ ! -f .env ]; then
|
|
echo "Error: .env file not found. Please run install.sh first."
|
|
exit 1
|
|
fi
|
|
|
|
source .env
|
|
|
|
ADMIN_DOMAIN=$(echo $ADMIN_URL | sed 's|https://||')
|
|
FRONTEND_DOMAIN=$(echo $FRONTEND_URL | sed 's|https://||')
|
|
|
|
if [ -z "$ADMIN_DOMAIN" ] || [ -z "$FRONTEND_DOMAIN" ]; then
|
|
echo "Error: Please set ADMIN_URL and FRONTEND_URL in .env file"
|
|
exit 1
|
|
fi
|
|
|
|
sed -i "s/admin.photos.yourdomain.com/$ADMIN_DOMAIN/g" nginx/sites-enabled/default.conf
|
|
sed -i "s/photos.yourdomain.com/$FRONTEND_DOMAIN/g" nginx/sites-enabled/default.conf
|
|
|
|
read -p "Enter email for Let's Encrypt notifications: " EMAIL
|
|
|
|
docker-compose -f docker-compose.prod.yml up -d nginx
|
|
|
|
sleep 5
|
|
|
|
echo "Obtaining SSL certificates for $ADMIN_DOMAIN and $FRONTEND_DOMAIN..."
|
|
|
|
docker-compose -f docker-compose.prod.yml run --rm certbot certonly \
|
|
--webroot \
|
|
--webroot-path=/var/www/certbot \
|
|
--email $EMAIL \
|
|
--agree-tos \
|
|
--no-eff-email \
|
|
-d $ADMIN_DOMAIN \
|
|
-d $FRONTEND_DOMAIN
|
|
|
|
echo "SSL certificates obtained successfully!"
|
|
EOF
|
|
|
|
chmod +x scripts/setup-ssl.sh
|
|
|
|
# Create update script
|
|
cat > scripts/update.sh << 'EOF'
|
|
#!/bin/bash
|
|
echo "Photo Sharing Platform - Update"
|
|
echo "=============================="
|
|
|
|
echo "Creating backup before update..."
|
|
./scripts/backup.sh
|
|
|
|
echo "Pulling latest changes..."
|
|
git pull origin main
|
|
|
|
echo "Rebuilding services..."
|
|
docker-compose -f docker-compose.prod.yml build
|
|
|
|
echo "Restarting services..."
|
|
docker-compose -f docker-compose.prod.yml down
|
|
docker-compose -f docker-compose.prod.yml up -d
|
|
|
|
echo "Running database migrations..."
|
|
docker-compose -f docker-compose.prod.yml exec backend npm run migrate
|
|
|
|
echo "Update completed successfully!"
|
|
EOF
|
|
|
|
chmod +x scripts/update.sh
|
|
|
|
echo ""
|
|
echo "Creating frontend files..."
|
|
|
|
# Create frontend Dockerfile
|
|
cat > frontend/Dockerfile << 'EOF'
|
|
FROM node:18-alpine AS builder
|
|
|
|
WORKDIR /app
|
|
|
|
COPY package*.json ./
|
|
RUN npm ci
|
|
|
|
COPY . .
|
|
RUN npm run build
|
|
|
|
FROM nginx:alpine
|
|
|
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
COPY --from=builder /app/build /usr/share/nginx/html
|
|
|
|
EXPOSE 80
|
|
|
|
CMD ["nginx", "-g", "daemon off;"]
|
|
EOF
|
|
|
|
# Create frontend nginx.conf
|
|
cat > frontend/nginx.conf << 'EOF'
|
|
server {
|
|
listen 80;
|
|
server_name localhost;
|
|
root /usr/share/nginx/html;
|
|
index index.html;
|
|
|
|
location / {
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
|
|
location /api {
|
|
proxy_pass http://backend:3000;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection 'upgrade';
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_cache_bypass $http_upgrade;
|
|
}
|
|
|
|
location /photos {
|
|
proxy_pass http://backend:3000;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
}
|
|
EOF
|
|
|
|
# Create minimal frontend files to get started
|
|
cat > frontend/public/index.html << 'EOF'
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<meta name="theme-color" content="#000000" />
|
|
<meta name="description" content="Share your event photos securely" />
|
|
<title>Photo Gallery</title>
|
|
</head>
|
|
<body>
|
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
<div id="root"></div>
|
|
</body>
|
|
</html>
|
|
EOF
|
|
|
|
# Create basic frontend files
|
|
cat > frontend/src/index.js << 'EOF'
|
|
import React from 'react';
|
|
import ReactDOM from 'react-dom/client';
|
|
import './index.css';
|
|
import App from './App';
|
|
|
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
root.render(
|
|
<React.StrictMode>
|
|
<App />
|
|
</React.StrictMode>
|
|
);
|
|
EOF
|
|
|
|
cat > frontend/src/App.js << 'EOF'
|
|
import React from 'react';
|
|
|
|
function App() {
|
|
return (
|
|
<div className="App">
|
|
<h1>Photo Sharing Platform</h1>
|
|
<p>Setup in progress. Please complete the frontend implementation.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|
|
EOF
|
|
|
|
cat > frontend/src/index.css << 'EOF'
|
|
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
body {
|
|
margin: 0;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
sans-serif;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
EOF
|
|
|
|
# Create Tailwind config
|
|
cat > frontend/tailwind.config.js << 'EOF'
|
|
module.exports = {
|
|
content: [
|
|
"./src/**/*.{js,jsx,ts,tsx}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
wedding: {
|
|
primary: '#d4a574',
|
|
secondary: '#f3e5d0',
|
|
accent: '#8b7355'
|
|
}
|
|
}
|
|
},
|
|
},
|
|
plugins: [],
|
|
}
|
|
EOF
|
|
|
|
# Create postcss config
|
|
cat > frontend/postcss.config.js << 'EOF'
|
|
module.exports = {
|
|
plugins: {
|
|
tailwindcss: {},
|
|
autoprefixer: {},
|
|
},
|
|
}
|
|
EOF
|
|
|
|
# Create nginx site config
|
|
cat > nginx/sites-enabled/default.conf << 'EOF'
|
|
# Redirect HTTP to HTTPS
|
|
server {
|
|
listen 80;
|
|
server_name admin.photos.yourdomain.com photos.yourdomain.com;
|
|
|
|
location /.well-known/acme-challenge/ {
|
|
root /var/www/certbot;
|
|
}
|
|
|
|
location / {
|
|
return 301 https://$server_name$request_uri;
|
|
}
|
|
}
|
|
|
|
# Admin backend
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name admin.photos.yourdomain.com;
|
|
|
|
ssl_certificate /etc/letsencrypt/live/admin.photos.yourdomain.com/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/admin.photos.yourdomain.com/privkey.pem;
|
|
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
ssl_prefer_server_ciphers off;
|
|
|
|
client_max_body_size 100M;
|
|
|
|
location / {
|
|
limit_req zone=general burst=20 nodelay;
|
|
|
|
proxy_pass http://backend:3000;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection 'upgrade';
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_cache_bypass $http_upgrade;
|
|
}
|
|
|
|
location /api/auth {
|
|
limit_req zone=auth burst=5 nodelay;
|
|
|
|
proxy_pass http://backend:3000;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
}
|
|
|
|
# Public frontend
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name photos.yourdomain.com;
|
|
|
|
ssl_certificate /etc/letsencrypt/live/photos.yourdomain.com/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/photos.yourdomain.com/privkey.pem;
|
|
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
ssl_prefer_server_ciphers off;
|
|
|
|
location / {
|
|
limit_req zone=general burst=20 nodelay;
|
|
proxy_pass http://frontend;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_cache_bypass $http_upgrade;
|
|
}
|
|
|
|
location /api {
|
|
limit_req zone=general burst=20 nodelay;
|
|
|
|
proxy_pass http://backend:3000;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection 'upgrade';
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_cache_bypass $http_upgrade;
|
|
}
|
|
|
|
location /photos {
|
|
proxy_pass http://backend:3000;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
# Cache images
|
|
proxy_cache_valid 200 30d;
|
|
add_header Cache-Control "public, max-age=2592000";
|
|
}
|
|
}
|
|
EOF
|
|
|
|
# Create DEPLOYMENT.md
|
|
cat > DEPLOYMENT.md << 'EOF'
|
|
# Production Deployment Guide
|
|
|
|
## System Requirements
|
|
|
|
- Ubuntu 20.04+ or similar Linux distribution
|
|
- 2GB RAM minimum (4GB recommended)
|
|
- 20GB storage minimum
|
|
- Docker and Docker Compose
|
|
- Valid domain names with DNS configured
|
|
|
|
## Step-by-Step Deployment
|
|
|
|
### 1. Server Preparation
|
|
|
|
```bash
|
|
# Update system
|
|
sudo apt update && sudo apt upgrade -y
|
|
|
|
# Install required packages
|
|
sudo apt install -y git curl ufw
|
|
|
|
# Configure firewall
|
|
sudo ufw allow 22/tcp
|
|
sudo ufw allow 80/tcp
|
|
sudo ufw allow 443/tcp
|
|
sudo ufw enable
|
|
```
|
|
|
|
### 2. Clone and Install
|
|
|
|
```bash
|
|
# Clone repository
|
|
cd /opt
|
|
sudo git clone https://github.com/yourusername/photo-sharing-platform.git
|
|
cd photo-sharing-platform
|
|
|
|
# Run installation script
|
|
sudo ./scripts/install.sh
|
|
```
|
|
|
|
### 3. Configuration
|
|
|
|
Edit `.env` file:
|
|
```bash
|
|
sudo nano .env
|
|
```
|
|
|
|
Required settings:
|
|
```env
|
|
# URLs (use your actual domains)
|
|
ADMIN_URL=https://admin.photos.yourdomain.com
|
|
FRONTEND_URL=https://photos.yourdomain.com
|
|
|
|
# Email Configuration
|
|
SMTP_HOST=smtp.gmail.com
|
|
SMTP_PORT=587
|
|
SMTP_USER=your-email@gmail.com
|
|
SMTP_PASS=your-app-password
|
|
EMAIL_FROM=noreply@yourdomain.com
|
|
```
|
|
|
|
### 4. SSL Certificate Setup
|
|
|
|
```bash
|
|
# Configure SSL
|
|
sudo ./scripts/setup-ssl.sh
|
|
```
|
|
|
|
### 5. Start Services
|
|
|
|
```bash
|
|
# Build and start all services
|
|
docker-compose -f docker-compose.prod.yml build
|
|
docker-compose -f docker-compose.prod.yml up -d
|
|
|
|
# Initialize database
|
|
docker-compose -f docker-compose.prod.yml exec backend npm run migrate
|
|
```
|
|
|
|
### 6. Verify Deployment
|
|
|
|
1. Check service status:
|
|
```bash
|
|
docker-compose -f docker-compose.prod.yml ps
|
|
```
|
|
|
|
2. View logs:
|
|
```bash
|
|
docker-compose -f docker-compose.prod.yml logs -f
|
|
```
|
|
|
|
3. Access sites:
|
|
- Admin panel: https://admin.photos.yourdomain.com
|
|
- Public gallery: https://photos.yourdomain.com
|
|
|
|
## Post-Deployment
|
|
|
|
### Configure Automatic Backups
|
|
|
|
```bash
|
|
# Add to crontab
|
|
sudo crontab -e
|
|
|
|
# Add this line for daily backups at 2 AM
|
|
0 2 * * * /opt/photo-sharing-platform/scripts/backup.sh
|
|
```
|
|
|
|
### Set Up Monitoring
|
|
|
|
```bash
|
|
# Add health check to crontab
|
|
*/5 * * * * /opt/photo-sharing-platform/scripts/monitoring.sh
|
|
```
|
|
|
|
## Security Recommendations
|
|
|
|
1. Change default admin password immediately
|
|
2. Configure firewall rules
|
|
3. Enable automatic security updates
|
|
4. Monitor access logs regularly
|
|
|
|
## Troubleshooting
|
|
|
|
### Services won't start
|
|
```bash
|
|
# Check logs
|
|
docker-compose -f docker-compose.prod.yml logs backend
|
|
docker-compose -f docker-compose.prod.yml logs frontend
|
|
|
|
# Restart services
|
|
docker-compose -f docker-compose.prod.yml restart
|
|
```
|
|
|
|
### Email not sending
|
|
1. Check SMTP settings in `.env`
|
|
2. View email queue in admin panel
|
|
3. Check logs: `docker-compose logs backend | grep email`
|
|
|
|
### SSL certificate issues
|
|
```bash
|
|
# Renew certificates
|
|
docker-compose -f docker-compose.prod.yml run --rm certbot renew
|
|
```
|
|
EOF
|
|
|
|
# Set all script permissions
|
|
chmod +x scripts/*.sh
|
|
|
|
echo ""
|
|
echo "========================================="
|
|
echo "✅ Setup Complete!"
|
|
echo "========================================="
|
|
echo ""
|
|
echo "All core files have been created. The platform structure is ready."
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo "1. Install dependencies:"
|
|
echo " cd backend && npm install"
|
|
echo " cd ../frontend && npm install"
|
|
echo ""
|
|
echo "2. Create a .env file from .env.example:"
|
|
echo " cp .env.example .env"
|
|
echo " nano .env # Edit with your settings"
|
|
echo ""
|
|
echo "3. Start development environment:"
|
|
echo " docker-compose up"
|
|
echo ""
|
|
echo "4. For production deployment:"
|
|
echo " Follow the instructions in DEPLOYMENT.md"
|
|
echo ""
|
|
echo "Note: The frontend is a basic skeleton. You'll need to implement:"
|
|
echo "- Authentication context (AuthContext.js)"
|
|
echo "- Page components (Login, Gallery, Admin pages)"
|
|
echo "- API service layer"
|
|
echo "- UI components"
|
|
echo ""
|
|
echo "All backend functionality is complete and ready to use!"
|
|
echo ""
|
|
echo "Default admin credentials: admin / admin123 (change immediately!)"
|