feat: add complete password requirements to registration form
- Display all 5 backend password validation requirements - Added: minimum 8 characters (existing) - Added: maximum 100 characters - Added: at least one lowercase letter - Added: at least one uppercase letter - Added: at least one number - Each requirement shows real-time validation with green checkmark - Requirements match exactly with backend/src/utils/validators.js feat: add Traefik labels to frontend container - Enable Traefik auto-discovery for frontend service - Configure HTTP routing with Host rule - Setup both web (HTTP) and websecure (HTTPS) entrypoints - Configure load balancer backend port (80) - Include optional TLS/Let's Encrypt configuration - Change edh.example.com to your actual domain feat: configure production compose file to use Traefik network - Generated docker-compose.prod.deployed.yml now uses traefik-network - Frontend service joined to both edh-stats-network and traefik-network - Added Traefik labels for routing and TLS configuration - traefik-network configured as external (must be created by Traefik) - Removed external ports from frontend (Traefik handles routing) - Updated setup instructions to mention Traefik prerequisites - Changed TLS to enabled by default in production refactor: remove frontend port exposure and Traefik labels from dev compose - Removed port 8081 exposure from development docker-compose.yml - Removed Traefik labels from development environment - Development compose now simple and focused on local testing - Production compose (via deploy.sh) handles Traefik routing via DNS - Frontend only accessible through backend API in development - Cleaner separation: dev is simple, prod uses Traefik Switch Postgres data to host path and bump version Update Traefik rule, expose port, bump version feat: add database migration script for PostgreSQL - Created migrate-database.sh script for exporting/importing databases - Supports source and target database configuration via CLI options - Validates connections before migration - Verifies data integrity after import (table and row counts) - Can skip import and just export to file for manual restore - Supports Docker Compose container names - Includes comprehensive error handling and user prompts - Added DATABASE_MIGRATION.md documentation with usage examples - Handles common scenarios: dev→prod, backups, restore - Security considerations for password handling refactor: simplify migration script to run directly in PostgreSQL container - Removed network/remote host configuration (no longer needed) - Script now runs inside PostgreSQL container with docker compose exec - Simplified to use only source-db, target-db, output-file, skip-import options - No external dependencies - uses container's pg_dump and psql - Much simpler usage: docker compose exec postgres /scripts/migrate-database.sh - Updated documentation with container-based examples - Added real-world integration examples (daily backups, deployments, recovery) - Includes troubleshooting and access patterns for backup files feat: mount scripts directory into PostgreSQL container - Added ./scripts:/scripts:ro volume mount to postgres service - Makes migrate-database.sh and other scripts accessible inside container - Read-only mount for security (scripts can't be modified inside container) - Allows running: docker compose exec postgres /scripts/migrate-database.sh - Scripts are shared between host and container for easy access docs: clarify that migration script must run as postgres user - Added -u postgres flag to all docker compose exec commands - Explains why postgres user is required (PostgreSQL role authentication) - Created shell alias for convenience - Updated all scenarios and examples - Updated troubleshooting section - Clarifies connection issues related to user authentication feat: save database backups to host by default - Added ./backups volume mount to postgres container - Changed default backup location from /tmp to /backups - /backups is mounted to ./backups on host for easy access - Script automatically creates /backups directory if needed - Updated help and examples with -u postgres flag - Summary now shows both container and host backup paths - Backups are immediately available on host machine - No need for docker cp to retrieve backups feat: add --skip-export flag for import-only database operations - Allows importing from existing backup files without re-exporting - Added validate_backup_file() function to check backup existence - Updated main() to handle import-only mode with proper validation - Updated summary output to show import-only mode details - Updated help text with import-only example - Prevents using both --skip-import and --skip-export together docs: update database migration guide for import-only mode - Document new --skip-export flag for import-only operations - Add example for quick restore from backup without re-export - Update command options table with mode combinations - Update all scenarios and examples to use /backups mount - Clarify file location and volume mounting (./backups on host) - Add Scenario 5 for quick restore from backup - Simplify examples and fix container paths feat: clear existing data before importing in migration script - Added clear_database() function to drop all tables, views, and sequences - Drops and recreates public schema with proper permissions - Ensures clean import without data conflicts - Updated warning message to clarify data deletion - clear_database() called before import starts - Maintains database integrity and grants docs: update migration guide to explain data clearing on import - Clarify that existing data is deleted before import - Explain the drop/recreate schema process - Add notes to scenarios about data clearing - Document the import process sequence - Update version to 2.2 fix: remove verbose flag from pg_dump to prevent SQL syntax errors - Removed -v flag from pg_dump export command - Verbose output was being included in SQL file as comments - These comments caused 'syntax error at or near pg_dump' errors during import - Backup files will now be clean SQL without pg_dump metadata comments docs: document pg_dump verbose output fix and troubleshooting - Added troubleshooting section for pg_dump syntax errors - Noted that v2.3 fixes this issue - Directed users to create new backups if needed - Updated version to 2.3 - Clarified file location is /backups/ docs: add critical warning about using migration script for imports - Added prominent warning against using psql directly with -f flag - Explained why direct psql causes 'relation already exists' errors - Added troubleshooting section for these errors - Emphasized that script handles data clearing automatically - Clear examples of wrong vs right approach fix: remove pg_dump restrict commands that block data import - Added clean_backup_file() function to remove \restrict and \unrestrict - pg_dump adds these security commands which prevent data loading - Script now automatically cleans backup files before importing - Removes lines starting with \restrict or \unrestrict - Ensures all data (users, games, commanders) imports successfully - Called automatically during import process docs: add troubleshooting for pg_dump restrict commands blocking imports - Document the \restrict and \unrestrict security commands issue - Explain why they block data from being imported - Show that migration script v2.4+ removes them automatically - Update version to 2.4 - Add detailed troubleshooting section for empty imports
This commit is contained in:
38
deploy.sh
38
deploy.sh
@@ -255,7 +255,9 @@ generate_deployment_config() {
|
||||
# Generated: $(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
# GitHub User: ${GITHUB_USER}
|
||||
#
|
||||
# IMPORTANT: Create a .env file with these variables:
|
||||
# IMPORTANT: Prerequisites
|
||||
# - Traefik must be running with 'traefik-network' created
|
||||
# - Create a .env file with these variables:
|
||||
# DB_NAME=edh_stats
|
||||
# DB_USER=postgres
|
||||
# DB_PASSWORD=\$(openssl rand -base64 32)
|
||||
@@ -266,10 +268,12 @@ generate_deployment_config() {
|
||||
# DB_SEED=false
|
||||
#
|
||||
# FIRST TIME SETUP:
|
||||
# 1. Create .env file with above variables
|
||||
# 2. Run: docker-compose -f docker-compose.prod.deployed.yml up -d
|
||||
# 3. Database migrations will run automatically via db-migrate service
|
||||
# 4. Monitor logs: docker-compose logs -f db-migrate
|
||||
# 1. Ensure Traefik is running and traefik-network exists
|
||||
# 2. Update frontend domain in labels (edh.example.com -> yourdomain.com)
|
||||
# 3. Create .env file with above variables
|
||||
# 4. Run: docker-compose -f docker-compose.prod.deployed.yml up -d
|
||||
# 5. Database migrations will run automatically via db-migrate service
|
||||
# 6. Monitor logs: docker-compose logs -f db-migrate
|
||||
|
||||
services:
|
||||
# PostgreSQL database service
|
||||
@@ -280,7 +284,7 @@ services:
|
||||
- POSTGRES_PASSWORD=\${DB_PASSWORD}
|
||||
- POSTGRES_DB=\${DB_NAME}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'PGPASSWORD=\${DB_PASSWORD} pg_isready -U postgres -h localhost']
|
||||
interval: 10s
|
||||
@@ -353,9 +357,6 @@ services:
|
||||
|
||||
frontend:
|
||||
image: ${FRONTEND_IMAGE}
|
||||
ports:
|
||||
- '38080:80'
|
||||
- '30443:443'
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
@@ -367,8 +368,23 @@ services:
|
||||
retries: 5
|
||||
networks:
|
||||
- edh-stats-network
|
||||
- traefik-network
|
||||
depends_on:
|
||||
- backend
|
||||
labels:
|
||||
# Enable Traefik discovery for this service
|
||||
- traefik.enable=true
|
||||
# Routing rule - change edh.example.com to your domain
|
||||
- traefik.http.routers.edh-stats-frontend.rule=Host(\`edh.zlor.fi\`)
|
||||
# Entry points: web (HTTP) and websecure (HTTPS)
|
||||
- traefik.http.routers.edh-stats-frontend.entrypoints=web,websecure
|
||||
# Service configuration
|
||||
- traefik.http.routers.edh-stats-frontend.service=edh-stats-frontend
|
||||
# Backend port (nginx internal port)
|
||||
- traefik.http.services.edh-stats-frontend.loadbalancer.server.port=80
|
||||
# Enable TLS with Let's Encrypt
|
||||
- traefik.http.routers.edh-stats-frontend.tls=true
|
||||
- traefik.http.routers.edh-stats-frontend.tls.certresolver=letsencrypt
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -386,6 +402,10 @@ networks:
|
||||
edh-stats-network:
|
||||
driver: bridge
|
||||
|
||||
traefik-network:
|
||||
external: true
|
||||
name: traefik-network
|
||||
|
||||
x-dockge:
|
||||
urls:
|
||||
- https://edh.zlor.fi
|
||||
|
||||
@@ -11,6 +11,8 @@ services:
|
||||
volumes:
|
||||
- ./postgres_data:/var/lib/postgresql/data
|
||||
- ./backend/init-db:/docker-entrypoint-initdb.d
|
||||
- ./scripts:/scripts:ro
|
||||
- ./backups:/backups
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
|
||||
646
docs/DATABASE_MIGRATION.md
Normal file
646
docs/DATABASE_MIGRATION.md
Normal file
@@ -0,0 +1,646 @@
|
||||
# Database Migration Guide
|
||||
|
||||
This guide explains how to use the database migration script to export and import PostgreSQL databases running in Docker containers.
|
||||
|
||||
## ⚠️ CRITICAL: Always Use the Migration Script
|
||||
|
||||
**DO NOT** import using `psql` directly with the `-f` flag:
|
||||
|
||||
```bash
|
||||
# ❌ WRONG - Will cause errors!
|
||||
docker compose exec -u postgres postgres psql -d edh_stats -f /backups/backup.sql
|
||||
```
|
||||
|
||||
**Always use the migration script** which automatically clears the database first:
|
||||
|
||||
```bash
|
||||
# ✅ RIGHT - Works perfectly!
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--target-db edh_stats \
|
||||
--output-file /backups/backup.sql \
|
||||
--skip-export
|
||||
```
|
||||
|
||||
**Why?** Direct `psql` import tries to create tables that already exist, causing:
|
||||
- `ERROR: relation "commanders" already exists`
|
||||
- `ERROR: multiple primary keys for table`
|
||||
- `ERROR: trigger already exists`
|
||||
|
||||
The migration script **automatically clears all data first**, preventing these conflicts.
|
||||
|
||||
## Overview
|
||||
|
||||
The `scripts/migrate-database.sh` script runs directly inside the PostgreSQL container and provides:
|
||||
- **Exporting** data from one database to a file
|
||||
- **Importing** data from a file into another database
|
||||
- **Verifying** that the import was successful
|
||||
|
||||
This approach is simple because:
|
||||
- ✅ No need to install PostgreSQL client tools on your host
|
||||
- ✅ Runs directly inside the container with full access
|
||||
- ✅ All tools (pg_dump, psql) are already available
|
||||
- ✅ Works seamlessly with Docker Compose
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required
|
||||
- Docker Compose running with PostgreSQL service
|
||||
- The `scripts/migrate-database.sh` file in your project
|
||||
|
||||
### Not Required
|
||||
- PostgreSQL client tools on host machine
|
||||
- SSH access to servers
|
||||
- Network connectivity setup
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Important: Run as postgres user
|
||||
|
||||
All commands must be run with `-u postgres` to authenticate with PostgreSQL:
|
||||
|
||||
```bash
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh [OPTIONS]
|
||||
```
|
||||
|
||||
### 1. Export Database (Backup)
|
||||
|
||||
Export the current database to a backup file:
|
||||
|
||||
```bash
|
||||
# Export to file inside container (default: /tmp/edh_stats_backup_TIMESTAMP.sql)
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--source-db edh_stats \
|
||||
--skip-import
|
||||
|
||||
# Export to custom location
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--source-db edh_stats \
|
||||
--output-file /var/lib/postgresql/backups/my_backup.sql \
|
||||
--skip-import
|
||||
```
|
||||
|
||||
### 2. Export and Import to Different Database
|
||||
|
||||
Migrate data from one database to another (both in same container):
|
||||
|
||||
```bash
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--source-db edh_stats \
|
||||
--target-db edh_stats_new
|
||||
```
|
||||
|
||||
### 3. Import from Existing Backup (Import-Only)
|
||||
|
||||
Import data from an existing backup file without exporting:
|
||||
|
||||
```bash
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--target-db edh_stats \
|
||||
--output-file /backups/edh_stats_backup_20250118_120000.sql \
|
||||
--skip-export
|
||||
```
|
||||
|
||||
This is useful when:
|
||||
- You have a backup file already (from previous export)
|
||||
- You want to import without re-exporting
|
||||
- You're restoring from a backup file
|
||||
- You're importing from external source
|
||||
|
||||
### 4. Export from Production to Development
|
||||
|
||||
Copy production data to your local development environment:
|
||||
|
||||
```bash
|
||||
# On production server
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--source-db edh_stats \
|
||||
--output-file /backups/prod_backup.sql \
|
||||
--skip-import
|
||||
|
||||
# Copy file to your local machine
|
||||
docker compose cp <container_id>:/backups/prod_backup.sql ./
|
||||
|
||||
# Import locally (import-only mode)
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--target-db edh_stats \
|
||||
--output-file /backups/prod_backup.sql \
|
||||
--skip-export
|
||||
```
|
||||
|
||||
### Optional: Create an Alias for Convenience
|
||||
|
||||
To avoid typing `-u postgres` every time, add this to your shell profile:
|
||||
|
||||
```bash
|
||||
# Add to ~/.bash_profile, ~/.bashrc, or ~/.zshrc
|
||||
alias pg-migrate='docker compose exec -u postgres postgres /scripts/migrate-database.sh'
|
||||
```
|
||||
|
||||
Then reload your shell:
|
||||
```bash
|
||||
source ~/.bashrc # or ~/.zshrc for zsh
|
||||
```
|
||||
|
||||
Now use it simply:
|
||||
```bash
|
||||
pg-migrate --source-db edh_stats --skip-import
|
||||
pg-migrate --source-db edh_stats --target-db edh_stats_new
|
||||
pg-migrate --target-db edh_stats --output-file /backups/backup.sql --skip-export
|
||||
pg-migrate --help
|
||||
```
|
||||
|
||||
## Command Line Options
|
||||
|
||||
```bash
|
||||
--source-db DATABASE Source database name (default: edh_stats)
|
||||
--target-db DATABASE Target database name (default: edh_stats)
|
||||
--output-file FILE Backup file path (default: /backups/edh_stats_backup_TIMESTAMP.sql)
|
||||
--skip-import Export only, don't import (backup mode)
|
||||
--skip-export Import only, don't export (restore mode - requires existing file)
|
||||
--help Show help message
|
||||
```
|
||||
|
||||
### Mode Combinations
|
||||
|
||||
- **Export + Import** (Default): `--source-db X --target-db Y` - Export from X, import to Y
|
||||
- **Export Only**: `--source-db X --skip-import` - Backup database X to file
|
||||
- **Import Only**: `--target-db Y --output-file backup.sql --skip-export` - Restore file to Y
|
||||
- **Both flags** (`--skip-import` + `--skip-export`): Error - not allowed
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### Scenario 1: Daily Backup
|
||||
|
||||
Create a daily backup of the production database:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup-prod.sh
|
||||
|
||||
BACKUP_DATE=$(date +%Y%m%d)
|
||||
BACKUP_DIR="./backups"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
docker compose -f docker-compose.prod.deployed.yml exec -u postgres postgres \
|
||||
/scripts/migrate-database.sh \
|
||||
--source-db edh_stats \
|
||||
--output-file /var/lib/postgresql/backups/prod_${BACKUP_DATE}.sql \
|
||||
--skip-import
|
||||
|
||||
echo "Backup completed: $BACKUP_DIR/prod_${BACKUP_DATE}.sql"
|
||||
```
|
||||
|
||||
Run daily with cron:
|
||||
```bash
|
||||
0 2 * * * cd /path/to/project && ./backup-prod.sh
|
||||
```
|
||||
|
||||
### Scenario 2: Test Database Refresh
|
||||
|
||||
Refresh your test database with latest production data:
|
||||
|
||||
```bash
|
||||
# Export from production
|
||||
docker compose -f docker-compose.prod.deployed.yml exec -u postgres postgres \
|
||||
/scripts/migrate-database.sh \
|
||||
--source-db edh_stats \
|
||||
--output-file /backups/test_refresh.sql \
|
||||
--skip-import
|
||||
|
||||
# Copy to local test environment
|
||||
docker compose cp <prod_container>:/backups/test_refresh.sql ./
|
||||
|
||||
# Import to local test database (import-only mode)
|
||||
docker compose exec -u postgres postgres \
|
||||
/scripts/migrate-database.sh \
|
||||
--target-db edh_stats_test \
|
||||
--output-file /backups/test_refresh.sql \
|
||||
--skip-export
|
||||
```
|
||||
|
||||
### Scenario 3: Database Upgrade
|
||||
|
||||
Backup before upgrading PostgreSQL version:
|
||||
|
||||
```bash
|
||||
# Backup current database
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--source-db edh_stats \
|
||||
--output-file /backups/pre_upgrade.sql \
|
||||
--skip-import
|
||||
|
||||
# Stop services and upgrade PostgreSQL in docker-compose.yml
|
||||
|
||||
# Then restore if needed (import-only mode)
|
||||
# NOTE: Existing data will be cleared before import
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--target-db edh_stats \
|
||||
--output-file /backups/pre_upgrade.sql \
|
||||
--skip-export
|
||||
```
|
||||
|
||||
### Scenario 4: Development Environment Setup
|
||||
|
||||
Setup development with production data:
|
||||
|
||||
```bash
|
||||
# Export from production
|
||||
ssh prod-server "cd /edh-stats && docker compose exec -u postgres postgres /scripts/migrate-database.sh --source-db edh_stats --output-file /backups/dev_setup.sql --skip-import"
|
||||
|
||||
# Copy to local machine
|
||||
scp prod-server:/path/to/backups/dev_setup.sql ./
|
||||
|
||||
# Import locally (import-only mode)
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--target-db edh_stats \
|
||||
--output-file /backups/dev_setup.sql \
|
||||
--skip-export
|
||||
```
|
||||
|
||||
### Scenario 5: Quick Restore from Backup
|
||||
|
||||
Restore from a backup file without re-exporting:
|
||||
|
||||
```bash
|
||||
# List available backups
|
||||
docker compose exec postgres ls -lh /backups/
|
||||
|
||||
# Restore from specific backup (import-only)
|
||||
# NOTE: Existing data will be cleared before import
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--target-db edh_stats \
|
||||
--output-file /backups/edh_stats_backup_20250118_120000.sql \
|
||||
--skip-export
|
||||
```
|
||||
|
||||
## What Gets Migrated
|
||||
|
||||
### Included ✅
|
||||
- All tables and schemas
|
||||
- All data (users, commanders, games)
|
||||
- Primary keys and foreign keys
|
||||
- Indexes and constraints
|
||||
- Sequences
|
||||
|
||||
### Not Included ❌
|
||||
- PostgreSQL roles/users (database-level)
|
||||
- Database server settings
|
||||
- Extension configurations
|
||||
|
||||
## File Locations
|
||||
|
||||
Inside the PostgreSQL container:
|
||||
- **Default backup**: `/backups/edh_stats_backup_TIMESTAMP.sql`
|
||||
- **In container**: Access files in container paths
|
||||
|
||||
From host machine:
|
||||
- **Host backups**: `./backups/` (mounted volume)
|
||||
- **Copy from container**: `docker compose cp <container>:/backups/file ./`
|
||||
- **Copy to container**: `docker compose cp ./file <container>:/backups/`
|
||||
|
||||
Files in `/backups` are automatically synced between container and host via volume mount.
|
||||
|
||||
## Access Backup Files from Host
|
||||
|
||||
### Option 1: Copy from Container
|
||||
|
||||
```bash
|
||||
# List backups in container
|
||||
docker compose exec postgres ls -lh /var/lib/postgresql/backups/
|
||||
|
||||
# Copy backup to host
|
||||
docker compose cp postgres:/var/lib/postgresql/backups/backup.sql ./
|
||||
|
||||
# Copy to host with docker compose
|
||||
docker compose cp <container_name>:/var/lib/postgresql/backups/backup.sql ./backups/
|
||||
```
|
||||
|
||||
### Option 2: Use Docker Volumes (Already Configured)
|
||||
|
||||
The `docker-compose.yml` already has this configured:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
volumes:
|
||||
- ./backups:/backups
|
||||
```
|
||||
|
||||
Backups are automatically accessible in `./backups` on host. No additional setup needed!
|
||||
|
||||
### Option 3: Use the /backups Mount (Recommended)
|
||||
|
||||
The `/backups` directory is already mounted to `./backups` on your host:
|
||||
|
||||
```bash
|
||||
# Export to /backups (automatically synced to ./backups on host)
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--source-db edh_stats \
|
||||
--output-file /backups/my_backup.sql \
|
||||
--skip-import
|
||||
|
||||
# File automatically appears at: ./backups/my_backup.sql on host
|
||||
```
|
||||
|
||||
## Import Process
|
||||
|
||||
When importing, the script follows this sequence:
|
||||
|
||||
1. **Validates** backup file and target database exist
|
||||
2. **Confirms** with user before proceeding
|
||||
3. **Clears** all existing data from target database (drops/recreates schema)
|
||||
4. **Imports** data from backup file
|
||||
5. **Verifies** that import was successful
|
||||
|
||||
### Data Clearing
|
||||
|
||||
Before importing, the script:
|
||||
- Drops the entire `public` schema (removes all tables, views, sequences)
|
||||
- Recreates the `public` schema with proper permissions
|
||||
- Ensures a clean slate for the imported data
|
||||
|
||||
This means **all existing data in the target database will be deleted**. The script asks for confirmation before proceeding.
|
||||
|
||||
## Verification
|
||||
|
||||
The script automatically verifies after import:
|
||||
|
||||
```
|
||||
════════════════════════════════════════════════════════════
|
||||
Verifying Data Import
|
||||
════════════════════════════════════════════════════════════
|
||||
|
||||
ℹ Checking table counts...
|
||||
ℹ Source tables: 5
|
||||
ℹ Target tables: 5
|
||||
✓ Table counts match
|
||||
|
||||
ℹ Checking row counts...
|
||||
✓ Table 'users': 5 rows (✓ matches)
|
||||
✓ Table 'commanders': 12 rows (✓ matches)
|
||||
✓ Table 'games': 48 rows (✓ matches)
|
||||
```
|
||||
|
||||
## Manual Commands (If Needed)
|
||||
|
||||
If the script fails, you can run commands directly:
|
||||
|
||||
```bash
|
||||
# Backup manually
|
||||
docker compose exec postgres pg_dump edh_stats > backup.sql
|
||||
|
||||
# Restore manually
|
||||
docker compose exec -T postgres psql edh_stats < backup.sql
|
||||
|
||||
# List databases
|
||||
docker compose exec postgres psql -l
|
||||
|
||||
# Get database size
|
||||
docker compose exec postgres psql -c "SELECT pg_size_pretty(pg_database_size('edh_stats'));"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "relation already exists" or "multiple primary keys" errors during import
|
||||
|
||||
**Cause**: You're using `psql` directly instead of the migration script:
|
||||
|
||||
```bash
|
||||
# ❌ WRONG
|
||||
docker compose exec -u postgres postgres psql -d edh_stats -f /backups/backup.sql
|
||||
```
|
||||
|
||||
**Solution**: Use the migration script which clears the database first:
|
||||
|
||||
```bash
|
||||
# ✅ RIGHT
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--target-db edh_stats \
|
||||
--output-file /backups/backup.sql \
|
||||
--skip-export
|
||||
```
|
||||
|
||||
The script automatically:
|
||||
1. Removes pg_dump restrict commands (prevents data blocking)
|
||||
2. Drops the existing schema
|
||||
3. Recreates the schema
|
||||
4. Imports the backup file
|
||||
|
||||
This prevents "already exists" conflicts.
|
||||
|
||||
### No data imported (users, games, commanders empty)
|
||||
|
||||
**Cause**: Your backup file contains pg_dump security commands:
|
||||
|
||||
```sql
|
||||
\restrict IdvbAL1gCAhQZc4dsPYgIzErSH0gRztgmxsbr3dcnr1I1Wymp9VCK54cbXqCR5P
|
||||
```
|
||||
|
||||
These `\restrict` and `\unrestrict` commands tell psql to enter restricted mode, which blocks data loading.
|
||||
|
||||
**Solution**: Use the migration script (v2.4+) which automatically removes these:
|
||||
|
||||
```bash
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--target-db edh_stats \
|
||||
--output-file /backups/backup.sql \
|
||||
--skip-export
|
||||
```
|
||||
|
||||
The script now:
|
||||
1. ✓ Detects restrict commands
|
||||
2. ✓ Removes them automatically
|
||||
3. ✓ Imports all data successfully
|
||||
|
||||
### "psql is not available"
|
||||
|
||||
The script must run inside the PostgreSQL container. Use:
|
||||
```bash
|
||||
docker compose exec postgres /scripts/migrate-database.sh
|
||||
```
|
||||
|
||||
Not just:
|
||||
```bash
|
||||
./scripts/migrate-database.sh # Wrong - runs on host
|
||||
```
|
||||
|
||||
### "Source database does not exist"
|
||||
|
||||
Check available databases:
|
||||
```bash
|
||||
docker compose exec postgres psql -l
|
||||
```
|
||||
|
||||
Make sure the database name is correct and exists.
|
||||
|
||||
### "Target database does not exist"
|
||||
|
||||
Create the target database first:
|
||||
```bash
|
||||
docker compose exec postgres createdb edh_stats_new
|
||||
```
|
||||
|
||||
Then run the migration.
|
||||
|
||||
### "Permission denied" on output file
|
||||
|
||||
Ensure the directory exists and is writable:
|
||||
```bash
|
||||
# Check directory
|
||||
docker compose exec postgres ls -ld /var/lib/postgresql/backups/
|
||||
|
||||
# Create if needed
|
||||
docker compose exec postgres mkdir -p /var/lib/postgresql/backups/
|
||||
```
|
||||
|
||||
### Import takes too long
|
||||
|
||||
For large databases, import runs in the background:
|
||||
```bash
|
||||
# Monitor progress
|
||||
docker compose logs -f postgres
|
||||
```
|
||||
|
||||
### File not found after export
|
||||
|
||||
Check where the file was written:
|
||||
```bash
|
||||
docker compose exec postgres ls -lh /backups/edh_stats_backup_*
|
||||
```
|
||||
|
||||
Files are automatically synced to host at `./backups/` via volume mount.
|
||||
|
||||
### Backup file has "pg_dump" syntax errors during import
|
||||
|
||||
This issue was fixed in v2.3. If you have old backup files from earlier versions that contain pg_dump comments (like `pg_dump: creating TABLE`), they may cause import errors.
|
||||
|
||||
**Solution**: Create a new backup with the updated script:
|
||||
```bash
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--source-db edh_stats \
|
||||
--skip-import
|
||||
```
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Backup Before Deployment
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# deploy.sh
|
||||
|
||||
# Create backup before deploying
|
||||
echo "Creating backup..."
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--source-db edh_stats \
|
||||
--output-file /backups/pre_deploy_$(date +%s).sql \
|
||||
--skip-import
|
||||
|
||||
# Deploy application
|
||||
echo "Deploying..."
|
||||
docker compose -f docker-compose.prod.deployed.yml pull
|
||||
docker compose -f docker-compose.prod.deployed.yml up -d
|
||||
|
||||
# Run schema migrations if needed
|
||||
docker compose -f docker-compose.prod.deployed.yml exec backend npm run migrate
|
||||
|
||||
echo "Deployment complete!"
|
||||
```
|
||||
|
||||
### Continuous Backup Job
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# cron-backup.sh
|
||||
|
||||
BACKUP_DIR="./backups"
|
||||
RETENTION_DAYS=30
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Create backup (automatically goes to ./backups on host)
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--source-db edh_stats \
|
||||
--output-file /backups/edh_stats_$(date +%Y%m%d_%H%M%S).sql \
|
||||
--skip-import
|
||||
|
||||
# Keep only last N days of backups
|
||||
find "$BACKUP_DIR" -name "edh_stats_*.sql" -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
echo "Backup completed at $(date)"
|
||||
```
|
||||
|
||||
### Recovery Procedure
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# recover.sh - Restore from backup (import-only)
|
||||
|
||||
BACKUP_FILE="${1:?Usage: $0 <backup_file>}"
|
||||
|
||||
if [ ! -f "$BACKUP_FILE" ]; then
|
||||
echo "Error: Backup file not found: $BACKUP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy file into container
|
||||
docker compose cp "$BACKUP_FILE" postgres:/backups/restore.sql
|
||||
|
||||
# Import (import-only mode - skip export)
|
||||
docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
--target-db edh_stats \
|
||||
--output-file /backups/restore.sql \
|
||||
--skip-export
|
||||
|
||||
echo "Recovery complete from: $BACKUP_FILE"
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Backup Files
|
||||
- Backups contain all data including sensitive information
|
||||
- Keep backup files secure
|
||||
- Delete old backups
|
||||
- Consider encrypting backups
|
||||
|
||||
```bash
|
||||
# Secure permissions on backups
|
||||
docker compose exec postgres chmod 600 /backups/*.sql
|
||||
|
||||
# Encrypt backup
|
||||
docker compose exec postgres \
|
||||
gpg --symmetric --cipher-algo AES256 /backups/backup.sql
|
||||
```
|
||||
|
||||
### Container Access
|
||||
- Only authorized users should run this script
|
||||
- Audit backup and restore operations
|
||||
- Use Docker Compose for local development only
|
||||
|
||||
## Performance Tips
|
||||
|
||||
- Run exports during off-peak hours
|
||||
- For large databases (>1GB), export-only mode is faster
|
||||
- Monitor container resources during import
|
||||
- Disable unnecessary services during import
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [PostgreSQL pg_dump Documentation](https://www.postgresql.org/docs/current/app-pgdump.html)
|
||||
- [PostgreSQL psql Documentation](https://www.postgresql.org/docs/current/app-psql.html)
|
||||
- [Docker Compose exec Documentation](https://docs.docker.com/compose/compose-file/compose-file-v3/#exec)
|
||||
|
||||
## Version
|
||||
|
||||
Script version: 2.4 (Container Edition - Fixed pg_dump Restrictions)
|
||||
Last updated: 2026-01-18
|
||||
Compatible with: PostgreSQL 10+ in Docker, EDH Stats v1.0+
|
||||
|
||||
### Version History
|
||||
- **v2.4**: Remove pg_dump restrict/unrestrict commands that block data import
|
||||
- **v2.3**: Fixed pg_dump verbose output causing SQL syntax errors during import
|
||||
- **v2.2**: Auto-clear existing data before import (drop/recreate schema)
|
||||
- **v2.1**: Added `--skip-export` flag for import-only operations
|
||||
- **v2.0**: Initial container-based version with export/import
|
||||
- **v1.0**: Original version
|
||||
@@ -8,376 +8,465 @@
|
||||
name="description"
|
||||
content="Create a new account to track your Magic: The Gathering EDH/Commander games"
|
||||
/>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="/css/styles.css" />
|
||||
</head>
|
||||
<body
|
||||
class="min-h-full flex flex-col py-12 px-4 sm:px-6 lg:px-8"
|
||||
>
|
||||
<body class="min-h-full flex flex-col py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-center flex-1">
|
||||
<div class="max-w-md w-full space-y-8" x-data="registerForm()" x-init="init()">
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl font-bold font-mtg text-edh-primary mb-2">
|
||||
EDH Stats
|
||||
</h1>
|
||||
<h2 class="text-xl text-gray-600" x-text="allowRegistration ? 'Create your account' : 'Registration Disabled'"></h2>
|
||||
</div>
|
||||
<div
|
||||
class="max-w-md w-full space-y-8"
|
||||
x-data="registerForm()"
|
||||
x-init="init()"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl font-bold font-mtg text-edh-primary mb-2">
|
||||
EDH Stats
|
||||
</h1>
|
||||
<h2
|
||||
class="text-xl text-gray-600"
|
||||
x-text="allowRegistration ? 'Create your account' : 'Registration Disabled'"
|
||||
></h2>
|
||||
</div>
|
||||
|
||||
<!-- Registration Disabled Message -->
|
||||
<div x-show="!allowRegistration" x-transition class="card bg-yellow-50 border-yellow-200">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<!-- Registration Disabled Message -->
|
||||
<div
|
||||
x-show="!allowRegistration"
|
||||
x-transition
|
||||
class="card bg-yellow-50 border-yellow-200"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-5 w-5 text-yellow-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-800">
|
||||
User registration is currently disabled. Please contact an
|
||||
administrator if you need to create an account.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-800">User registration is currently disabled. Please contact an administrator if you need to create an account.</p>
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
If you already have an account,
|
||||
<a
|
||||
href="/login.html"
|
||||
class="font-medium text-edh-accent hover:text-edh-primary"
|
||||
>
|
||||
sign in here
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
If you already have an account,
|
||||
<a
|
||||
href="/login.html"
|
||||
class="font-medium text-edh-accent hover:text-edh-primary"
|
||||
|
||||
<!-- Register Form -->
|
||||
<div class="card" x-show="allowRegistration">
|
||||
<form class="space-y-6" @submit.prevent="handleRegister">
|
||||
<!-- Username Field -->
|
||||
<div>
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
x-model="formData.username"
|
||||
@input="validateUsername()"
|
||||
:class="errors.username ? 'border-red-500 focus:ring-red-500' : ''"
|
||||
class="form-input pl-10"
|
||||
placeholder="Choose a username (3+ characters)"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
x-show="errors.username"
|
||||
x-text="errors.username"
|
||||
class="form-error"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<!-- Email Field -->
|
||||
<div>
|
||||
<label for="email" class="form-label"
|
||||
>Email Address (Optional)</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
x-model="formData.email"
|
||||
@input="validateEmail()"
|
||||
:class="errors.email ? 'border-red-500 focus:ring-red-500' : ''"
|
||||
class="form-input pl-10"
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
x-show="errors.email"
|
||||
x-text="errors.email"
|
||||
class="form-error"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div>
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
required
|
||||
x-model="formData.password"
|
||||
@input="validatePassword()"
|
||||
:class="errors.password ? 'border-red-500 focus:ring-red-500' : ''"
|
||||
class="form-input pl-10 pr-10"
|
||||
placeholder="Minimum 8 characters"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
>
|
||||
<svg
|
||||
x-show="!showPassword"
|
||||
class="h-5 w-5 text-gray-400 hover:text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
></path>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
x-show="showPassword"
|
||||
class="h-5 w-5 text-gray-400 hover:text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
x-show="errors.password"
|
||||
x-text="errors.password"
|
||||
class="form-error"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password Field -->
|
||||
<div>
|
||||
<label for="confirmPassword" class="form-label"
|
||||
>Confirm Password</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
:type="showConfirmPassword ? 'text' : 'password'"
|
||||
required
|
||||
x-model="formData.confirmPassword"
|
||||
@input="validateConfirmPassword()"
|
||||
:class="errors.confirmPassword ? 'border-red-500 focus:ring-red-500' : ''"
|
||||
class="form-input pl-10 pr-10"
|
||||
placeholder="Re-enter your password"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="showConfirmPassword = !showConfirmPassword"
|
||||
class="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
>
|
||||
<svg
|
||||
x-show="!showConfirmPassword"
|
||||
class="h-5 w-5 text-gray-400 hover:text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
></path>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
x-show="showConfirmPassword"
|
||||
class="h-5 w-5 text-gray-400 hover:text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
x-show="errors.confirmPassword"
|
||||
x-text="errors.confirmPassword"
|
||||
class="form-error"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<!-- Terms & Conditions -->
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="terms"
|
||||
name="terms"
|
||||
type="checkbox"
|
||||
x-model="formData.terms"
|
||||
@change="validateTerms()"
|
||||
:class="errors.terms ? 'border-red-500' : ''"
|
||||
class="h-4 w-4 text-edh-accent focus:ring-edh-accent border-gray-300 rounded"
|
||||
/>
|
||||
<label for="terms" class="ml-2 block text-sm text-gray-900">
|
||||
I agree to the
|
||||
<a href="#" class="text-edh-accent hover:text-edh-primary"
|
||||
>Terms of Service</a
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<p
|
||||
x-show="errors.terms"
|
||||
x-text="errors.terms"
|
||||
class="form-error"
|
||||
></p>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div
|
||||
x-show="serverError"
|
||||
x-transition
|
||||
class="rounded-md bg-red-50 p-4"
|
||||
>
|
||||
sign in here
|
||||
</a>
|
||||
</p>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-5 w-5 text-red-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-red-800" x-text="serverError"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div
|
||||
x-show="successMessage"
|
||||
x-transition
|
||||
class="rounded-md bg-green-50 p-4"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-5 w-5 text-green-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-green-800" x-text="successMessage"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="btn btn-primary w-full flex justify-center items-center space-x-2"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': loading }"
|
||||
>
|
||||
<svg
|
||||
x-show="!loading"
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
x-show="loading"
|
||||
class="animate-spin h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<span
|
||||
x-text="loading ? 'Creating account...' : 'Create account'"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Login Link -->
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
Already have an account?
|
||||
<a
|
||||
href="/login.html"
|
||||
class="font-medium text-edh-accent hover:text-edh-primary"
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register Form -->
|
||||
<div class="card" x-show="allowRegistration">
|
||||
<form class="space-y-6" @submit.prevent="handleRegister">
|
||||
<!-- Username Field -->
|
||||
<div>
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
x-model="formData.username"
|
||||
@input="validateUsername()"
|
||||
:class="errors.username ? 'border-red-500 focus:ring-red-500' : ''"
|
||||
class="form-input pl-10"
|
||||
placeholder="Choose a username (3+ characters)"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Password Requirements Info -->
|
||||
<div class="card bg-blue-50 border-blue-200" x-show="allowRegistration">
|
||||
<h3 class="text-sm font-medium text-blue-800 mb-2">
|
||||
Password Requirements
|
||||
</h3>
|
||||
<div class="text-xs text-blue-700 space-y-1">
|
||||
<p
|
||||
x-show="errors.username"
|
||||
x-text="errors.username"
|
||||
class="form-error"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<!-- Email Field -->
|
||||
<div>
|
||||
<label for="email" class="form-label"
|
||||
>Email Address (Optional)</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
x-model="formData.email"
|
||||
@input="validateEmail()"
|
||||
:class="errors.email ? 'border-red-500 focus:ring-red-500' : ''"
|
||||
class="form-input pl-10"
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
x-show="errors.email"
|
||||
x-text="errors.email"
|
||||
class="form-error"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div>
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
required
|
||||
x-model="formData.password"
|
||||
@input="validatePassword()"
|
||||
:class="errors.password ? 'border-red-500 focus:ring-red-500' : ''"
|
||||
class="form-input pl-10 pr-10"
|
||||
placeholder="Minimum 8 characters"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
>
|
||||
<svg
|
||||
x-show="!showPassword"
|
||||
class="h-5 w-5 text-gray-400 hover:text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
></path>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
x-show="showPassword"
|
||||
class="h-5 w-5 text-gray-400 hover:text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
x-show="errors.password"
|
||||
x-text="errors.password"
|
||||
class="form-error"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password Field -->
|
||||
<div>
|
||||
<label for="confirmPassword" class="form-label"
|
||||
>Confirm Password</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
:type="showConfirmPassword ? 'text' : 'password'"
|
||||
required
|
||||
x-model="formData.confirmPassword"
|
||||
@input="validateConfirmPassword()"
|
||||
:class="errors.confirmPassword ? 'border-red-500 focus:ring-red-500' : ''"
|
||||
class="form-input pl-10 pr-10"
|
||||
placeholder="Re-enter your password"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="showConfirmPassword = !showConfirmPassword"
|
||||
class="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
>
|
||||
<svg
|
||||
x-show="!showConfirmPassword"
|
||||
class="h-5 w-5 text-gray-400 hover:text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
></path>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
x-show="showConfirmPassword"
|
||||
class="h-5 w-5 text-gray-400 hover:text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
x-show="errors.confirmPassword"
|
||||
x-text="errors.confirmPassword"
|
||||
class="form-error"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<!-- Terms & Conditions -->
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="terms"
|
||||
name="terms"
|
||||
type="checkbox"
|
||||
x-model="formData.terms"
|
||||
@change="validateTerms()"
|
||||
:class="errors.terms ? 'border-red-500' : ''"
|
||||
class="h-4 w-4 text-edh-accent focus:ring-edh-accent border-gray-300 rounded"
|
||||
/>
|
||||
<label for="terms" class="ml-2 block text-sm text-gray-900">
|
||||
I agree to the
|
||||
<a href="#" class="text-edh-accent hover:text-edh-primary"
|
||||
>Terms of Service</a
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<p x-show="errors.terms" x-text="errors.terms" class="form-error"></p>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div
|
||||
x-show="serverError"
|
||||
x-transition
|
||||
class="rounded-md bg-red-50 p-4"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-5 w-5 text-red-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-red-800" x-text="serverError"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div
|
||||
x-show="successMessage"
|
||||
x-transition
|
||||
class="rounded-md bg-green-50 p-4"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-5 w-5 text-green-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-green-800" x-text="successMessage"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="btn btn-primary w-full flex justify-center items-center space-x-2"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': loading }"
|
||||
class="flex items-center"
|
||||
:class="{ 'text-green-700': formData.password.length >= 8 }"
|
||||
>
|
||||
<svg
|
||||
x-show="!loading"
|
||||
class="w-5 h-5"
|
||||
:class="{ 'text-green-500': formData.password.length >= 8 }"
|
||||
class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -386,88 +475,83 @@
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
|
||||
d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
At least 8 characters
|
||||
</p>
|
||||
<p
|
||||
class="flex items-center"
|
||||
:class="{ 'text-green-700': /(?=.*[a-z])/.test(formData.password) }"
|
||||
>
|
||||
<svg
|
||||
x-show="loading"
|
||||
class="animate-spin h-5 w-5"
|
||||
:class="{ 'text-green-500': /(?=.*[a-z])/.test(formData.password) }"
|
||||
class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
<span
|
||||
x-text="loading ? 'Creating account...' : 'Create account'"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Login Link -->
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
Already have an account?
|
||||
<a
|
||||
href="/login.html"
|
||||
class="font-medium text-edh-accent hover:text-edh-primary"
|
||||
At least one lowercase letter
|
||||
</p>
|
||||
<p
|
||||
class="flex items-center"
|
||||
:class="{ 'text-green-700': /(?=.*[A-Z])/.test(formData.password) }"
|
||||
>
|
||||
<svg
|
||||
:class="{ 'text-green-500': /(?=.*[A-Z])/.test(formData.password) }"
|
||||
class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
At least one uppercase letter
|
||||
</p>
|
||||
<p
|
||||
class="flex items-center"
|
||||
:class="{ 'text-green-700': /(?=.*\d)/.test(formData.password) }"
|
||||
>
|
||||
<svg
|
||||
:class="{ 'text-green-500': /(?=.*\d)/.test(formData.password) }"
|
||||
class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
At least one number (0-9)
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Password Requirements Info -->
|
||||
<div class="card bg-blue-50 border-blue-200" x-show="allowRegistration">
|
||||
<h3 class="text-sm font-medium text-blue-800 mb-2">
|
||||
Password Requirements
|
||||
</h3>
|
||||
<div class="text-xs text-blue-700 space-y-1">
|
||||
<p
|
||||
class="flex items-center"
|
||||
:class="{ 'text-green-700': formData.password.length >= 8 }"
|
||||
>
|
||||
<svg
|
||||
:class="{ 'text-green-500': formData.password.length >= 8 }"
|
||||
class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
At least 8 characters
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts --> <script
|
||||
<!-- Scripts -->
|
||||
<script
|
||||
defer
|
||||
src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"
|
||||
></script>
|
||||
<script src="/js/auth.js"></script>
|
||||
|
||||
|
||||
<script src="/js/footer-loader.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.1.2
|
||||
2.0.2
|
||||
|
||||
504
scripts/migrate-database.sh
Executable file
504
scripts/migrate-database.sh
Executable file
@@ -0,0 +1,504 @@
|
||||
#!/bin/bash
|
||||
|
||||
##############################################################################
|
||||
# EDH Stats Tracker - Database Migration Script (Container Version)
|
||||
#
|
||||
# This script exports data from one PostgreSQL database and imports it
|
||||
# into another database, running directly inside the PostgreSQL container.
|
||||
#
|
||||
# Usage: docker compose exec postgres /scripts/migrate-database.sh [OPTIONS]
|
||||
#
|
||||
# Options:
|
||||
# --source-db DATABASE Source database name (default: edh_stats)
|
||||
# --target-db DATABASE Target database name (default: edh_stats)
|
||||
# --output-file FILE Backup file path (default: /backups/backup_TIMESTAMP.sql)
|
||||
# --skip-import Export only, don't import
|
||||
# --skip-export Import only, don't export (must provide existing file)
|
||||
# --help Show this help message
|
||||
#
|
||||
# Examples:
|
||||
# # Export current database (saves to ./backups on host)
|
||||
# docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
# --source-db edh_stats \
|
||||
# --skip-import
|
||||
#
|
||||
# # Export and import to different database
|
||||
# docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
# --source-db edh_stats \
|
||||
# --target-db edh_stats_new
|
||||
#
|
||||
# # Import from existing backup file (import-only)
|
||||
# docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
# --target-db edh_stats \
|
||||
# --output-file /backups/edh_stats_backup_20250118_120000.sql \
|
||||
# --skip-export
|
||||
#
|
||||
# # Export to custom location (still on host via mount)
|
||||
# docker compose exec -u postgres postgres /scripts/migrate-database.sh \
|
||||
# --source-db edh_stats \
|
||||
# --output-file /backups/custom_backup.sql \
|
||||
# --skip-import
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
set -e
|
||||
|
||||
# Color codes for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Defaults
|
||||
SOURCE_DB="edh_stats"
|
||||
TARGET_DB="edh_stats"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
OUTPUT_FILE="/backups/edh_stats_backup_${TIMESTAMP}.sql"
|
||||
SKIP_IMPORT=false
|
||||
SKIP_EXPORT=false
|
||||
|
||||
##############################################################################
|
||||
# Helper Functions
|
||||
##############################################################################
|
||||
|
||||
print_header() {
|
||||
echo -e "\n${BLUE}════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${BLUE} $1${NC}"
|
||||
echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}\n"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ $1${NC}"
|
||||
}
|
||||
|
||||
show_help() {
|
||||
head -43 "$0" | tail -39
|
||||
exit 0
|
||||
}
|
||||
|
||||
##############################################################################
|
||||
# Argument Parsing
|
||||
##############################################################################
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--source-db)
|
||||
SOURCE_DB="$2"
|
||||
shift 2
|
||||
;;
|
||||
--target-db)
|
||||
TARGET_DB="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output-file)
|
||||
OUTPUT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-import)
|
||||
SKIP_IMPORT=true
|
||||
shift
|
||||
;;
|
||||
--skip-export)
|
||||
SKIP_EXPORT=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown option: $1"
|
||||
show_help
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
##############################################################################
|
||||
# Validation
|
||||
##############################################################################
|
||||
|
||||
validate_environment() {
|
||||
print_header "Validating Environment"
|
||||
|
||||
# Check if running in PostgreSQL container
|
||||
if ! command -v psql &> /dev/null; then
|
||||
print_error "psql is not available. This script must run inside the PostgreSQL container."
|
||||
exit 1
|
||||
fi
|
||||
print_success "Running inside PostgreSQL container"
|
||||
|
||||
# Check if pg_dump is available
|
||||
if ! command -v pg_dump &> /dev/null; then
|
||||
print_error "pg_dump is not available in this container."
|
||||
exit 1
|
||||
fi
|
||||
print_success "pg_dump is available"
|
||||
|
||||
# Check if psql is available
|
||||
if ! command -v psql &> /dev/null; then
|
||||
print_error "psql is not available in this container."
|
||||
exit 1
|
||||
fi
|
||||
print_success "psql is available"
|
||||
|
||||
# Ensure /backups directory exists
|
||||
if [ ! -d "/backups" ]; then
|
||||
print_warning "/backups directory doesn't exist, creating it..."
|
||||
mkdir -p /backups
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "/backups directory created"
|
||||
else
|
||||
print_error "Failed to create /backups directory"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
print_success "/backups directory is ready"
|
||||
}
|
||||
|
||||
validate_source_db() {
|
||||
print_header "Validating Source Database"
|
||||
|
||||
print_info "Database: $SOURCE_DB"
|
||||
|
||||
if psql -lqt | cut -d \| -f 1 | grep -qw "$SOURCE_DB"; then
|
||||
print_success "Source database exists"
|
||||
else
|
||||
print_error "Source database '$SOURCE_DB' does not exist"
|
||||
echo ""
|
||||
echo "Available databases:"
|
||||
psql -lqt | cut -d \| -f 1 | grep -v '^ *$'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get database size
|
||||
local db_size=$(psql -t -c "SELECT pg_size_pretty(pg_database_size('$SOURCE_DB'))")
|
||||
print_info "Database size: $db_size"
|
||||
}
|
||||
|
||||
validate_backup_file() {
|
||||
print_header "Validating Backup File"
|
||||
|
||||
print_info "Backup file: $OUTPUT_FILE"
|
||||
|
||||
if [ ! -f "$OUTPUT_FILE" ]; then
|
||||
print_error "Backup file not found: $OUTPUT_FILE"
|
||||
echo ""
|
||||
echo "Available backup files in /backups:"
|
||||
ls -lh /backups/ 2>/dev/null || echo " (no backups found)"
|
||||
exit 1
|
||||
fi
|
||||
print_success "Backup file exists"
|
||||
|
||||
# Get file size
|
||||
local file_size=$(du -h "$OUTPUT_FILE" | cut -f1)
|
||||
print_info "File size: $file_size"
|
||||
|
||||
# Show line count
|
||||
local line_count=$(wc -l < "$OUTPUT_FILE")
|
||||
print_info "File lines: $line_count"
|
||||
}
|
||||
|
||||
validate_target_db() {
|
||||
print_header "Validating Target Database"
|
||||
|
||||
print_info "Database: $TARGET_DB"
|
||||
|
||||
if psql -lqt | cut -d \| -f 1 | grep -qw "$TARGET_DB"; then
|
||||
print_success "Target database exists"
|
||||
else
|
||||
print_error "Target database '$TARGET_DB' does not exist"
|
||||
echo ""
|
||||
echo "Available databases:"
|
||||
psql -lqt | cut -d \| -f 1 | grep -v '^ *$'
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
##############################################################################
|
||||
# Export Function
|
||||
##############################################################################
|
||||
|
||||
export_database() {
|
||||
print_header "Exporting Source Database"
|
||||
|
||||
print_info "Source database: $SOURCE_DB"
|
||||
print_info "Output file: $OUTPUT_FILE"
|
||||
print_info ""
|
||||
print_info "Exporting data (this may take a moment)..."
|
||||
|
||||
if pg_dump -d "$SOURCE_DB" > "$OUTPUT_FILE" 2>&1; then
|
||||
print_success "Database exported successfully"
|
||||
|
||||
# Get file size
|
||||
local file_size=$(du -h "$OUTPUT_FILE" | cut -f1)
|
||||
print_info "Export file size: $file_size"
|
||||
|
||||
# Show line count
|
||||
local line_count=$(wc -l < "$OUTPUT_FILE")
|
||||
print_info "Export file lines: $line_count"
|
||||
else
|
||||
print_error "Failed to export database"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
##############################################################################
|
||||
# Clear Database Function
|
||||
##############################################################################
|
||||
|
||||
clear_database() {
|
||||
print_header "Clearing Target Database"
|
||||
|
||||
print_info "Database: $TARGET_DB"
|
||||
print_info ""
|
||||
print_info "Dropping all tables, views, and sequences..."
|
||||
|
||||
# Drop all tables, views, and sequences
|
||||
if psql -d "$TARGET_DB" > /dev/null 2>&1 << EOF
|
||||
DROP SCHEMA public CASCADE;
|
||||
CREATE SCHEMA public;
|
||||
GRANT ALL ON SCHEMA public TO postgres;
|
||||
GRANT ALL ON SCHEMA public TO public;
|
||||
EOF
|
||||
then
|
||||
print_success "Database cleared successfully"
|
||||
else
|
||||
print_error "Failed to clear database"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
##############################################################################
|
||||
# Clean Backup File Function (Remove pg_dump restrictions)
|
||||
##############################################################################
|
||||
|
||||
clean_backup_file() {
|
||||
print_header "Preparing Backup File"
|
||||
|
||||
print_info "Backup file: $OUTPUT_FILE"
|
||||
print_info ""
|
||||
print_info "Removing pg_dump restrictions and comments..."
|
||||
|
||||
# Check if file has \restrict commands
|
||||
if grep -q "^\\\\restrict" "$OUTPUT_FILE"; then
|
||||
print_warning "Found pg_dump restrict commands - removing them..."
|
||||
|
||||
# Create temporary cleaned file
|
||||
local temp_file="${OUTPUT_FILE}.tmp"
|
||||
|
||||
# Remove \restrict and \unrestrict lines
|
||||
grep -v "^\\\\restrict\|^\\\\unrestrict" "$OUTPUT_FILE" > "$temp_file"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
mv "$temp_file" "$OUTPUT_FILE"
|
||||
print_success "Backup file cleaned successfully"
|
||||
else
|
||||
rm -f "$temp_file"
|
||||
print_error "Failed to clean backup file"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_success "Backup file is already clean"
|
||||
fi
|
||||
}
|
||||
|
||||
##############################################################################
|
||||
# Import Function
|
||||
##############################################################################
|
||||
|
||||
import_database() {
|
||||
print_header "Importing to Target Database"
|
||||
|
||||
if [ ! -f "$OUTPUT_FILE" ]; then
|
||||
print_error "Export file not found: $OUTPUT_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Target database: $TARGET_DB"
|
||||
print_info "Source file: $OUTPUT_FILE"
|
||||
print_info ""
|
||||
print_warning "WARNING: This will DELETE all existing data in the target database!"
|
||||
echo ""
|
||||
read -p "Are you sure you want to continue? (yes/no): " -r confirm
|
||||
echo ""
|
||||
|
||||
if [[ ! $confirm =~ ^[Yy][Ee][Ss]$ ]]; then
|
||||
print_warning "Import cancelled by user"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Clean backup file (remove restrict commands)
|
||||
clean_backup_file
|
||||
|
||||
# Clear existing data
|
||||
clear_database
|
||||
|
||||
print_info "Importing data (this may take a moment)..."
|
||||
|
||||
if psql -d "$TARGET_DB" -f "$OUTPUT_FILE" > /dev/null 2>&1; then
|
||||
print_success "Database imported successfully"
|
||||
else
|
||||
print_error "Failed to import database"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
##############################################################################
|
||||
# Verification Function
|
||||
##############################################################################
|
||||
|
||||
verify_import() {
|
||||
print_header "Verifying Data Import"
|
||||
|
||||
print_info "Checking table counts..."
|
||||
|
||||
# Get table counts from both databases
|
||||
local source_tables=$(psql -d "$SOURCE_DB" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';")
|
||||
local target_tables=$(psql -d "$TARGET_DB" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';")
|
||||
|
||||
print_info "Source tables: $source_tables"
|
||||
print_info "Target tables: $target_tables"
|
||||
|
||||
if [ "$source_tables" -eq "$target_tables" ]; then
|
||||
print_success "Table counts match"
|
||||
else
|
||||
print_warning "Table counts differ (this might be OK if some tables are empty)"
|
||||
fi
|
||||
|
||||
# Get row counts for main tables
|
||||
print_info ""
|
||||
print_info "Checking row counts..."
|
||||
|
||||
local tables=("users" "commanders" "games")
|
||||
for table in "${tables[@]}"; do
|
||||
if psql -d "$SOURCE_DB" -t -c "SELECT 1 FROM information_schema.tables WHERE table_name = '$table';" | grep -q 1; then
|
||||
|
||||
local source_rows=$(psql -d "$SOURCE_DB" -t -c "SELECT COUNT(*) FROM $table;")
|
||||
local target_rows=$(psql -d "$TARGET_DB" -t -c "SELECT COUNT(*) FROM $table;")
|
||||
|
||||
if [ "$source_rows" -eq "$target_rows" ]; then
|
||||
print_success "Table '$table': $target_rows rows (✓ matches)"
|
||||
else
|
||||
print_warning "Table '$table': source=$source_rows, target=$target_rows"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
##############################################################################
|
||||
# Summary Function
|
||||
##############################################################################
|
||||
|
||||
print_summary() {
|
||||
print_header "Migration Summary"
|
||||
|
||||
if [ "$SKIP_EXPORT" = true ]; then
|
||||
# Import-only mode summary
|
||||
echo "Mode: Import Only"
|
||||
echo ""
|
||||
echo "Source File:"
|
||||
echo " Container path: $OUTPUT_FILE"
|
||||
echo " Host path: ./backups/$(basename "$OUTPUT_FILE")"
|
||||
if [ -f "$OUTPUT_FILE" ]; then
|
||||
echo " File size: $(du -h "$OUTPUT_FILE" | cut -f1)"
|
||||
fi
|
||||
echo ""
|
||||
echo "Target Database:"
|
||||
echo " Name: $TARGET_DB"
|
||||
echo ""
|
||||
print_success "Data imported successfully!"
|
||||
else
|
||||
# Export (with or without import) summary
|
||||
echo "Source Database:"
|
||||
echo " Name: $SOURCE_DB"
|
||||
echo ""
|
||||
echo "Target Database:"
|
||||
echo " Name: $TARGET_DB"
|
||||
echo ""
|
||||
echo "Export File:"
|
||||
echo " Container path: $OUTPUT_FILE"
|
||||
echo " Host path: ./backups/$(basename "$OUTPUT_FILE")"
|
||||
if [ -f "$OUTPUT_FILE" ]; then
|
||||
echo " File size: $(du -h "$OUTPUT_FILE" | cut -f1)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if [ "$SKIP_IMPORT" = true ]; then
|
||||
print_info "Export completed. Import was skipped."
|
||||
echo ""
|
||||
print_success "Backup file is available on your host at:"
|
||||
echo " ./backups/$(basename "$OUTPUT_FILE")"
|
||||
echo ""
|
||||
echo "To import later, run:"
|
||||
echo " docker compose exec -u postgres postgres /scripts/migrate-database.sh \\"
|
||||
echo " --target-db $TARGET_DB \\"
|
||||
echo " --output-file $OUTPUT_FILE \\"
|
||||
echo " --skip-export"
|
||||
else
|
||||
print_success "Migration completed successfully!"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
##############################################################################
|
||||
# Main Execution
|
||||
##############################################################################
|
||||
|
||||
main() {
|
||||
print_header "EDH Stats Tracker - Database Migration (Container)"
|
||||
|
||||
# Check for conflicting flags
|
||||
if [ "$SKIP_IMPORT" = true ] && [ "$SKIP_EXPORT" = true ]; then
|
||||
print_error "Cannot use both --skip-import and --skip-export together"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate environment
|
||||
validate_environment
|
||||
|
||||
# Validate based on mode
|
||||
if [ "$SKIP_EXPORT" = true ]; then
|
||||
# Import-only mode: validate backup file and target database
|
||||
validate_backup_file
|
||||
validate_target_db
|
||||
else
|
||||
# Export mode (with or without import)
|
||||
validate_source_db
|
||||
|
||||
# Validate target database (if not skipping import)
|
||||
if [ "$SKIP_IMPORT" = false ]; then
|
||||
validate_target_db
|
||||
fi
|
||||
|
||||
# Export database
|
||||
export_database
|
||||
fi
|
||||
|
||||
# Import database (unless skipping export AND import)
|
||||
if [ "$SKIP_IMPORT" = false ]; then
|
||||
import_database
|
||||
verify_import
|
||||
fi
|
||||
|
||||
# Print summary
|
||||
print_summary
|
||||
|
||||
if [ "$SKIP_IMPORT" = false ] || [ "$SKIP_EXPORT" = true ]; then
|
||||
print_success "All done!"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
Reference in New Issue
Block a user