fully working when deployed
This commit is contained in:
@@ -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
@@ -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
@@ -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 <<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
|
||||
|
||||
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"]
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["node", "server.js"]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user