diff --git a/.do/app.yaml b/.do/app.yaml new file mode 100644 index 0000000..1e9e103 --- /dev/null +++ b/.do/app.yaml @@ -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} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..cfa5b87 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..539c2bd --- /dev/null +++ b/DEPLOYMENT.md @@ -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 --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 --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. diff --git a/Dockerfile.api b/Dockerfile.api new file mode 100644 index 0000000..3cdb497 --- /dev/null +++ b/Dockerfile.api @@ -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"] diff --git a/drizzle.config.production.ts b/drizzle.config.production.ts new file mode 100644 index 0000000..87ace56 --- /dev/null +++ b/drizzle.config.production.ts @@ -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' + } +}); diff --git a/env.production.example b/env.production.example new file mode 100644 index 0000000..697628b --- /dev/null +++ b/env.production.example @@ -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="" diff --git a/package.json b/package.json index b51c4dd..31109b5 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,15 @@ "dev": "vite", "build": "tsc && vite build", "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", "preview": "vite preview", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", + "db:migrate:prod": "drizzle-kit migrate --config=drizzle.config.production.ts", "db:studio": "drizzle-kit studio", + "db:setup:prod": "node scripts/setup-production-db.js", "create-superuser": "node scripts/create-superuser.js", "create-default-superuser": "node scripts/create-default-superuser.js", "setup-oliver": "node scripts/setup-oliver-admin.js" diff --git a/scripts/migrate-production.js b/scripts/migrate-production.js new file mode 100644 index 0000000..69bb5ed --- /dev/null +++ b/scripts/migrate-production.js @@ -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 }; diff --git a/scripts/setup-production-db.js b/scripts/setup-production-db.js new file mode 100644 index 0000000..8b07153 --- /dev/null +++ b/scripts/setup-production-db.js @@ -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 };