Files
picpeak/complete-setup.sh
T
paul 288b0c25e6
Create Release / check-version-change (push) Successful in 2m25s
Automatic Version Bump / version-bump (push) Failing after 8m2s
Create Release / create-release (push) Has been skipped
refactor: rename project from wedding-photo-sharing to PicPeak
- 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>
2025-07-12 09:22:14 +02:00

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