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:
2026-01-17 22:29:51 +01:00
parent e7d6c67c39
commit 4986c4f6b8
6 changed files with 1684 additions and 428 deletions

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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>

View File

@@ -1 +1 @@
1.1.2
2.0.2

504
scripts/migrate-database.sh Executable file
View 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