fully working when deployed

This commit is contained in:
2025-09-28 18:47:31 +01:00
parent 38ee5a8886
commit d4aa460f91
9 changed files with 303 additions and 81 deletions
-33
View File
@@ -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
+22
View File
@@ -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
+17 -28
View File
@@ -34,41 +34,29 @@ WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1 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 necessary files from builder stage
COPY --from=builder /app/.next/standalone ./ 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 # Create public directory if it doesn't exist
RUN mkdir -p public RUN mkdir -p public
# Create directories for data and backups # Create directories for data and backups
RUN mkdir -p /app/data /app/backups /app/logs && \ RUN mkdir -p /app/data /app/backups /app/logs
chown -R nextjs:nodejs /app/data /app/backups /app/logs
# Create startup script
COPY --chown=nextjs:nodejs <<EOF /app/start.sh
#!/bin/sh
set -e
echo "🚀 Starting TT Booking Production..."
# Ensure database directory has proper permissions
if [ -d "/app/data" ]; then
echo "📁 Setting database permissions..."
chmod -R 755 /app/data /app/backups /app/logs 2>/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 EXPOSE 3000
ENV PORT=3000 ENV PORT=3000
@@ -78,4 +66,5 @@ ENV HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:3000/api/health || exit 1 CMD curl -f http://localhost:3000/api/health || exit 1
CMD ["/app/start.sh"] ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "server.js"]
+1 -11
View File
@@ -28,18 +28,8 @@ if [ -d ".git" ]; then
git pull origin main || echo "⚠️ Git pull failed or not needed" git pull origin main || echo "⚠️ Git pull failed or not needed"
fi 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 # Build and deploy with Docker Compose
echo " Building and starting Docker containers..."
echo "🐳 Building and starting Docker containers..." echo "🐳 Building and starting Docker containers..."
# Stop existing containers # Stop existing containers
-7
View File
@@ -4,17 +4,10 @@ services:
context: . context: .
dockerfile: Dockerfile.production dockerfile: Dockerfile.production
container_name: lcc-tt-booking container_name: lcc-tt-booking
user: "1000:1000"
ports: ports:
- '3036:3000' - '3036:3000'
env_file: env_file:
- .env.production - .env.production
- '3036:3000'
env_file:
- .env.production
environment:
# Container-specific override
- PORT=3000
volumes: volumes:
- ./data:/app/data - ./data:/app/data
- ./backups:/app/backups - ./backups:/app/backups
+54
View File
@@ -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
+76
View File
@@ -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 "$@"
+3 -2
View File
@@ -15,6 +15,7 @@
"db:reset": "tsx scripts/reset-db.ts", "db:reset": "tsx scripts/reset-db.ts",
"db:reset-confirm": "tsx scripts/reset-db.ts --confirm", "db:reset-confirm": "tsx scripts/reset-db.ts --confirm",
"db:seed": "tsx scripts/setup-database.ts --essential-only", "db:seed": "tsx scripts/setup-database.ts --essential-only",
"db:check": "tsx scripts/check-database.ts",
"postinstall": "npm run db:init" "postinstall": "npm run db:init"
}, },
"dependencies": { "dependencies": {
@@ -41,6 +42,7 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"drizzle-kit": "^0.20.6",
"drizzle-orm": "^0.29.1", "drizzle-orm": "^0.29.1",
"drizzle-zod": "^0.5.1", "drizzle-zod": "^0.5.1",
"jose": "^6.1.0", "jose": "^6.1.0",
@@ -55,6 +57,7 @@
"react-hook-form": "^7.62.0", "react-hook-form": "^7.62.0",
"tailwind-merge": "^2.1.0", "tailwind-merge": "^2.1.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tsx": "^4.20.5",
"zod": "^3.25.76" "zod": "^3.25.76"
}, },
"devDependencies": { "devDependencies": {
@@ -66,12 +69,10 @@
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"autoprefixer": "^10.0.1", "autoprefixer": "^10.0.1",
"drizzle-kit": "^0.20.6",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "^15.5.3", "eslint-config-next": "^15.5.3",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"tsx": "^4.20.5",
"typescript": "^5" "typescript": "^5"
} }
} }
+130
View File
@@ -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<InitResult> {
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 };