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')
|
# Generated: $(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||||
# GitHub User: ${GITHUB_USER}
|
# 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_NAME=edh_stats
|
||||||
# DB_USER=postgres
|
# DB_USER=postgres
|
||||||
# DB_PASSWORD=\$(openssl rand -base64 32)
|
# DB_PASSWORD=\$(openssl rand -base64 32)
|
||||||
@@ -266,10 +268,12 @@ generate_deployment_config() {
|
|||||||
# DB_SEED=false
|
# DB_SEED=false
|
||||||
#
|
#
|
||||||
# FIRST TIME SETUP:
|
# FIRST TIME SETUP:
|
||||||
# 1. Create .env file with above variables
|
# 1. Ensure Traefik is running and traefik-network exists
|
||||||
# 2. Run: docker-compose -f docker-compose.prod.deployed.yml up -d
|
# 2. Update frontend domain in labels (edh.example.com -> yourdomain.com)
|
||||||
# 3. Database migrations will run automatically via db-migrate service
|
# 3. Create .env file with above variables
|
||||||
# 4. Monitor logs: docker-compose logs -f db-migrate
|
# 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:
|
services:
|
||||||
# PostgreSQL database service
|
# PostgreSQL database service
|
||||||
@@ -280,7 +284,7 @@ services:
|
|||||||
- POSTGRES_PASSWORD=\${DB_PASSWORD}
|
- POSTGRES_PASSWORD=\${DB_PASSWORD}
|
||||||
- POSTGRES_DB=\${DB_NAME}
|
- POSTGRES_DB=\${DB_NAME}
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- ./postgres_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD-SHELL', 'PGPASSWORD=\${DB_PASSWORD} pg_isready -U postgres -h localhost']
|
test: ['CMD-SHELL', 'PGPASSWORD=\${DB_PASSWORD} pg_isready -U postgres -h localhost']
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -353,9 +357,6 @@ services:
|
|||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: ${FRONTEND_IMAGE}
|
image: ${FRONTEND_IMAGE}
|
||||||
ports:
|
|
||||||
- '38080:80'
|
|
||||||
- '30443:443'
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
@@ -367,8 +368,23 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
networks:
|
networks:
|
||||||
- edh-stats-network
|
- edh-stats-network
|
||||||
|
- traefik-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- 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:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -386,6 +402,10 @@ networks:
|
|||||||
edh-stats-network:
|
edh-stats-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
|
traefik-network:
|
||||||
|
external: true
|
||||||
|
name: traefik-network
|
||||||
|
|
||||||
x-dockge:
|
x-dockge:
|
||||||
urls:
|
urls:
|
||||||
- https://edh.zlor.fi
|
- https://edh.zlor.fi
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./postgres_data:/var/lib/postgresql/data
|
- ./postgres_data:/var/lib/postgresql/data
|
||||||
- ./backend/init-db:/docker-entrypoint-initdb.d
|
- ./backend/init-db:/docker-entrypoint-initdb.d
|
||||||
|
- ./scripts:/scripts:ro
|
||||||
|
- ./backups:/backups
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
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"
|
name="description"
|
||||||
content="Create a new account to track your Magic: The Gathering EDH/Commander games"
|
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" />
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body class="min-h-full flex flex-col py-12 px-4 sm:px-6 lg:px-8">
|
||||||
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="flex items-center justify-center flex-1">
|
||||||
<div class="max-w-md w-full space-y-8" x-data="registerForm()" x-init="init()">
|
<div
|
||||||
<!-- Header -->
|
class="max-w-md w-full space-y-8"
|
||||||
<div class="text-center">
|
x-data="registerForm()"
|
||||||
<h1 class="text-4xl font-bold font-mtg text-edh-primary mb-2">
|
x-init="init()"
|
||||||
EDH Stats
|
>
|
||||||
</h1>
|
<!-- Header -->
|
||||||
<h2 class="text-xl text-gray-600" x-text="allowRegistration ? 'Create your account' : 'Registration Disabled'"></h2>
|
<div class="text-center">
|
||||||
</div>
|
<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 -->
|
<!-- Registration Disabled Message -->
|
||||||
<div x-show="!allowRegistration" x-transition class="card bg-yellow-50 border-yellow-200">
|
<div
|
||||||
<div class="flex">
|
x-show="!allowRegistration"
|
||||||
<div class="flex-shrink-0">
|
x-transition
|
||||||
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
|
class="card bg-yellow-50 border-yellow-200"
|
||||||
<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 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>
|
||||||
<div class="ml-3">
|
<div class="mt-6 text-center">
|
||||||
<p class="text-sm text-yellow-800">User registration is currently disabled. Please contact an administrator if you need to create an account.</p>
|
<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>
|
</div>
|
||||||
<div class="mt-6 text-center">
|
|
||||||
<p class="text-sm text-gray-600">
|
<!-- Register Form -->
|
||||||
If you already have an account,
|
<div class="card" x-show="allowRegistration">
|
||||||
<a
|
<form class="space-y-6" @submit.prevent="handleRegister">
|
||||||
href="/login.html"
|
<!-- Username Field -->
|
||||||
class="font-medium text-edh-accent hover:text-edh-primary"
|
<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
|
<div class="flex">
|
||||||
</a>
|
<div class="flex-shrink-0">
|
||||||
</p>
|
<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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Register Form -->
|
<!-- Password Requirements Info -->
|
||||||
<div class="card" x-show="allowRegistration">
|
<div class="card bg-blue-50 border-blue-200" x-show="allowRegistration">
|
||||||
<form class="space-y-6" @submit.prevent="handleRegister">
|
<h3 class="text-sm font-medium text-blue-800 mb-2">
|
||||||
<!-- Username Field -->
|
Password Requirements
|
||||||
<div>
|
</h3>
|
||||||
<label for="username" class="form-label">Username</label>
|
<div class="text-xs text-blue-700 space-y-1">
|
||||||
<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
|
<p
|
||||||
x-show="errors.username"
|
class="flex items-center"
|
||||||
x-text="errors.username"
|
:class="{ 'text-green-700': formData.password.length >= 8 }"
|
||||||
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 }"
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
x-show="!loading"
|
:class="{ 'text-green-500': formData.password.length >= 8 }"
|
||||||
class="w-5 h-5"
|
class="w-4 h-4 mr-2"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -386,88 +475,83 @@
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
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>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
At least 8 characters
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="flex items-center"
|
||||||
|
:class="{ 'text-green-700': /(?=.*[a-z])/.test(formData.password) }"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
x-show="loading"
|
:class="{ 'text-green-500': /(?=.*[a-z])/.test(formData.password) }"
|
||||||
class="animate-spin h-5 w-5"
|
class="w-4 h-4 mr-2"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<circle
|
|
||||||
class="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="4"
|
|
||||||
></circle>
|
|
||||||
<path
|
<path
|
||||||
class="opacity-75"
|
stroke-linecap="round"
|
||||||
fill="currentColor"
|
stroke-linejoin="round"
|
||||||
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-width="2"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span
|
At least one lowercase letter
|
||||||
x-text="loading ? 'Creating account...' : 'Create account'"
|
</p>
|
||||||
></span>
|
<p
|
||||||
</button>
|
class="flex items-center"
|
||||||
</div>
|
:class="{ 'text-green-700': /(?=.*[A-Z])/.test(formData.password) }"
|
||||||
|
>
|
||||||
<!-- Login Link -->
|
<svg
|
||||||
<div class="text-center">
|
:class="{ 'text-green-500': /(?=.*[A-Z])/.test(formData.password) }"
|
||||||
<p class="text-sm text-gray-600">
|
class="w-4 h-4 mr-2"
|
||||||
Already have an account?
|
fill="none"
|
||||||
<a
|
stroke="currentColor"
|
||||||
href="/login.html"
|
viewBox="0 0 24 24"
|
||||||
class="font-medium text-edh-accent hover:text-edh-primary"
|
|
||||||
>
|
>
|
||||||
Sign in
|
<path
|
||||||
</a>
|
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>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scripts --> <script
|
<!-- Scripts -->
|
||||||
|
<script
|
||||||
defer
|
defer
|
||||||
src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"
|
src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"
|
||||||
></script>
|
></script>
|
||||||
<script src="/js/auth.js"></script>
|
<script src="/js/auth.js"></script>
|
||||||
|
|
||||||
|
|
||||||
<script src="/js/footer-loader.js"></script>
|
<script src="/js/footer-loader.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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