diff --git a/.env.example b/.env.example deleted file mode 100644 index ee4d92e..0000000 --- a/.env.example +++ /dev/null @@ -1,33 +0,0 @@ -# Environment Configuration Template -# Copy this to .env.production and fill in your values - -# === REQUIRED VARIABLES === -# Database URL (host path - gets mounted into container) -DATABASE_URL=./data/sqlite.db - -# NextAuth.js Configuration (REQUIRED) -NEXTAUTH_URL=https://your-domain.com -NEXTAUTH_SECRET=your-long-random-secret-here-generate-with-openssl-rand-base64-32 - -# 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/.env.sample b/.env.sample new file mode 100644 index 0000000..78164bf --- /dev/null +++ b/.env.sample @@ -0,0 +1,22 @@ +# Required Environment Variables for TT Booking + +# Generate with: openssl rand -base64 32 +NEXTAUTH_SECRET=your-secret-key-here + +# Your application URL +NEXTAUTH_URL=http://localhost:3000 + +# Admin user credentials (change these!) +ADMIN_EMAIL=admin@your-domain.com +ADMIN_PASSWORD=your-secure-password + +# Optional: Email configuration for notifications +EMAIL_USER=your-email@gmail.com +EMAIL_PASSWORD=your-gmail-app-password + +# Optional: Rate limiting (defaults shown) +RATE_LIMIT_MAX=100 +RATE_LIMIT_WINDOW=900000 + +# Optional: Logging +LOG_LEVEL=info \ No newline at end of file diff --git a/Dockerfile.production b/Dockerfile.production index 446d4bf..5e63216 100644 --- a/Dockerfile.production +++ b/Dockerfile.production @@ -34,41 +34,29 @@ WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 -# Create system user and group -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - # Copy necessary files from builder stage COPY --from=builder /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=root:root /app/.next/static ./.next/static + +# Copy full node_modules for database operations +COPY --from=builder /app/node_modules ./node_modules + +# Copy database setup scripts and package.json +COPY --from=builder /app/scripts ./scripts +COPY --from=builder /app/lib ./lib +COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts +COPY --from=builder /app/package.json ./package.json + +# Copy entrypoint script +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh # Create public directory if it doesn't exist RUN mkdir -p public # Create directories for data and backups -RUN mkdir -p /app/data /app/backups /app/logs && \ - chown -R nextjs:nodejs /app/data /app/backups /app/logs +RUN mkdir -p /app/data /app/backups /app/logs -# Create startup script -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 EXPOSE 3000 ENV PORT=3000 @@ -78,4 +66,5 @@ ENV HOSTNAME="0.0.0.0" HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD curl -f http://localhost:3000/api/health || exit 1 -CMD ["/app/start.sh"] \ No newline at end of file +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["node", "server.js"] \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index 9d76662..44e0b05 100755 --- a/deploy.sh +++ b/deploy.sh @@ -28,18 +28,8 @@ if [ -d ".git" ]; then git pull origin main || echo "⚠️ Git pull failed or not needed" fi -# Setup database -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..." echo "🐳 Building and starting Docker containers..." # Stop existing containers diff --git a/docker-compose.production.yml b/docker-compose.production.yml index 08cf4b0..afe8d5a 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -4,17 +4,10 @@ services: context: . dockerfile: Dockerfile.production container_name: lcc-tt-booking - user: "1000:1000" ports: - '3036:3000' env_file: - .env.production - - '3036:3000' - env_file: - - .env.production - environment: - # Container-specific override - - PORT=3000 volumes: - ./data:/app/data - ./backups:/app/backups diff --git a/docker-compose.sample.yml b/docker-compose.sample.yml new file mode 100644 index 0000000..ad0ba93 --- /dev/null +++ b/docker-compose.sample.yml @@ -0,0 +1,54 @@ +# Sample Docker Compose for TT Booking +# Copy this file and create a .env file with your settings + +services: + tt-booking: + image: your-registry/tt-booking:latest + container_name: tt-booking + ports: + - "3000:3000" + environment: + - NEXTAUTH_URL=http://localhost:3000 + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} + - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@tabletennis.com} + - ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123} + # Optional email settings + - EMAIL_USER=${EMAIL_USER} + - EMAIL_PASSWORD=${EMAIL_PASSWORD} + # Optional rate limiting + - RATE_LIMIT_MAX=${RATE_LIMIT_MAX:-100} + - RATE_LIMIT_WINDOW=${RATE_LIMIT_WINDOW:-900000} + volumes: + - ./data:/app/data + - ./backups:/app/backups + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + + # Optional: Automated backup service + backup: + image: alpine:latest + container_name: tt-booking-backup + volumes: + - ./data:/data:ro + - ./backups:/backups + environment: + - TZ=UTC + command: > + sh -c " + apk add --no-cache tzdata && + while true; do + timestamp=$$(date +%Y%m%d-%H%M%S) + echo \"Creating backup at $$timestamp\" + cp /data/sqlite.db \"/backups/sqlite-$$timestamp.db\" 2>/dev/null || echo 'No database file found yet' + # Keep backups for 7 days + find /backups -name 'sqlite-*.db' -mtime +7 -delete 2>/dev/null || true + echo \"Backup completed, sleeping for 24 hours\" + sleep 86400 + done" + restart: unless-stopped + depends_on: + - tt-booking \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..fb08013 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,76 @@ +#!/bin/bash +set -e + +echo "🚀 Starting TT Booking Production..." + +# Create required directories if they don't exist +echo "📁 Ensuring directories exist..." +mkdir -p /app/data /app/backups /app/logs +chmod 755 /app/data /app/backups /app/logs + +# Set defaults for environment variables +export DATABASE_URL="${DATABASE_URL:-/app/data/sqlite.db}" +export NODE_ENV="${NODE_ENV:-production}" +export ADMIN_EMAIL="${ADMIN_EMAIL:-admin@tabletennis.com}" +export ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin123}" + +# Validate required environment variables +if [ -z "$NEXTAUTH_SECRET" ]; then + echo "❌ NEXTAUTH_SECRET is required but not set" + echo "💡 Generate one with: openssl rand -base64 32" + exit 1 +fi + +if [ -z "$NEXTAUTH_URL" ]; then + echo "❌ NEXTAUTH_URL is required but not set" + echo "💡 Set to your application URL, e.g., https://your-domain.com" + exit 1 +fi + +# Check database state and determine what actions are needed +echo "� Analyzing database state..." +DB_CHECK_EXIT=0 +npx tsx scripts/check-database.ts || DB_CHECK_EXIT=$? + +case $DB_CHECK_EXIT in + 0) + echo "✅ Database is ready - no action needed" + ;; + 1) + echo "� Database needs migration..." + npm run db:push + echo "✅ Migration completed" + ;; + 2) + echo "🌱 Database needs seeding..." + echo " Admin Email: $ADMIN_EMAIL" + echo " Admin Password: [HIDDEN]" + npm run db:seed + echo "✅ Seeding completed" + echo "💡 You can now login with: $ADMIN_EMAIL / $ADMIN_PASSWORD" + ;; + 3) + echo "🔄 Database needs migration and seeding..." + npm run db:push + echo "✅ Migration completed" + echo "🌱 Seeding database..." + echo " Admin Email: $ADMIN_EMAIL" + echo " Admin Password: [HIDDEN]" + npm run db:seed + echo "✅ Database initialization completed" + echo "💡 You can now login with: $ADMIN_EMAIL / $ADMIN_PASSWORD" + ;; + 4) + echo "❌ Database state check failed - see logs above" + exit 1 + ;; + *) + echo "❌ Unexpected database check result: $DB_CHECK_EXIT" + exit 1 + ;; +esac + +echo "🌟 Starting server..." + +# Execute the main command +exec "$@" \ No newline at end of file diff --git a/package.json b/package.json index 7ffa06a..1a83b23 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "db:reset": "tsx scripts/reset-db.ts", "db:reset-confirm": "tsx scripts/reset-db.ts --confirm", "db:seed": "tsx scripts/setup-database.ts --essential-only", + "db:check": "tsx scripts/check-database.ts", "postinstall": "npm run db:init" }, "dependencies": { @@ -41,6 +42,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "date-fns": "^2.30.0", + "drizzle-kit": "^0.20.6", "drizzle-orm": "^0.29.1", "drizzle-zod": "^0.5.1", "jose": "^6.1.0", @@ -55,6 +57,7 @@ "react-hook-form": "^7.62.0", "tailwind-merge": "^2.1.0", "tailwindcss-animate": "^1.0.7", + "tsx": "^4.20.5", "zod": "^3.25.76" }, "devDependencies": { @@ -66,12 +69,10 @@ "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", - "drizzle-kit": "^0.20.6", "eslint": "^8", "eslint-config-next": "^15.5.3", "postcss": "^8", "tailwindcss": "^3.3.0", - "tsx": "^4.20.5", "typescript": "^5" } } \ No newline at end of file diff --git a/scripts/check-database.ts b/scripts/check-database.ts new file mode 100644 index 0000000..a667401 --- /dev/null +++ b/scripts/check-database.ts @@ -0,0 +1,130 @@ +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import * as schema from '../lib/db/schema'; +import { eq } from 'drizzle-orm'; +import { existsSync } from 'fs'; + +interface InitResult { + needsMigration: boolean; + needsSeeding: boolean; + hasData: boolean; + adminExists: boolean; + summary: string; +} + +async function checkDatabaseState(): Promise { + const dbPath = process.env.DATABASE_URL || './data/sqlite.db'; + const result: InitResult = { + needsMigration: false, + needsSeeding: false, + hasData: false, + adminExists: false, + summary: '' + }; + + // Check if database file exists + if (!existsSync(dbPath)) { + result.needsMigration = true; + result.needsSeeding = true; + result.summary = 'Database file does not exist - full initialization needed'; + return result; + } + + let sqlite: Database.Database | null = null; + + try { + sqlite = new Database(dbPath); + const db = drizzle(sqlite, { schema }); + + // Check if core tables exist by trying to query them + try { + const userCount = db.select().from(schema.users).limit(1).all(); + const courtCount = db.select().from(schema.courts).limit(1).all(); + const settingsCount = db.select().from(schema.settings).limit(1).all(); + + // Database has tables and some basic structure + result.hasData = userCount.length > 0 || courtCount.length > 0 || settingsCount.length > 0; + + // Check for admin users specifically + const adminUsers = db + .select() + .from(schema.users) + .where(eq(schema.users.role, 'admin')) + .limit(1) + .all(); + + result.adminExists = adminUsers.length > 0; + + // Determine what's needed + if (!result.hasData) { + result.needsSeeding = true; + result.summary = 'Database exists with empty tables - seeding needed'; + } else if (!result.adminExists) { + result.needsSeeding = true; + result.summary = 'Database has data but no admin users found - admin creation needed'; + } else { + result.summary = `Database ready - found ${userCount.length ? 'users' : 'no users'}, ${courtCount.length ? 'courts' : 'no courts'}, admin users present`; + } + } catch (tableError) { + // Tables don't exist or schema is outdated + result.needsMigration = true; + result.needsSeeding = true; + result.summary = 'Database exists but tables missing/outdated - migration and seeding needed'; + } + } catch (dbError) { + // Database file exists but is corrupted or inaccessible + result.needsMigration = true; + result.needsSeeding = true; + result.summary = `Database file exists but inaccessible: ${(dbError as Error).message}`; + } finally { + if (sqlite) { + try { + sqlite.close(); + } catch (e) { + // Ignore close errors + } + } + } + + return result; +} + +async function main() { + try { + console.log('🔍 Checking database state...'); + const state = await checkDatabaseState(); + + console.log(`📊 ${state.summary}`); + console.log(` Migration needed: ${state.needsMigration ? '✅' : '❌'}`); + console.log(` Seeding needed: ${state.needsSeeding ? '✅' : '❌'}`); + console.log(` Has existing data: ${state.hasData ? '✅' : '❌'}`); + console.log(` Admin user exists: ${state.adminExists ? '✅' : '❌'}`); + + // Output structured result for shell consumption + process.env.DB_NEEDS_MIGRATION = state.needsMigration.toString(); + process.env.DB_NEEDS_SEEDING = state.needsSeeding.toString(); + process.env.DB_HAS_DATA = state.hasData.toString(); + process.env.DB_ADMIN_EXISTS = state.adminExists.toString(); + + // Exit codes for shell scripting + // 0 = ready, 1 = needs migration, 2 = needs seeding, 3 = needs both + if (state.needsMigration && state.needsSeeding) { + process.exit(3); + } else if (state.needsMigration) { + process.exit(1); + } else if (state.needsSeeding) { + process.exit(2); + } else { + process.exit(0); + } + } catch (error) { + console.error('❌ Database state check failed:', error); + process.exit(4); // Error state + } +} + +if (require.main === module) { + main(); +} + +export { checkDatabaseState, type InitResult }; \ No newline at end of file