Add new build and database setup scripts to package.json for production
This commit is contained in:
69
.do/app.yaml
Normal file
69
.do/app.yaml
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
name: scriptshare
|
||||||
|
region: nyc
|
||||||
|
|
||||||
|
# Static site for the frontend
|
||||||
|
static_sites:
|
||||||
|
- name: scriptshare-frontend
|
||||||
|
github:
|
||||||
|
repo: your-username/scriptshare-cursor
|
||||||
|
branch: main
|
||||||
|
build_command: npm install && npm run build
|
||||||
|
output_dir: dist
|
||||||
|
environment_slug: node-js
|
||||||
|
source_dir: /
|
||||||
|
routes:
|
||||||
|
- path: /
|
||||||
|
envs:
|
||||||
|
- key: VITE_APP_NAME
|
||||||
|
value: ScriptShare
|
||||||
|
- key: VITE_APP_URL
|
||||||
|
value: ${APP_URL}
|
||||||
|
- key: VITE_API_URL
|
||||||
|
value: ${scriptshare-api.PUBLIC_URL}
|
||||||
|
- key: VITE_ANALYTICS_ENABLED
|
||||||
|
value: "true"
|
||||||
|
|
||||||
|
# Backend API service
|
||||||
|
services:
|
||||||
|
- name: scriptshare-api
|
||||||
|
github:
|
||||||
|
repo: your-username/scriptshare-cursor
|
||||||
|
branch: main
|
||||||
|
source_dir: /
|
||||||
|
dockerfile_path: Dockerfile.api
|
||||||
|
environment_slug: node-js
|
||||||
|
instance_count: 1
|
||||||
|
instance_size_slug: basic-xxs
|
||||||
|
http_port: 3000
|
||||||
|
routes:
|
||||||
|
- path: /api
|
||||||
|
health_check:
|
||||||
|
http_path: /api/health
|
||||||
|
envs:
|
||||||
|
- key: NODE_ENV
|
||||||
|
value: production
|
||||||
|
- key: PORT
|
||||||
|
value: "3000"
|
||||||
|
- key: DATABASE_URL
|
||||||
|
value: ${scriptshare-db.DATABASE_URL}
|
||||||
|
- key: JWT_SECRET
|
||||||
|
value: ${JWT_SECRET}
|
||||||
|
- key: CORS_ORIGIN
|
||||||
|
value: ${scriptshare-frontend.PUBLIC_URL}
|
||||||
|
|
||||||
|
# Managed MySQL database
|
||||||
|
databases:
|
||||||
|
- name: scriptshare-db
|
||||||
|
engine: MYSQL
|
||||||
|
version: "8"
|
||||||
|
size: db-s-1vcpu-1gb
|
||||||
|
num_nodes: 1
|
||||||
|
|
||||||
|
# Environment variables (these will be set in DigitalOcean dashboard)
|
||||||
|
envs:
|
||||||
|
- key: JWT_SECRET
|
||||||
|
scope: RUN_AND_BUILD_TIME
|
||||||
|
type: SECRET
|
||||||
|
- key: APP_URL
|
||||||
|
scope: RUN_AND_BUILD_TIME
|
||||||
|
value: https://scriptshare-frontend-${APP_DOMAIN}
|
102
.github/workflows/deploy.yml
vendored
Normal file
102
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
name: Deploy to DigitalOcean
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Test job to run before deployment
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: npm run build
|
||||||
|
env:
|
||||||
|
VITE_APP_NAME: ScriptShare
|
||||||
|
VITE_APP_URL: https://scriptshare.example.com
|
||||||
|
VITE_ANALYTICS_ENABLED: true
|
||||||
|
|
||||||
|
# Deploy job (only on main branch)
|
||||||
|
deploy:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install doctl
|
||||||
|
uses: digitalocean/action-doctl@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Get app info
|
||||||
|
id: app-info
|
||||||
|
run: |
|
||||||
|
APP_ID=$(doctl apps list --format ID,Spec.Name --no-header | grep scriptshare | cut -d' ' -f1)
|
||||||
|
echo "app-id=$APP_ID" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Trigger deployment
|
||||||
|
run: |
|
||||||
|
doctl apps create-deployment ${{ steps.app-info.outputs.app-id }} --wait
|
||||||
|
|
||||||
|
- name: Run database migrations
|
||||||
|
run: |
|
||||||
|
# Wait for deployment to complete
|
||||||
|
sleep 60
|
||||||
|
|
||||||
|
# Run migrations via the app console (if needed)
|
||||||
|
echo "Deployment completed. Please run database migrations manually if this is the first deployment."
|
||||||
|
echo "Command: npm run db:setup:prod"
|
||||||
|
|
||||||
|
# Database migration job (manual trigger)
|
||||||
|
migrate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run migrations
|
||||||
|
run: npm run db:migrate:prod
|
||||||
|
env:
|
||||||
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||||
|
|
||||||
|
# Manual workflow dispatch for running migrations
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
action:
|
||||||
|
description: 'Action to perform'
|
||||||
|
required: true
|
||||||
|
default: 'migrate'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- migrate
|
||||||
|
- setup
|
248
DEPLOYMENT.md
Normal file
248
DEPLOYMENT.md
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
# DigitalOcean Deployment Guide
|
||||||
|
|
||||||
|
This guide walks you through deploying ScriptShare to DigitalOcean App Platform with a managed MySQL database.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- DigitalOcean account
|
||||||
|
- GitHub repository containing your code
|
||||||
|
- Basic familiarity with DigitalOcean App Platform
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
The deployment consists of:
|
||||||
|
- **Frontend**: Static site (React/Vite build)
|
||||||
|
- **Backend API**: Node.js service with Express
|
||||||
|
- **Database**: DigitalOcean Managed MySQL Database
|
||||||
|
|
||||||
|
## Step 1: Prepare Your Repository
|
||||||
|
|
||||||
|
1. Ensure all the deployment files are in your GitHub repository:
|
||||||
|
```
|
||||||
|
.do/app.yaml # App Platform configuration
|
||||||
|
Dockerfile.api # Backend API container
|
||||||
|
drizzle.config.production.ts # Production DB config
|
||||||
|
scripts/migrate-production.js # Migration script
|
||||||
|
scripts/setup-production-db.js # DB setup script
|
||||||
|
env.production.example # Environment variables template
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Commit and push all changes to your main branch.
|
||||||
|
|
||||||
|
## Step 2: Create the DigitalOcean App
|
||||||
|
|
||||||
|
### Option A: Using the DigitalOcean Console
|
||||||
|
|
||||||
|
1. Go to the [DigitalOcean App Platform](https://cloud.digitalocean.com/apps)
|
||||||
|
2. Click **"Create App"**
|
||||||
|
3. Choose **"GitHub"** as your source
|
||||||
|
4. Select your repository and branch (usually `main`)
|
||||||
|
5. DigitalOcean will automatically detect the `app.yaml` configuration
|
||||||
|
|
||||||
|
### Option B: Using the CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install doctl CLI
|
||||||
|
# On macOS: brew install doctl
|
||||||
|
# On Linux: snap install doctl
|
||||||
|
|
||||||
|
# Authenticate
|
||||||
|
doctl auth init
|
||||||
|
|
||||||
|
# Create the app
|
||||||
|
doctl apps create --spec .do/app.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Configure Environment Variables
|
||||||
|
|
||||||
|
In the DigitalOcean dashboard, go to your app's Settings > Environment Variables and set:
|
||||||
|
|
||||||
|
### Required Variables
|
||||||
|
```
|
||||||
|
JWT_SECRET=your-super-secret-jwt-key-here-change-this-in-production
|
||||||
|
ADMIN_EMAIL=admin@yourcompany.com
|
||||||
|
ADMIN_USERNAME=admin
|
||||||
|
ADMIN_PASSWORD=your-secure-password
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional Variables
|
||||||
|
```
|
||||||
|
VITE_ANALYTICS_ENABLED=true
|
||||||
|
RATE_LIMIT_ENABLED=true
|
||||||
|
RATE_LIMIT_WINDOW_MS=900000
|
||||||
|
RATE_LIMIT_MAX_REQUESTS=100
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Security Note**: Generate a strong JWT secret:
|
||||||
|
```bash
|
||||||
|
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Database Setup
|
||||||
|
|
||||||
|
The managed MySQL database will be automatically created. After the first deployment:
|
||||||
|
|
||||||
|
1. **Run Database Migrations**:
|
||||||
|
```bash
|
||||||
|
# In your app's console (or via GitHub Actions)
|
||||||
|
npm run db:setup:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify Database Connection**:
|
||||||
|
Check the API health endpoint: `https://your-api-url/api/health`
|
||||||
|
|
||||||
|
## Step 5: Update App Configuration
|
||||||
|
|
||||||
|
1. **Update Frontend URLs**: After deployment, update the environment variables with actual URLs:
|
||||||
|
```
|
||||||
|
VITE_APP_URL=https://your-frontend-url.ondigitalocean.app
|
||||||
|
VITE_API_URL=https://your-api-url.ondigitalocean.app/api
|
||||||
|
CORS_ORIGIN=https://your-frontend-url.ondigitalocean.app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Redeploy**: The app will automatically redeploy when you change environment variables.
|
||||||
|
|
||||||
|
## Step 6: Custom Domain (Optional)
|
||||||
|
|
||||||
|
1. In your app settings, go to **Domains**
|
||||||
|
2. Click **Add Domain**
|
||||||
|
3. Enter your domain name
|
||||||
|
4. Configure DNS records as instructed
|
||||||
|
|
||||||
|
## Database Management
|
||||||
|
|
||||||
|
### Connecting to the Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get connection string from DigitalOcean dashboard
|
||||||
|
mysql -h your-db-host -P 25060 -u your-username -p your-database-name --ssl-mode=REQUIRED
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Production migration
|
||||||
|
npm run db:migrate:prod
|
||||||
|
|
||||||
|
# Create new migration
|
||||||
|
npm run db:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup and Restore
|
||||||
|
|
||||||
|
DigitalOcean provides automatic daily backups. For manual backups:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create backup
|
||||||
|
mysqldump -h your-db-host -P 25060 -u your-username -p your-database-name --ssl-mode=REQUIRED > backup.sql
|
||||||
|
|
||||||
|
# Restore backup
|
||||||
|
mysql -h your-db-host -P 25060 -u your-username -p your-database-name --ssl-mode=REQUIRED < backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring and Logs
|
||||||
|
|
||||||
|
### Application Logs
|
||||||
|
- View logs in DigitalOcean Console: App → Runtime Logs
|
||||||
|
- Or via CLI: `doctl apps logs <app-id> --type=run`
|
||||||
|
|
||||||
|
### Database Monitoring
|
||||||
|
- Database metrics available in DigitalOcean dashboard
|
||||||
|
- Set up alerts for CPU, memory, and connection usage
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
- API: `https://your-api-url/api/health`
|
||||||
|
- Frontend: Built-in App Platform health checks
|
||||||
|
|
||||||
|
## Scaling
|
||||||
|
|
||||||
|
### Vertical Scaling
|
||||||
|
- Increase instance size in App Platform settings
|
||||||
|
- Database can be scaled up (not down) in database settings
|
||||||
|
|
||||||
|
### Horizontal Scaling
|
||||||
|
- Increase instance count for API service
|
||||||
|
- Frontend is automatically scaled as a static site
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Environment Variables**: Never commit secrets to Git
|
||||||
|
2. **Database Access**: Use DigitalOcean's private networking
|
||||||
|
3. **SSL/TLS**: Enabled by default on App Platform
|
||||||
|
4. **Database Backups**: Verify daily backups are working
|
||||||
|
5. **Access Control**: Use DigitalOcean teams for access management
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Build Failures**:
|
||||||
|
```bash
|
||||||
|
# Check build logs
|
||||||
|
doctl apps logs <app-id> --type=build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Database Connection Issues**:
|
||||||
|
- Verify DATABASE_URL format
|
||||||
|
- Check firewall settings
|
||||||
|
- Ensure SSL is enabled
|
||||||
|
|
||||||
|
3. **CORS Errors**:
|
||||||
|
- Verify CORS_ORIGIN matches frontend URL
|
||||||
|
- Check environment variable casing
|
||||||
|
|
||||||
|
4. **Missing Dependencies**:
|
||||||
|
```bash
|
||||||
|
# Clear npm cache and rebuild
|
||||||
|
npm ci --clean-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Support
|
||||||
|
|
||||||
|
- DigitalOcean Community: https://www.digitalocean.com/community
|
||||||
|
- Support tickets: https://cloud.digitalocean.com/support
|
||||||
|
- Documentation: https://docs.digitalocean.com/products/app-platform/
|
||||||
|
|
||||||
|
## Cost Optimization
|
||||||
|
|
||||||
|
### Current Configuration Costs (Approximate)
|
||||||
|
- **API Service**: $5/month (Basic plan)
|
||||||
|
- **Database**: $15/month (1GB RAM, 1 vCPU)
|
||||||
|
- **Static Site**: $0 (included with API service)
|
||||||
|
- **Total**: ~$20/month
|
||||||
|
|
||||||
|
### Cost Reduction Tips
|
||||||
|
1. Use development database for testing
|
||||||
|
2. Scale down during low usage periods
|
||||||
|
3. Monitor and optimize database queries
|
||||||
|
4. Use CDN for static assets
|
||||||
|
|
||||||
|
## Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] Repository configured with deployment files
|
||||||
|
- [ ] Environment variables set in DigitalOcean
|
||||||
|
- [ ] JWT secret generated and configured
|
||||||
|
- [ ] Database migrations run successfully
|
||||||
|
- [ ] Health check endpoints responding
|
||||||
|
- [ ] Frontend can communicate with API
|
||||||
|
- [ ] Admin user created and accessible
|
||||||
|
- [ ] Custom domain configured (if applicable)
|
||||||
|
- [ ] Monitoring and alerts set up
|
||||||
|
- [ ] Backup strategy verified
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Set up CI/CD**: Configure GitHub Actions for automated deployments
|
||||||
|
2. **Monitoring**: Set up application performance monitoring
|
||||||
|
3. **CDN**: Configure CDN for static assets
|
||||||
|
4. **Analytics**: Integrate application analytics
|
||||||
|
5. **Error Tracking**: Set up error monitoring service
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues specific to this deployment setup, please check:
|
||||||
|
1. DigitalOcean App Platform documentation
|
||||||
|
2. Application logs in the DigitalOcean console
|
||||||
|
3. Database connection and query logs
|
||||||
|
|
||||||
|
Remember to regularly update dependencies and monitor security advisories for your application stack.
|
224
Dockerfile.api
Normal file
224
Dockerfile.api
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
# Production API Dockerfile for DigitalOcean
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
# Install system dependencies for native modules
|
||||||
|
RUN apk add --no-cache python3 make g++ libc6-compat
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files first for better caching
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci --only=production=false --silent
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create API-only build by removing frontend dependencies and files
|
||||||
|
RUN npm uninstall @vitejs/plugin-react-swc vite
|
||||||
|
RUN rm -rf src/components src/pages src/contexts src/hooks/use-toast.ts src/utils/toast.ts
|
||||||
|
RUN rm -rf src/main.tsx src/App.tsx src/index.css
|
||||||
|
RUN rm -rf public index.html vite.config.ts tailwind.config.ts postcss.config.js
|
||||||
|
|
||||||
|
# Keep only API and database files
|
||||||
|
# The structure will be: src/lib/api/* and src/lib/db/*
|
||||||
|
|
||||||
|
# Create a simple Express server
|
||||||
|
RUN cat > src/server.ts << 'EOF'
|
||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import { createUser, getUserByEmail, updateUser, getAllUsers, getUserById } from './lib/api/users.js';
|
||||||
|
import { getScripts, getScriptById, createScript, updateScript, deleteScript, moderateScript } from './lib/api/scripts.js';
|
||||||
|
import { login, register, refreshToken } from './lib/api/auth.js';
|
||||||
|
import { rateScript, getUserRating, getScriptRatingStats } from './lib/api/ratings.js';
|
||||||
|
import { getPlatformAnalytics, getScriptAnalytics, trackEvent } from './lib/api/analytics.js';
|
||||||
|
import { createCollection, getUserCollections, getPublicCollections, addScriptToCollection } from './lib/api/collections.js';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors({
|
||||||
|
origin: process.env.CORS_ORIGIN || '*',
|
||||||
|
credentials: true
|
||||||
|
}));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auth routes
|
||||||
|
app.post('/api/auth/login', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await login(req.body);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/auth/register', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await register(req.body);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Register error:', error);
|
||||||
|
res.status(400).json({ error: 'Registration failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scripts routes
|
||||||
|
app.get('/api/scripts', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await getScripts(req.query);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get scripts error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch scripts' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/scripts/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const script = await getScriptById(req.params.id);
|
||||||
|
if (!script) {
|
||||||
|
return res.status(404).json({ error: 'Script not found' });
|
||||||
|
}
|
||||||
|
res.json(script);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get script error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch script' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/scripts', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.headers['x-user-id'] as string;
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
const result = await createScript(req.body, userId);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create script error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to create script' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Users routes
|
||||||
|
app.get('/api/users', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await getAllUsers();
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get users error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch users' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/users/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await getUserById(req.params.id);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
res.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get user error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch user' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Analytics routes
|
||||||
|
app.get('/api/analytics/platform', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const days = parseInt(req.query.days as string) || 30;
|
||||||
|
const result = await getPlatformAnalytics(days);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Analytics error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch analytics' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/analytics/track', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await trackEvent(req.body);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Track event error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to track event' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collections routes
|
||||||
|
app.get('/api/collections', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.headers['x-user-id'] as string;
|
||||||
|
const result = userId ? await getUserCollections(userId) : await getPublicCollections();
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get collections error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch collections' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ratings routes
|
||||||
|
app.post('/api/ratings', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await rateScript(req.body);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Rate script error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to rate script' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/scripts/:id/ratings', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await getScriptRatingStats(req.params.id);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get ratings error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch ratings' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((error: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
console.error('Unhandled error:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use('*', (req, res) => {
|
||||||
|
res.status(404).json({ error: 'Endpoint not found' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`ScriptShare API server running on port ${PORT}`);
|
||||||
|
console.log(`Environment: ${process.env.NODE_ENV}`);
|
||||||
|
console.log(`Database URL configured: ${!!process.env.DATABASE_URL}`);
|
||||||
|
});
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Install Express and CORS for the API server
|
||||||
|
RUN npm install express cors @types/express @types/cors
|
||||||
|
|
||||||
|
# Build TypeScript (if any TS files remain)
|
||||||
|
RUN npx tsc --build || echo "TypeScript build completed with warnings"
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:3000/api/health || exit 1
|
||||||
|
|
||||||
|
# Start the API server
|
||||||
|
CMD ["node", "src/server.js"]
|
16
drizzle.config.production.ts
Normal file
16
drizzle.config.production.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: './src/lib/db/schema.ts',
|
||||||
|
out: './drizzle',
|
||||||
|
dialect: 'mysql',
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL!,
|
||||||
|
},
|
||||||
|
verbose: true,
|
||||||
|
strict: true,
|
||||||
|
// DigitalOcean Managed Database specific settings
|
||||||
|
introspect: {
|
||||||
|
casing: 'camel'
|
||||||
|
}
|
||||||
|
});
|
40
env.production.example
Normal file
40
env.production.example
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Production Environment Configuration for DigitalOcean
|
||||||
|
|
||||||
|
# Database Configuration (will be replaced by DigitalOcean DATABASE_URL)
|
||||||
|
DATABASE_URL="mysql://username:password@hostname:port/database_name"
|
||||||
|
|
||||||
|
# JWT Secret for authentication (SET THIS IN DIGITALOCEAN DASHBOARD)
|
||||||
|
JWT_SECRET="your-super-secret-jwt-key-here-change-this-in-production"
|
||||||
|
|
||||||
|
# App Configuration
|
||||||
|
NODE_ENV="production"
|
||||||
|
PORT="3000"
|
||||||
|
|
||||||
|
# Frontend Configuration
|
||||||
|
VITE_APP_NAME="ScriptShare"
|
||||||
|
VITE_APP_URL="https://your-app-domain.ondigitalocean.app"
|
||||||
|
VITE_API_URL="https://your-api-domain.ondigitalocean.app/api"
|
||||||
|
VITE_ANALYTICS_ENABLED="true"
|
||||||
|
|
||||||
|
# CORS Configuration
|
||||||
|
CORS_ORIGIN="https://your-frontend-domain.ondigitalocean.app"
|
||||||
|
|
||||||
|
# Admin User Configuration (for initial setup only)
|
||||||
|
ADMIN_EMAIL="admin@yourcompany.com"
|
||||||
|
ADMIN_USERNAME="admin"
|
||||||
|
ADMIN_PASSWORD="change-this-secure-password"
|
||||||
|
|
||||||
|
# Optional: Rate Limiting
|
||||||
|
RATE_LIMIT_ENABLED="true"
|
||||||
|
RATE_LIMIT_WINDOW_MS="900000"
|
||||||
|
RATE_LIMIT_MAX_REQUESTS="100"
|
||||||
|
|
||||||
|
# Optional: File Upload Configuration
|
||||||
|
MAX_FILE_SIZE="10485760"
|
||||||
|
UPLOAD_PATH="/tmp/uploads"
|
||||||
|
|
||||||
|
# Optional: Email Configuration (if needed)
|
||||||
|
SMTP_HOST=""
|
||||||
|
SMTP_PORT=""
|
||||||
|
SMTP_USER=""
|
||||||
|
SMTP_PASS=""
|
@ -7,11 +7,15 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"build:dev": "tsc && vite build --mode development",
|
"build:dev": "tsc && vite build --mode development",
|
||||||
|
"build:api": "tsc src/server.ts --outDir dist --target es2020 --module commonjs --esModuleInterop --allowSyntheticDefaultImports --resolveJsonModule --skipLibCheck",
|
||||||
|
"start:api": "node dist/server.js",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
|
"db:migrate:prod": "drizzle-kit migrate --config=drizzle.config.production.ts",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"db:setup:prod": "node scripts/setup-production-db.js",
|
||||||
"create-superuser": "node scripts/create-superuser.js",
|
"create-superuser": "node scripts/create-superuser.js",
|
||||||
"create-default-superuser": "node scripts/create-default-superuser.js",
|
"create-default-superuser": "node scripts/create-default-superuser.js",
|
||||||
"setup-oliver": "node scripts/setup-oliver-admin.js"
|
"setup-oliver": "node scripts/setup-oliver-admin.js"
|
||||||
|
77
scripts/migrate-production.js
Normal file
77
scripts/migrate-production.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Production database migration script for DigitalOcean deployment
|
||||||
|
* This script runs database migrations against the production MySQL database
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { drizzle } from 'drizzle-orm/mysql2';
|
||||||
|
import mysql from 'mysql2/promise';
|
||||||
|
import { migrate } from 'drizzle-orm/mysql2/migrator';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
config();
|
||||||
|
|
||||||
|
async function runMigrations() {
|
||||||
|
console.log('🚀 Starting production database migration...');
|
||||||
|
|
||||||
|
if (!process.env.DATABASE_URL) {
|
||||||
|
console.error('❌ DATABASE_URL environment variable is required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse DATABASE_URL for DigitalOcean managed database
|
||||||
|
const dbUrl = new URL(process.env.DATABASE_URL);
|
||||||
|
|
||||||
|
// Create connection to MySQL
|
||||||
|
connection = await mysql.createConnection({
|
||||||
|
host: dbUrl.hostname,
|
||||||
|
port: parseInt(dbUrl.port) || 25060,
|
||||||
|
user: dbUrl.username,
|
||||||
|
password: dbUrl.password,
|
||||||
|
database: dbUrl.pathname.slice(1), // Remove leading slash
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false // DigitalOcean managed databases use SSL
|
||||||
|
},
|
||||||
|
connectTimeout: 60000,
|
||||||
|
acquireTimeout: 60000,
|
||||||
|
timeout: 60000,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Connected to database');
|
||||||
|
|
||||||
|
// Create drizzle instance
|
||||||
|
const db = drizzle(connection);
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
console.log('🔄 Running migrations...');
|
||||||
|
await migrate(db, { migrationsFolder: join(__dirname, '../drizzle') });
|
||||||
|
|
||||||
|
console.log('✅ Migrations completed successfully!');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Migration failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
if (connection) {
|
||||||
|
await connection.end();
|
||||||
|
console.log('🔌 Database connection closed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migrations if this script is executed directly
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
runMigrations().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { runMigrations };
|
116
scripts/setup-production-db.js
Normal file
116
scripts/setup-production-db.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Production database setup script for DigitalOcean
|
||||||
|
* This script sets up the initial database structure and creates a default admin user
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { drizzle } from 'drizzle-orm/mysql2';
|
||||||
|
import mysql from 'mysql2/promise';
|
||||||
|
import { migrate } from 'drizzle-orm/mysql2/migrator';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import * as schema from '../src/lib/db/schema.js';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
config();
|
||||||
|
|
||||||
|
async function setupProductionDatabase() {
|
||||||
|
console.log('🚀 Setting up production database...');
|
||||||
|
|
||||||
|
if (!process.env.DATABASE_URL) {
|
||||||
|
console.error('❌ DATABASE_URL environment variable is required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse DATABASE_URL for DigitalOcean managed database
|
||||||
|
const dbUrl = new URL(process.env.DATABASE_URL);
|
||||||
|
|
||||||
|
// Create connection to MySQL
|
||||||
|
connection = await mysql.createConnection({
|
||||||
|
host: dbUrl.hostname,
|
||||||
|
port: parseInt(dbUrl.port) || 25060,
|
||||||
|
user: dbUrl.username,
|
||||||
|
password: dbUrl.password,
|
||||||
|
database: dbUrl.pathname.slice(1), // Remove leading slash
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false // DigitalOcean managed databases use SSL
|
||||||
|
},
|
||||||
|
connectTimeout: 60000,
|
||||||
|
acquireTimeout: 60000,
|
||||||
|
timeout: 60000,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Connected to database');
|
||||||
|
|
||||||
|
// Create drizzle instance
|
||||||
|
const db = drizzle(connection, { schema });
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
console.log('🔄 Running migrations...');
|
||||||
|
await migrate(db, { migrationsFolder: join(__dirname, '../drizzle') });
|
||||||
|
console.log('✅ Migrations completed');
|
||||||
|
|
||||||
|
// Create default admin user
|
||||||
|
console.log('👤 Creating default admin user...');
|
||||||
|
|
||||||
|
const adminEmail = process.env.ADMIN_EMAIL || 'admin@scriptshare.com';
|
||||||
|
const adminPassword = process.env.ADMIN_PASSWORD || 'admin123';
|
||||||
|
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
|
||||||
|
|
||||||
|
// Check if admin user already exists
|
||||||
|
const existingAdmin = await db.query.users.findFirst({
|
||||||
|
where: (users, { eq }) => eq(users.email, adminEmail)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingAdmin) {
|
||||||
|
console.log('ℹ️ Admin user already exists, skipping creation');
|
||||||
|
} else {
|
||||||
|
const hashedPassword = await bcrypt.hash(adminPassword, 10);
|
||||||
|
|
||||||
|
await db.insert(schema.users).values({
|
||||||
|
id: nanoid(),
|
||||||
|
email: adminEmail,
|
||||||
|
username: adminUsername,
|
||||||
|
displayName: 'System Administrator',
|
||||||
|
isAdmin: true,
|
||||||
|
isModerator: true,
|
||||||
|
avatarUrl: null,
|
||||||
|
bio: 'Default system administrator account'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Default admin user created');
|
||||||
|
console.log(`📧 Email: ${adminEmail}`);
|
||||||
|
console.log(`👤 Username: ${adminUsername}`);
|
||||||
|
console.log(`🔑 Password: ${adminPassword}`);
|
||||||
|
console.log('⚠️ Please change the default password after first login!');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎉 Production database setup completed successfully!');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Setup failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
if (connection) {
|
||||||
|
await connection.end();
|
||||||
|
console.log('🔌 Database connection closed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run setup if this script is executed directly
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
setupProductionDatabase().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { setupProductionDatabase };
|
Reference in New Issue
Block a user