From 2d31c492353c781f695a13470e3d266bcd00e965 Mon Sep 17 00:00:00 2001 From: mikicv Date: Sun, 28 Sep 2025 16:32:31 +0100 Subject: [PATCH] Refactor production setup and database management - Updated Dockerfile for production to ensure proper database permissions and improved startup script. - Removed outdated PRODUCTION_README.md and consolidated relevant information into other documentation. - Enhanced deploy.sh script to fix database permissions and streamline deployment process. - Modified docker-compose configuration to use a dedicated production file and adjusted port mappings. - Removed legacy docker-compose.yml file to avoid confusion. - Improved session management by refining secure cookie settings based on environment variables. - Deleted obsolete Nginx configuration and old seed scripts to clean up the project structure. - Updated database setup scripts to reflect new structure and removed old seed data scripts. - Adjusted reset-db script to use environment variable for database path. - Enhanced setup-database script to provide dynamic admin credentials in the summary. - Removed unnecessary backup file for SQLite database. --- .env.example | 41 +++- Dockerfile | 38 ---- Dockerfile.production | 13 +- PRODUCTION_README.md | 149 -------------- deploy.sh | 29 ++- docker-compose.production.yml | 21 +- docker-compose.yml | 32 --- lib/session.ts | 4 +- nginx.conf | 78 ------- scripts/cleanup-old-seeds.sh | 101 --------- scripts/old-seeds/README.md | 40 ---- scripts/old-seeds/init-admin-data.ts | 90 -------- scripts/old-seeds/reset-database.ts | 263 ------------------------ scripts/old-seeds/seed-announcements.ts | 50 ----- scripts/old-seeds/seed-data.ts | 203 ------------------ scripts/old-seeds/seed-time-slots.ts | 55 ----- scripts/reset-db.ts | 3 +- scripts/setup-database.ts | 2 +- sqlite.db.backup | Bin 98304 -> 0 bytes 19 files changed, 69 insertions(+), 1143 deletions(-) delete mode 100644 Dockerfile delete mode 100644 PRODUCTION_README.md delete mode 100644 docker-compose.yml delete mode 100644 nginx.conf delete mode 100755 scripts/cleanup-old-seeds.sh delete mode 100644 scripts/old-seeds/README.md delete mode 100644 scripts/old-seeds/init-admin-data.ts delete mode 100644 scripts/old-seeds/reset-database.ts delete mode 100644 scripts/old-seeds/seed-announcements.ts delete mode 100644 scripts/old-seeds/seed-data.ts delete mode 100644 scripts/old-seeds/seed-time-slots.ts delete mode 100644 sqlite.db.backup diff --git a/.env.example b/.env.example index e61504f..ee4d92e 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,33 @@ -# Database -DATABASE_URL="./sqlite.db" +# Environment Configuration Template +# Copy this to .env.production and fill in your values -# NextAuth.js -NEXTAUTH_URL="http://localhost:3000" -NEXTAUTH_SECRET="your-secret-key-here-make-this-very-long-and-random" +# === REQUIRED VARIABLES === +# Database URL (host path - gets mounted into container) +DATABASE_URL=./data/sqlite.db -# Email Configuration (Gmail) -EMAIL_USER="your-email@gmail.com" -EMAIL_PASSWORD="your-app-password-here" +# NextAuth.js Configuration (REQUIRED) +NEXTAUTH_URL=https://your-domain.com +NEXTAUTH_SECRET=your-long-random-secret-here-generate-with-openssl-rand-base64-32 -# Admin Configuration -ADMIN_EMAIL="admin@example.com" -ADMIN_PASSWORD="admin123" +# Admin User (CHANGE THESE!) +ADMIN_EMAIL=admin@your-domain.com +ADMIN_PASSWORD=your-secure-admin-password + +# === OPTIONAL VARIABLES === +# Application Environment +NODE_ENV=production +PORT=3000 + +# Email Configuration (for notifications - optional) +EMAIL_USER=your-email@gmail.com +EMAIL_PASSWORD=your-gmail-app-password + +# Rate Limiting (defaults provided) +RATE_LIMIT_MAX=100 +RATE_LIMIT_WINDOW=900000 + +# Logging Level +LOG_LEVEL=info + +# Local Development Override (for HTTP testing) +DISABLE_SECURE_COOKIES=false diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index c05c5bd..0000000 --- a/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -# Use the official Node.js runtime as the base image -FROM node:18-alpine - -# Set the working directory in the container -WORKDIR /app - -# Copy package.json and package-lock.json (if available) -COPY package*.json ./ - -# Install dependencies -RUN npm ci --only=production - -# Copy the rest of the application code -COPY . . - -# Create the SQLite database directory -RUN mkdir -p /app/data - -# Build the Next.js application -RUN npm run build - -# Expose the port the app runs on -EXPOSE 3000 - -# Set environment variables -ENV NODE_ENV=production -ENV DATABASE_URL=/app/data/sqlite.db - -# Create a non-root user to run the application -RUN addgroup -g 1001 -S nodejs -RUN adduser -S nextjs -u 1001 - -# Change ownership of the app directory to the nextjs user -RUN chown -R nextjs:nodejs /app -USER nextjs - -# Command to run the application -CMD ["npm", "start"] diff --git a/Dockerfile.production b/Dockerfile.production index 392caef..446d4bf 100644 --- a/Dockerfile.production +++ b/Dockerfile.production @@ -54,14 +54,21 @@ COPY --chown=nextjs:nodejs </dev/null || true + if [ -f "/app/data/sqlite.db" ]; then + chmod 644 /app/data/sqlite.db 2>/dev/null || true + fi +fi + echo "๐ŸŒŸ Starting server..." exec node server.js EOF RUN chmod +x /app/start.sh - -USER nextjs - EXPOSE 3000 ENV PORT=3000 diff --git a/PRODUCTION_README.md b/PRODUCTION_README.md deleted file mode 100644 index 9b9623b..0000000 --- a/PRODUCTION_README.md +++ /dev/null @@ -1,149 +0,0 @@ -# LCC Table Tennis Booking - Production Setup - -## Quick Start - -Your production environment is configured for domain: **lcc-tt-booking.mikicvi.com** - -### 1. Deploy the Application - -```bash -# Make deployment script executable (if not already done) -chmod +x deploy.sh - -# Deploy to production -./deploy.sh -``` - -### 2. Set Up Cloudflare Tunnel - -```bash -# Run the tunnel setup script -./setup-tunnel.sh - -# Follow the instructions provided by the script -``` - -## Configuration Files Created - -### Environment Configuration - -- **`.env.production`** - Production environment variables -- **`docker-compose.production.yml`** - Production Docker Compose configuration -- **`Dockerfile.production`** - Optimized production Docker build - -### Security & Secrets - -- **NEXTAUTH_SECRET**: `qHYNaq516ByAY6H4HdxacMICd05I1DqvrTitIuVtT20=` (pre-generated) -- **Domain**: `lcc-tt-booking.mikicvi.com` -- **Admin Email**: `admin@lcc-tt-booking.mikicvi.com` - -### Scripts & Automation - -- **`deploy.sh`** - One-command production deployment -- **`setup-tunnel.sh`** - Cloudflare Tunnel setup assistant -- **`cloudflare-tunnel-config.yml`** - Tunnel configuration template - -### Health & Monitoring - -- **`/api/health`** - Health check endpoint with database connectivity and memory usage -- **Automated backups** - Daily SQLite backups (30-day retention) -- **Log rotation** - Automatic log management - -## Production Checklist - -### Before Going Live: - -- [ ] Update email credentials in `.env.production` -- [ ] Change admin password from default -- [ ] Set up Cloudflare Tunnel -- [ ] Test health check endpoint -- [ ] Verify SSL certificate is working - -### After Deployment: - -- [ ] Test all booking functionality -- [ ] Verify admin panel access -- [ ] Check automated backups are working -- [ ] Set up monitoring alerts (optional) - -## Management Commands - -```bash -# View application logs -docker-compose -f docker-compose.production.yml logs -f tt-booking - -# Restart application -docker-compose -f docker-compose.production.yml restart tt-booking - -# Stop all services -docker-compose -f docker-compose.production.yml down - -# Update and redeploy -./deploy.sh - -# Access database backup -ls -la backups/ - -# Check application health -curl http://localhost:3000/api/health -``` - -## Directory Structure - -``` -/Users/mikicv/Documents/tt-booking/ -โ”œโ”€โ”€ .env.production # Production environment variables -โ”œโ”€โ”€ docker-compose.production.yml # Production Docker Compose -โ”œโ”€โ”€ Dockerfile.production # Production Docker build -โ”œโ”€โ”€ deploy.sh # Deployment script -โ”œโ”€โ”€ setup-tunnel.sh # Cloudflare Tunnel setup -โ”œโ”€โ”€ cloudflare-tunnel-config.yml # Tunnel configuration -โ”œโ”€โ”€ data/ # SQLite database storage -โ”œโ”€โ”€ backups/ # Automated backups -โ””โ”€โ”€ logs/ # Application logs -``` - -## Default Admin Access - -- **URL**: https://lcc-tt-booking.mikicvi.com/admin -- **Email**: admin@lcc-tt-booking.mikicvi.com -- **Password**: ChangeMeInProduction123! (โš ๏ธ CHANGE THIS!) - -## Support & Troubleshooting - -### Common Issues: - -1. **Container won't start**: Check `docker-compose -f docker-compose.production.yml logs` -2. **Database issues**: Ensure `data/` directory permissions are correct -3. **Tunnel not working**: Verify Cloudflare DNS settings and tunnel configuration - -### Health Check: - -The health endpoint (`/api/health`) provides: - -- Application status -- Database connectivity -- Memory usage -- Uptime information - -### Backup Verification: - -```bash -# List all backups -ls -la backups/ - -# Check latest backup size -du -h backups/sqlite-$(date +%Y%m%d)*.db | tail -1 -``` - -## Production Features Included: - -- โœ… Automated daily backups (30-day retention) -- โœ… Health monitoring endpoint -- โœ… Log rotation and management -- โœ… Multi-stage Docker optimization -- โœ… Security hardening -- โœ… Rate limiting configured -- โœ… SSL-ready with Cloudflare integration - -Your LCC Table Tennis Booking System is ready for production! ๐Ÿ“ diff --git a/deploy.sh b/deploy.sh index 76e2493..9d76662 100755 --- a/deploy.sh +++ b/deploy.sh @@ -32,14 +32,21 @@ fi echo "๐Ÿ› ๏ธ Setting up the database..." npx tsx scripts/setup-database.ts +# Fix database permissions for container +echo "๐Ÿ”’ Fixing database permissions for container..." +if [ -f "data/sqlite.db" ]; then + chmod 666 data/sqlite.db +fi +chmod 777 data backups logs + # Build and deploy with Docker Compose echo "๐Ÿณ Building and starting Docker containers..." # Stop existing containers -docker-compose -f docker-compose.production.yml down || echo "No existing containers to stop" +docker compose -f docker-compose.production.yml down || echo "No existing containers to stop" # Build and start containers -docker-compose -f docker-compose.production.yml up -d --build +docker compose -f docker-compose.production.yml up -d --build # Wait for containers to be healthy echo "โณ Waiting for containers to be healthy..." @@ -48,13 +55,13 @@ sleep 30 # Check health echo "๐Ÿ” Checking application health..." for i in {1..10}; do - if curl -f http://localhost:3000/api/health >/dev/null 2>&1; then + if curl -f http://localhost:3036/api/health >/dev/null 2>&1; then echo "โœ… Application is healthy!" break elif [ $i -eq 10 ]; then echo "โŒ Application health check failed after 10 attempts" echo "๐Ÿ“‹ Container logs:" - docker-compose -f docker-compose.production.yml logs tt-booking + docker compose -f docker-compose.production.yml logs tt-booking exit 1 else echo "โณ Attempt $i/10: Application not ready yet, waiting..." @@ -64,24 +71,24 @@ done # Show running containers echo "๐Ÿ“Š Running containers:" -docker-compose -f docker-compose.production.yml ps +docker compose -f docker-compose.production.yml ps # Show logs echo "๐Ÿ“‹ Recent application logs:" -docker-compose -f docker-compose.production.yml logs --tail=20 tt-booking +docker compose -f docker-compose.production.yml logs --tail=20 tt-booking echo "" echo "๐ŸŽ‰ Deployment completed successfully!" echo "" echo "๐Ÿ“Š Application Status:" echo " โ€ข URL: https://lcc-tt-booking.mikicvi.com" -echo " โ€ข Health Check: http://localhost:3000/api/health" -echo " โ€ข Container Status: $(docker-compose -f docker-compose.production.yml ps -q tt-booking | xargs docker inspect -f '{{.State.Status}}')" +echo " โ€ข Health Check: http://localhost:3036/api/health" +echo " โ€ข Container Status: $(docker compose -f docker-compose.production.yml ps -q tt-booking | xargs docker inspect -f '{{.State.Status}}')" echo "" echo "๐Ÿ”ง Useful commands:" -echo " โ€ข View logs: docker-compose -f docker-compose.production.yml logs -f tt-booking" -echo " โ€ข Restart: docker-compose -f docker-compose.production.yml restart tt-booking" -echo " โ€ข Stop: docker-compose -f docker-compose.production.yml down" +echo " โ€ข View logs: docker compose -f docker-compose.production.yml logs -f tt-booking" +echo " โ€ข Restart: docker compose -f docker-compose.production.yml restart tt-booking" +echo " โ€ข Stop: docker compose -f docker-compose.production.yml down" echo "" echo "โš ๏ธ Don't forget to:" echo " 1. Set up Cloudflare Tunnel to expose your application" diff --git a/docker-compose.production.yml b/docker-compose.production.yml index 75ffdf1..b94e4cd 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -1,26 +1,17 @@ -version: '3.8' - services: tt-booking: build: context: . - dockerfile: Dockerfile + dockerfile: Dockerfile.production container_name: lcc-tt-booking + user: "1000:1000" ports: - - '3000:3000' + - '3036:3000' + env_file: + - .env.production environment: - - NODE_ENV=production - - DATABASE_URL=/app/data/sqlite.db - - NEXTAUTH_URL=https://lcc-tt-booking.mikicvi.com - - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} - - EMAIL_USER=${EMAIL_USER} - - EMAIL_PASSWORD=${EMAIL_PASSWORD} - - ADMIN_EMAIL=${ADMIN_EMAIL} - - ADMIN_PASSWORD=${ADMIN_PASSWORD} + # Container-specific override - PORT=3000 - - RATE_LIMIT_MAX=${RATE_LIMIT_MAX:-100} - - RATE_LIMIT_WINDOW=${RATE_LIMIT_WINDOW:-900000} - - LOG_LEVEL=${LOG_LEVEL:-info} volumes: - ./data:/app/data - ./backups:/app/backups diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 84904c4..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: '3.8' - -services: - tt-booking: - build: . - ports: - - '3000:3000' - environment: - - NODE_ENV=production - - DATABASE_URL=/app/data/sqlite.db - - NEXTAUTH_URL=http://localhost:3000 - - NEXTAUTH_SECRET=your-secret-key-here-make-this-very-long-and-random - - EMAIL_USER=your-email@gmail.com - - EMAIL_PASSWORD=your-app-password-here - - ADMIN_EMAIL=admin@example.com - - ADMIN_PASSWORD=admin123 - volumes: - - ./data:/app/data - restart: unless-stopped - - # Nginx reverse proxy (optional, for production deployment) - nginx: - image: nginx:alpine - ports: - - '80:80' - - '443:443' - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - - ./ssl:/etc/nginx/ssl:ro - depends_on: - - tt-booking - restart: unless-stopped diff --git a/lib/session.ts b/lib/session.ts index d8813a2..364479d 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -49,7 +49,7 @@ export async function createSession(payload: Omit) const cookieStore = await cookies(); cookieStore.set('session', session, { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: process.env.NODE_ENV === 'production' && process.env.NEXTAUTH_URL?.startsWith('https'), expires: expiresAt, sameSite: 'lax', path: '/', @@ -70,7 +70,7 @@ export async function updateSession() { cookieStore.set('session', newSession, { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: process.env.NODE_ENV === 'production' && process.env.NEXTAUTH_URL?.startsWith('https'), expires: expires, sameSite: 'lax', path: '/', diff --git a/nginx.conf b/nginx.conf deleted file mode 100644 index cc617be..0000000 --- a/nginx.conf +++ /dev/null @@ -1,78 +0,0 @@ -events { - worker_connections 1024; -} - -http { - upstream app { - server tt-booking:3000; - } - - # Rate limiting - limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; - limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; - - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always; - - server { - listen 80; - server_name your-domain.com; - - # Redirect HTTP to HTTPS - return 301 https://$server_name$request_uri; - } - - server { - listen 443 ssl http2; - server_name your-domain.com; - - # SSL configuration - ssl_certificate /etc/nginx/ssl/cert.pem; - ssl_certificate_key /etc/nginx/ssl/key.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; - ssl_prefer_server_ciphers off; - - # API rate limiting - location /api/ { - limit_req zone=api burst=20 nodelay; - proxy_pass http://app; - 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; - } - - # Login rate limiting - location /api/auth/login { - limit_req zone=login burst=3 nodelay; - proxy_pass http://app; - 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; - } - - # Static files and general requests - location / { - proxy_pass http://app; - 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; - } - } -} diff --git a/scripts/cleanup-old-seeds.sh b/scripts/cleanup-old-seeds.sh deleted file mode 100755 index 33a9c3b..0000000 --- a/scripts/cleanup-old-seeds.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/bin/bash - -# Cleanup old seed scripts and organize database utilities -# This script consolidates old individual seed scripts into the new unified system - -echo "๐Ÿงน Cleaning up old database scripts..." - -# Create a backup directory for old scripts -mkdir -p scripts/old-seeds - -# Move old individual seed scripts to backup -echo "๐Ÿ“ฆ Moving old seed scripts to scripts/old-seeds/..." - -if [ -f "scripts/seed-data.ts" ]; then - mv scripts/seed-data.ts scripts/old-seeds/ - echo " โœ“ Moved seed-data.ts" -fi - -if [ -f "scripts/seed-announcements.ts" ]; then - mv scripts/seed-announcements.ts scripts/old-seeds/ - echo " โœ“ Moved seed-announcements.ts" -fi - -if [ -f "scripts/seed-time-slots.ts" ]; then - mv scripts/seed-time-slots.ts scripts/old-seeds/ - echo " โœ“ Moved seed-time-slots.ts" -fi - -if [ -f "scripts/init-admin-data.ts" ]; then - mv scripts/init-admin-data.ts scripts/old-seeds/ - echo " โœ“ Moved init-admin-data.ts" -fi - -# Remove old reset script if it exists -if [ -f "scripts/reset-database.ts" ]; then - mv scripts/reset-database.ts scripts/old-seeds/ - echo " โœ“ Moved old reset-database.ts" -fi - -# Create a README in the old-seeds directory -cat > scripts/old-seeds/README.md << 'EOF' -# Old Seed Scripts (Archived) - -These are the original individual seed scripts that have been consolidated into the new unified database setup system. - -## Consolidated Into: - -- **setup-database.ts** - Unified setup script with all functionality -- **reset-db.ts** - Improved reset script with safety features - -## Original Scripts: - -- `seed-data.ts` - Sample bookings and activity logs -- `seed-announcements.ts` - Test announcements -- `seed-time-slots.ts` - Time slot configuration -- `init-admin-data.ts` - Admin dashboard initialization -- `reset-database.ts` - Original reset script - -## Migration Notes: - -All functionality from these scripts has been intelligently integrated into the new system: - -### New Command Equivalents: - -| Old Script | New Command | -|------------|-------------| -| `tsx scripts/seed-data.ts` | `npm run db:setup` | -| `tsx scripts/seed-announcements.ts` | Integrated into setup | -| `tsx scripts/seed-time-slots.ts` | Integrated into setup | -| `tsx scripts/init-admin-data.ts` | Integrated into setup | -| `tsx scripts/reset-database.ts` | `npm run db:reset-confirm` | - -### Advantages of New System: - -1. **Intelligent Setup** - Automatically handles dependencies and order -2. **Flexible Options** - Essential-only or full sample data modes -3. **Safety Features** - Reset confirmation and detailed logging -4. **Better Documentation** - Comprehensive help and summaries -5. **Single Source** - All database setup in one place - -These old scripts are kept for reference but should not be used in new development. -EOF - -echo " โœ“ Created README in old-seeds directory" - -echo "" -echo "โœ… Cleanup complete!" -echo "" -echo "๐Ÿ“‹ Summary of changes:" -echo " โ€ข Old seed scripts moved to scripts/old-seeds/" -echo " โ€ข New unified system active:" -echo " - setup-database.ts (comprehensive setup)" -echo " - reset-db.ts (safe reset with confirmation)" -echo "" -echo "๐Ÿš€ New database commands:" -echo " npm run db:setup # Full setup with sample data" -echo " npm run db:seed # Essential data only" -echo " npm run db:reset # Safe reset (shows warning)" -echo " npm run db:reset-confirm # Immediate reset" -echo "" -echo "๐Ÿ“– See DATABASE_SETUP.md for detailed documentation" \ No newline at end of file diff --git a/scripts/old-seeds/README.md b/scripts/old-seeds/README.md deleted file mode 100644 index 8d9c1b8..0000000 --- a/scripts/old-seeds/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Old Seed Scripts (Archived) - -These are the original individual seed scripts that have been consolidated into the new unified database setup system. - -## Consolidated Into: - -- **setup-database.ts** - Unified setup script with all functionality -- **reset-db.ts** - Improved reset script with safety features - -## Original Scripts: - -- `seed-data.ts` - Sample bookings and activity logs -- `seed-announcements.ts` - Test announcements -- `seed-time-slots.ts` - Time slot configuration -- `init-admin-data.ts` - Admin dashboard initialization -- `reset-database.ts` - Original reset script - -## Migration Notes: - -All functionality from these scripts has been intelligently integrated into the new system: - -### New Command Equivalents: - -| Old Script | New Command | -|------------|-------------| -| `tsx scripts/seed-data.ts` | `npm run db:setup` | -| `tsx scripts/seed-announcements.ts` | Integrated into setup | -| `tsx scripts/seed-time-slots.ts` | Integrated into setup | -| `tsx scripts/init-admin-data.ts` | Integrated into setup | -| `tsx scripts/reset-database.ts` | `npm run db:reset-confirm` | - -### Advantages of New System: - -1. **Intelligent Setup** - Automatically handles dependencies and order -2. **Flexible Options** - Essential-only or full sample data modes -3. **Safety Features** - Reset confirmation and detailed logging -4. **Better Documentation** - Comprehensive help and summaries -5. **Single Source** - All database setup in one place - -These old scripts are kept for reference but should not be used in new development. diff --git a/scripts/old-seeds/init-admin-data.ts b/scripts/old-seeds/init-admin-data.ts deleted file mode 100644 index 1cafc81..0000000 --- a/scripts/old-seeds/init-admin-data.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { db } from '../lib/db'; -import { settings, metrics } from '../lib/db/schema'; -import { randomUUID } from 'crypto'; -import { eq } from 'drizzle-orm'; - -async function initializeAdminData() { - try { - console.log('Initializing admin dashboard data...'); - - const now = new Date(); - - // Initialize default settings - console.log('Setting up default settings...'); - - const defaultSettings = [ - { - id: randomUUID(), - key: 'booking_window_days', - value: '14', - updatedAt: now, - }, - { - id: randomUUID(), - key: 'max_booking_duration_hours', - value: '2', - updatedAt: now, - }, - { - id: randomUUID(), - key: 'max_bookings_per_user_per_hour_per_day', - value: '1', - updatedAt: now, - }, - { - id: randomUUID(), - key: 'allow_booking_modifications', - value: 'true', - updatedAt: now, - }, - { - id: randomUUID(), - key: 'booking_modification_hours_before', - value: '2', - updatedAt: now, - }, - ]; - - // Insert settings if they don't exist - for (const setting of defaultSettings) { - const existingSetting = await db.select().from(settings).where(eq(settings.key, setting.key)).limit(1); - - if (existingSetting.length === 0) { - await db.insert(settings).values(setting); - console.log(`โœ“ Setting '${setting.key}' initialized with value '${setting.value}'`); - } else { - console.log(`- Setting '${setting.key}' already exists`); - } - } - - // Initialize current month's metrics - console.log('Initializing monthly metrics...'); - - const currentMonth = now.toISOString().substring(0, 7); // "2025-09" - - const existingMetric = await db.select().from(metrics).where(eq(metrics.period, currentMonth)).limit(1); - - if (existingMetric.length === 0) { - const monthlyMetric = { - id: randomUUID(), - metricType: 'monthly_bookings', - period: currentMonth, - value: 0, - createdAt: now, - updatedAt: now, - }; - - await db.insert(metrics).values(monthlyMetric); - console.log(`โœ“ Monthly metrics initialized for ${currentMonth}`); - } else { - console.log(`- Monthly metrics for ${currentMonth} already exist`); - } - - console.log('Admin dashboard data initialization complete!'); - } catch (error) { - console.error('Error initializing admin data:', error); - throw error; - } -} - -initializeAdminData(); diff --git a/scripts/old-seeds/reset-database.ts b/scripts/old-seeds/reset-database.ts deleted file mode 100644 index 7dc7915..0000000 --- a/scripts/old-seeds/reset-database.ts +++ /dev/null @@ -1,263 +0,0 @@ -import Database from 'better-sqlite3'; -import { drizzle } from 'drizzle-orm/better-sqlite3'; -import * as schema from '../lib/db/schema'; -import { sql } from 'drizzle-orm'; -import { randomUUID } from 'crypto'; -import bcrypt from 'bcryptjs'; - -const sqlite = new Database('./sqlite.db'); -const db = drizzle(sqlite, { schema }); - -async function resetDatabase() { - console.log('Resetting database...'); - - // Drop all tables - const tables = [ - 'activity_logs', - 'bookings', - 'announcements', - 'time_slots', - 'settings', - 'courts', - 'users', - '__drizzle_migrations', - '__old_push_courts', - '__old_push_users', - ]; - - for (const table of tables) { - try { - await db.run(sql.raw(`DROP TABLE IF EXISTS ${table}`)); - console.log(`Dropped table: ${table}`); - } catch (error) { - console.log(`Table ${table} doesn't exist or error dropping:`, error); - } - } - - // Create all tables with current schema - console.log('Creating tables...'); - - // Users table - await db.run(sql` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - email TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - surname TEXT NOT NULL, - password TEXT NOT NULL, - role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')), - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - - // Courts table - await db.run(sql` - CREATE TABLE courts ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - is_active INTEGER NOT NULL DEFAULT 1, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - - // Settings table - await db.run(sql` - CREATE TABLE settings ( - id TEXT PRIMARY KEY, - key TEXT NOT NULL UNIQUE, - value TEXT NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - - // Time slots table - await db.run(sql` - CREATE TABLE time_slots ( - id TEXT PRIMARY KEY, - day_of_week INTEGER NOT NULL, - start_time TEXT NOT NULL, - end_time TEXT NOT NULL, - is_active INTEGER NOT NULL DEFAULT 1, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - - // Bookings table - await db.run(sql` - CREATE TABLE bookings ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - court_id TEXT NOT NULL REFERENCES courts(id) ON DELETE CASCADE, - date TEXT NOT NULL, - start_time TEXT NOT NULL, - end_time TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'cancelled')), - notes TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - - // Announcements table with all required columns - await db.run(sql` - CREATE TABLE announcements ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - content TEXT NOT NULL, - is_active INTEGER NOT NULL DEFAULT 1, - priority TEXT NOT NULL DEFAULT 'medium' CHECK (priority IN ('low', 'medium', 'high')), - expires_at INTEGER, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - - // Activity logs table - await db.run(sql` - CREATE TABLE activity_logs ( - id TEXT PRIMARY KEY, - user_id TEXT REFERENCES users(id) ON DELETE SET NULL, - action TEXT NOT NULL, - entity_type TEXT NOT NULL, - entity_id TEXT, - details TEXT, - ip_address TEXT, - user_agent TEXT, - created_at INTEGER NOT NULL - ) - `); - - console.log('All tables created successfully!'); - - // Insert seed data - console.log('Inserting seed data...'); - - const now = Date.now(); - - // Create admin user - const hashedPassword = await bcrypt.hash('admin123', 12); - await db.insert(schema.users).values({ - id: randomUUID(), - email: 'admin@ttbooking.com', - name: 'Admin', - surname: 'User', - password: hashedPassword, - role: 'admin', - createdAt: new Date(now), - updatedAt: new Date(now), - }); - - // Create test user - const testPassword = await bcrypt.hash('password123', 12); - await db.insert(schema.users).values({ - id: randomUUID(), - email: 'user@test.com', - name: 'Test', - surname: 'User', - password: testPassword, - role: 'user', - createdAt: new Date(now), - updatedAt: new Date(now), - }); - - // Create courts - const court1Id = randomUUID(); - const court2Id = randomUUID(); - - await db.insert(schema.courts).values([ - { - id: court1Id, - name: 'Court 1', - isActive: true, - createdAt: new Date(now), - updatedAt: new Date(now), - }, - { - id: court2Id, - name: 'Court 2', - isActive: true, - createdAt: new Date(now), - updatedAt: new Date(now), - }, - ]); - - // Insert default settings - await db.insert(schema.settings).values([ - { - id: randomUUID(), - key: 'booking_window_days', - value: '7', - updatedAt: new Date(now), - }, - { - id: randomUUID(), - key: 'max_booking_duration_hours', - value: '2', - updatedAt: new Date(now), - }, - { - id: randomUUID(), - key: 'min_booking_duration_minutes', - value: '30', - updatedAt: new Date(now), - }, - { - id: randomUUID(), - key: 'booking_start_time', - value: '08:00', - updatedAt: new Date(now), - }, - { - id: randomUUID(), - key: 'booking_end_time', - value: '22:00', - updatedAt: new Date(now), - }, - { - id: randomUUID(), - key: 'allow_weekend_bookings', - value: 'true', - updatedAt: new Date(now), - }, - ]); - - // Create time slots for all days (8 AM to 10 PM) - const timeSlotData = []; - for (let day = 0; day < 7; day++) { - for (let hour = 8; hour < 22; hour += 2) { - timeSlotData.push({ - id: randomUUID(), - dayOfWeek: day, - startTime: `${hour.toString().padStart(2, '0')}:00`, - endTime: `${(hour + 2).toString().padStart(2, '0')}:00`, - isActive: true, - createdAt: new Date(now), - updatedAt: new Date(now), - }); - } - } - - await db.insert(schema.timeSlots).values(timeSlotData); - - // Create sample announcement - await db.insert(schema.announcements).values({ - id: randomUUID(), - title: 'Welcome to Table Tennis Booking System', - content: 'Book your court times easily and manage your games efficiently.', - isActive: true, - priority: 'high', - expiresAt: null, - createdAt: new Date(now), - updatedAt: new Date(now), - }); - - console.log('Seed data inserted successfully!'); - console.log('Database reset complete!'); - - sqlite.close(); -} - -resetDatabase().catch(console.error); diff --git a/scripts/old-seeds/seed-announcements.ts b/scripts/old-seeds/seed-announcements.ts deleted file mode 100644 index b9b6453..0000000 --- a/scripts/old-seeds/seed-announcements.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { db } from '@/lib/db'; -import { announcements } from '@/lib/db/schema'; -import { randomUUID } from 'crypto'; - -async function seedAnnouncements() { - try { - const testAnnouncements = [ - { - id: randomUUID(), - title: 'Welcome to the New Booking System!', - content: - 'We have upgraded our table tennis booking system with new features including mobile support, partner booking, and booking management. Enjoy your games!', - priority: 'high' as const, - isActive: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: randomUUID(), - title: 'Court Maintenance Schedule', - content: - 'Court 2 will be under maintenance this Friday from 2 PM to 4 PM. Please plan your bookings accordingly.', - priority: 'medium' as const, - isActive: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: randomUUID(), - title: 'New Partnership Feature', - content: - 'You can now specify your playing partner when making a booking. This helps other players know who will be using the court.', - priority: 'low' as const, - isActive: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - ]; - - for (const announcement of testAnnouncements) { - await db.insert(announcements).values(announcement); - } - - console.log('Test announcements created successfully!'); - } catch (error) { - console.error('Error creating test announcements:', error); - } -} - -seedAnnouncements(); diff --git a/scripts/old-seeds/seed-data.ts b/scripts/old-seeds/seed-data.ts deleted file mode 100644 index 7bf2f8a..0000000 --- a/scripts/old-seeds/seed-data.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { db } from '../lib/db'; -import { users, courts as courtsTable, bookings, announcements, activityLogs } from '../lib/db/schema'; -import { randomUUID } from 'crypto'; -import bcrypt from 'bcryptjs'; - -async function seedData() { - try { - console.log('Starting data seeding...'); - - // Get existing users to add sample bookings and activities - const existingUsers = await db.select().from(users); - - if (existingUsers.length < 2) { - console.log('Not enough users found. Please run the reset-database script first.'); - return; - } - - const adminUser = existingUsers.find((u) => u.role === 'admin'); - const regularUser = existingUsers.find((u) => u.role === 'user'); - const courts = await db.select().from(courtsTable); - - if (!adminUser || !regularUser || courts.length === 0) { - console.log('Missing admin user, regular user, or courts. Please run reset-database first.'); - return; - } - - const now = new Date(); - const today = now.toISOString().split('T')[0]; - const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString().split('T')[0]; - - // Add some sample bookings - console.log('Creating sample bookings...'); - - const sampleBookings = [ - { - id: randomUUID(), - userId: regularUser.id, - courtId: courts[0].id, - date: today, - startTime: '19:00', - endTime: '20:00', - status: 'active' as const, - notes: 'Regular evening practice session', - createdAt: new Date(now.getTime() - 2 * 60 * 60 * 1000), // 2 hours ago - updatedAt: new Date(now.getTime() - 2 * 60 * 60 * 1000), - }, - { - id: randomUUID(), - userId: regularUser.id, - courtId: courts[1] ? courts[1].id : courts[0].id, - date: tomorrow, - startTime: '20:00', - endTime: '21:00', - status: 'active' as const, - notes: 'Tournament preparation', - createdAt: new Date(now.getTime() - 1 * 60 * 60 * 1000), // 1 hour ago - updatedAt: new Date(now.getTime() - 1 * 60 * 60 * 1000), - }, - ]; - - await db.insert(bookings).values(sampleBookings); - - // Add sample activity logs - console.log('Creating sample activity logs...'); - - const sampleLogs = [ - { - id: randomUUID(), - userId: adminUser.id, - action: 'login', - entityType: 'user', - entityId: adminUser.id, - details: JSON.stringify({ - email: adminUser.email, - role: adminUser.role, - loginMethod: 'password', - }), - ipAddress: '192.168.1.100', - userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', - createdAt: new Date(now.getTime() - 3 * 60 * 60 * 1000), // 3 hours ago - }, - { - id: randomUUID(), - userId: regularUser.id, - action: 'create_booking', - entityType: 'booking', - entityId: sampleBookings[0].id, - details: JSON.stringify({ - courtId: courts[0].id, - courtName: courts[0].name, - date: today, - startTime: '19:00', - endTime: '20:00', - }), - ipAddress: '192.168.1.101', - userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15', - createdAt: new Date(now.getTime() - 2 * 60 * 60 * 1000), // 2 hours ago - }, - { - id: randomUUID(), - userId: regularUser.id, - action: 'login', - entityType: 'user', - entityId: regularUser.id, - details: JSON.stringify({ - email: regularUser.email, - role: regularUser.role, - loginMethod: 'password', - }), - ipAddress: '192.168.1.101', - userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15', - createdAt: new Date(now.getTime() - 2.5 * 60 * 60 * 1000), // 2.5 hours ago - }, - { - id: randomUUID(), - userId: adminUser.id, - action: 'create_announcement', - entityType: 'announcement', - entityId: null, - details: JSON.stringify({ - title: 'System Maintenance', - priority: 'high', - action: 'created_via_seed', - }), - ipAddress: '192.168.1.100', - userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', - createdAt: new Date(now.getTime() - 1 * 60 * 60 * 1000), // 1 hour ago - }, - { - id: randomUUID(), - userId: regularUser.id, - action: 'create_booking', - entityType: 'booking', - entityId: sampleBookings[1].id, - details: JSON.stringify({ - courtId: courts[1] ? courts[1].id : courts[0].id, - courtName: courts[1] ? courts[1].name : courts[0].name, - date: tomorrow, - startTime: '20:00', - endTime: '21:00', - }), - ipAddress: '192.168.1.101', - userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15', - createdAt: new Date(now.getTime() - 1 * 60 * 60 * 1000), // 1 hour ago - }, - { - id: randomUUID(), - userId: adminUser.id, - action: 'update_settings', - entityType: 'settings', - entityId: null, - details: JSON.stringify({ - changedSettings: ['booking_window_days', 'max_booking_duration_hours'], - previousValues: { booking_window_days: '7', max_booking_duration_hours: '2' }, - newValues: { booking_window_days: '14', max_booking_duration_hours: '3' }, - }), - ipAddress: '192.168.1.100', - userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', - createdAt: new Date(now.getTime() - 30 * 60 * 1000), // 30 minutes ago - }, - ]; - - await db.insert(activityLogs).values(sampleLogs); - - // Add more announcements for testing - console.log('Creating additional announcements...'); - - const additionalAnnouncements = [ - { - id: randomUUID(), - title: 'New Court Rules', - content: 'Please remember to clean up after your sessions and respect the time limits.', - isActive: true, - priority: 'medium' as const, - expiresAt: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000), // 1 week from now - createdAt: new Date(now.getTime() - 4 * 60 * 60 * 1000), // 4 hours ago - updatedAt: new Date(now.getTime() - 4 * 60 * 60 * 1000), - }, - { - id: randomUUID(), - title: 'Tournament Sign-ups Open', - content: 'The annual table tennis tournament sign-ups are now open! Register by the end of this month.', - isActive: true, - priority: 'high' as const, - expiresAt: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000), // 30 days from now - createdAt: new Date(now.getTime() - 24 * 60 * 60 * 1000), // 1 day ago - updatedAt: new Date(now.getTime() - 24 * 60 * 60 * 1000), - }, - ]; - - await db.insert(announcements).values(additionalAnnouncements); - - console.log('Sample data seeding completed successfully!'); - console.log(`Created: - - ${sampleBookings.length} sample bookings - - ${sampleLogs.length} activity logs - - ${additionalAnnouncements.length} additional announcements`); - } catch (error) { - console.error('Error seeding data:', error); - } -} - -seedData(); diff --git a/scripts/old-seeds/seed-time-slots.ts b/scripts/old-seeds/seed-time-slots.ts deleted file mode 100644 index 2fd5c76..0000000 --- a/scripts/old-seeds/seed-time-slots.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { db } from '../lib/db'; -import { timeSlots } from '../lib/db/schema'; - -async function seedTimeSlots() { - console.log('๐ŸŒฑ Seeding time slots...'); - - // Example time slots for different days - const timeSlotData = [ - // Sunday: 12:00 - 17:00 - { dayOfWeek: 0, startTime: '12:00', endTime: '17:00' }, - - // Monday: 19:00 - 23:00 - { dayOfWeek: 1, startTime: '19:00', endTime: '23:00' }, - - // Tuesday: 19:00 - 23:00 - { dayOfWeek: 2, startTime: '19:00', endTime: '23:00' }, - - // Wednesday: NO SLOTS (facility closed) - // { dayOfWeek: 3, startTime: '18:00', endTime: '22:00' }, - - // Thursday: NO SLOTS (facility closed) - // { dayOfWeek: 4, startTime: '19:00', endTime: '23:00' }, - - // Friday: 18:00 - 22:00 - { dayOfWeek: 5, startTime: '18:00', endTime: '22:00' }, - - // Saturday: 10:00 - 18:00 - { dayOfWeek: 6, startTime: '10:00', endTime: '18:00' }, - ]; - - for (const slot of timeSlotData) { - await db.insert(timeSlots).values({ - id: crypto.randomUUID(), - dayOfWeek: slot.dayOfWeek, - startTime: slot.startTime, - endTime: slot.endTime, - isActive: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - } - - console.log('โœ… Time slots seeding completed'); -} - -// Run the seeding function -seedTimeSlots() - .then(() => { - console.log('Time slots seeding process completed'); - process.exit(0); - }) - .catch((error) => { - console.error('Error during time slots seeding:', error); - process.exit(1); - }); diff --git a/scripts/reset-db.ts b/scripts/reset-db.ts index 5c8c98a..dc14ebf 100644 --- a/scripts/reset-db.ts +++ b/scripts/reset-db.ts @@ -3,7 +3,8 @@ import { drizzle } from 'drizzle-orm/better-sqlite3'; import * as schema from '../lib/db/schema'; import { sql } from 'drizzle-orm'; -const sqlite = new Database('./sqlite.db'); +const dbPath = process.env.DATABASE_URL || './data/sqlite.db'; +const sqlite = new Database(dbPath); const db = drizzle(sqlite, { schema }); interface ResetOptions { diff --git a/scripts/setup-database.ts b/scripts/setup-database.ts index b874674..39ee934 100644 --- a/scripts/setup-database.ts +++ b/scripts/setup-database.ts @@ -598,7 +598,7 @@ async function printDatabaseSummary() { console.log(`\nโš™๏ธ Settings: ${settings.length} configured`); console.log('\n๐Ÿ’ก Login Credentials:'); - console.log(' Admin: admin@tabletennis.com / admin123'); + console.log(` Admin: ${process.env.ADMIN_EMAIL || 'admin@tabletennis.com'} / ${process.env.ADMIN_PASSWORD || 'admin123'}`); console.log(' User: user@tabletennis.com / user123'); console.log('\n๐Ÿš€ Ready to start! Run: npm run dev'); diff --git a/sqlite.db.backup b/sqlite.db.backup deleted file mode 100644 index 4657d0c8b7416b2d768a383b8364092cba6f576b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98304 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