Compare commits

...

2 Commits

Author SHA1 Message Date
mikicv 38ee5a8886 Merge branch 'main' of https://gitea.mikicvi.com/mikicv/tt-booking 2025-09-28 16:34:20 +01:00
mikicv 2d31c49235 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.
2025-09-28 16:32:31 +01:00
19 changed files with 69 additions and 1134 deletions
+30 -11
View File
@@ -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
-38
View File
@@ -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"]
+10 -3
View File
@@ -54,14 +54,21 @@ 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
USER nextjs
EXPOSE 3000
ENV PORT=3000
-149
View File
@@ -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! 🏓
+18 -11
View File
@@ -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"
+6 -6
View File
@@ -1,19 +1,19 @@
version: '3.8'
services:
tt-booking:
build:
context: .
dockerfile: Dockerfile
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:
- NODE_ENV=production
- DATABASE_URL=/app/data/sqlite.db
- NEXTAUTH_URL=https://lcc-tt-booking.mikicvi.com
# Container-specific override
- PORT=3000
volumes:
- ./data:/app/data
-32
View File
@@ -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
+2 -2
View File
@@ -49,7 +49,7 @@ export async function createSession(payload: Omit<SessionPayload, 'expiresAt'>)
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: '/',
-78
View File
@@ -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;
}
}
}
-101
View File
@@ -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"
-40
View File
@@ -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.
-90
View File
@@ -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();
-263
View File
@@ -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);
-50
View File
@@ -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();
-203
View File
@@ -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();
-55
View File
@@ -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);
});
+2 -1
View File
@@ -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 {
+1 -1
View File
@@ -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');
BIN
View File
Binary file not shown.