743 lines
30 KiB
Bash
Executable File
743 lines
30 KiB
Bash
Executable File
#!/bin/bash
|
||
#
|
||
# Universal White-Label Deployment Script
|
||
#
|
||
# This script is fully configurable via .env variables for any installation.
|
||
#
|
||
# Required .env variables for SERVER environment:
|
||
# DEPLOY_ENVIRONMENT=server - Set to "server" for remote deployments
|
||
# DEPLOY_SERVER_TYPE=dev|prod - "dev" or "prod" (required for server)
|
||
# DEPLOY_APP_DIR=/path/to/app - Full path to application directory
|
||
# APP_URL=https://yourdomain.org - Your application URL
|
||
# DEPLOY_WS_URL=wss://ws.yourdomain - Your WebSocket server URL
|
||
# DEPLOY_WEB_USER=www-data - Web server user (optional, defaults to www-data)
|
||
# DEPLOY_WEB_GROUP=www-data - Web server group (optional, defaults to www-data)
|
||
#
|
||
# For LOCAL environment (defaults):
|
||
# DEPLOY_ENVIRONMENT=local - Set to "local" or leave unset
|
||
# DEPLOY_WS_URL=ws://localhost:8080 - Local WebSocket URL (optional)
|
||
# APP_URL=http://localhost:8000 - Local app URL (optional)
|
||
#
|
||
# Command line options:
|
||
# -m Skip migrations
|
||
# -n Skip npm build
|
||
# -e Specify environment (server or local) - overrides DEPLOY_ENVIRONMENT
|
||
# -d Force development build (not recommended for server environments)
|
||
#
|
||
# Build behavior:
|
||
# - SERVER environments: Always uses 'npm run build' (production) for proper standalone assets
|
||
# - LOCAL environments: Uses 'npm run dev' (development) for faster builds
|
||
# - Use -d flag to force 'npm run dev' on servers (only for debugging, not recommended)
|
||
#
|
||
# Examples:
|
||
# ./deploy.sh (full deployment with local settings)
|
||
# ./deploy.sh -m -n (skip migrations and npm build)
|
||
# ./deploy.sh -e server (deploy to server using .env configuration with production build)
|
||
#
|
||
|
||
# Colors for output
|
||
GREEN='\033[0;32m'
|
||
BLUE='\033[0;34m'
|
||
YELLOW='\033[0;33m'
|
||
RED='\033[0;31m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Function for printing section headers
|
||
section_header() {
|
||
echo -e "\n${BLUE}===========================================================${NC}"
|
||
echo -e "${BLUE} $1${NC}"
|
||
echo -e "${BLUE}===========================================================${NC}\n"
|
||
}
|
||
|
||
# Error handling
|
||
error_exit() {
|
||
echo -e "${RED}ERROR: $1${NC}" >&2
|
||
exit 1
|
||
}
|
||
|
||
# Function to prompt for MySQL deployment credentials if not in .env
|
||
prompt_mysql_deploy_credentials() {
|
||
if [ -z "$DB_DEPLOY_USERNAME" ]; then
|
||
echo -e "${YELLOW}MySQL user with GRANT privileges needed to manage ALTER permission${NC}"
|
||
echo -e "${YELLOW}This can be root or a dedicated deployment user${NC}"
|
||
read -p "MySQL username [root]: " DB_DEPLOY_USERNAME
|
||
DB_DEPLOY_USERNAME="${DB_DEPLOY_USERNAME:-root}"
|
||
fi
|
||
|
||
if [ -z "$DB_DEPLOY_PASSWORD" ]; then
|
||
read -sp "MySQL password: " DB_DEPLOY_PASSWORD
|
||
echo ""
|
||
fi
|
||
}
|
||
|
||
# Function to execute MySQL commands with deployment user credentials
|
||
mysql_deploy_exec() {
|
||
local sql="$1"
|
||
|
||
# Ensure we have deployment credentials
|
||
if [ -z "$DB_DEPLOY_USERNAME" ]; then
|
||
return 1
|
||
fi
|
||
|
||
# Execute with credentials
|
||
if [ -n "$DB_DEPLOY_PASSWORD" ]; then
|
||
mysql -u "$DB_DEPLOY_USERNAME" -p"$DB_DEPLOY_PASSWORD" -e "$sql" 2>/dev/null
|
||
else
|
||
mysql -u "$DB_DEPLOY_USERNAME" -e "$sql" 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
# Function to grant ALTER permission to application database user
|
||
grant_alter_permission() {
|
||
local db_user=$(php artisan tinker --execute="echo config('database.connections.mysql.username');" 2>/dev/null | grep -v ">>>" | grep -v "Psy" | tr -d '\n' | xargs)
|
||
local db_name=$(php artisan tinker --execute="echo config('database.connections.mysql.database');" 2>/dev/null | grep -v ">>>" | grep -v "Psy" | tr -d '\n' | xargs)
|
||
local db_host=$(php artisan tinker --execute="echo config('database.connections.mysql.host');" 2>/dev/null | grep -v ">>>" | grep -v "Psy" | tr -d '\n' | xargs)
|
||
|
||
if [ -z "$db_user" ] || [ -z "$db_name" ]; then
|
||
echo -e "${RED}✗ Could not determine database user or name from Laravel config${NC}"
|
||
return 1
|
||
fi
|
||
|
||
echo -e "${BLUE}Database: $db_name${NC}"
|
||
echo -e "${BLUE}User: $db_user${NC}"
|
||
echo -e "${BLUE}Connection Host: $db_host${NC}"
|
||
echo ""
|
||
|
||
# Prompt for MySQL deployment credentials if not already set
|
||
prompt_mysql_deploy_credentials
|
||
|
||
# Test connection first
|
||
if ! mysql_deploy_exec "SELECT 1;" >/dev/null 2>&1; then
|
||
echo -e "${RED}✗ MySQL connection failed. Check credentials.${NC}"
|
||
return 1
|
||
fi
|
||
|
||
# Determine which host(s) to grant to based on Laravel config
|
||
local grant_hosts=()
|
||
if [ "$db_host" = "127.0.0.1" ]; then
|
||
grant_hosts+=("127.0.0.1")
|
||
echo -e "${BLUE}Granting ALTER to 'timebank_cc_dev'@'127.0.0.1' (Laravel connects via IP)${NC}"
|
||
elif [ "$db_host" = "localhost" ]; then
|
||
grant_hosts+=("localhost")
|
||
echo -e "${BLUE}Granting ALTER to 'timebank_cc_dev'@'localhost' (Laravel connects via socket)${NC}"
|
||
else
|
||
# If it's something else, grant to both to be safe
|
||
grant_hosts+=("localhost" "127.0.0.1")
|
||
echo -e "${BLUE}Granting ALTER to both localhost and 127.0.0.1 (host: $db_host)${NC}"
|
||
fi
|
||
|
||
# Grant ALTER, CREATE, DROP to the appropriate host(s) (all needed for migrations)
|
||
for host in "${grant_hosts[@]}"; do
|
||
if ! mysql_deploy_exec "GRANT ALTER, CREATE, DROP ON \`$db_name\`.* TO '$db_user'@'$host';"; then
|
||
echo -e "${YELLOW}⚠ Warning: Could not grant to $host (user may not exist)${NC}"
|
||
fi
|
||
done
|
||
|
||
# Flush privileges
|
||
mysql_deploy_exec "FLUSH PRIVILEGES;"
|
||
|
||
# Verify the grant actually worked by checking the host Laravel uses
|
||
echo -e "${BLUE}Verifying ALTER permission was granted...${NC}"
|
||
local verify_host="${grant_hosts[0]}"
|
||
local has_alter=$(mysql_deploy_exec "SHOW GRANTS FOR '$db_user'@'$verify_host';" 2>/dev/null | grep -i "ALTER" | wc -l)
|
||
|
||
if [ "$has_alter" -gt 0 ]; then
|
||
echo -e "${GREEN}✓ ALTER permission verified for $db_user@$verify_host on $db_name${NC}"
|
||
return 0
|
||
else
|
||
echo -e "${RED}✗ ALTER permission was NOT granted (verification failed)${NC}"
|
||
echo -e "${YELLOW}Current grants for $db_user@$verify_host:${NC}"
|
||
mysql_deploy_exec "SHOW GRANTS FOR '$db_user'@'$verify_host';" 2>/dev/null || echo "Could not retrieve grants"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Function to grant UPDATE/DELETE on any tables missing those permissions (e.g. tables added after restricted user was created)
|
||
grant_missing_table_permissions() {
|
||
local db_user=$(php artisan tinker --execute="echo config('database.connections.mysql.username');" 2>/dev/null | grep -v ">>>" | grep -v "Psy" | tr -d '\n' | xargs)
|
||
local db_name=$(php artisan tinker --execute="echo config('database.connections.mysql.database');" 2>/dev/null | grep -v ">>>" | grep -v "Psy" | tr -d '\n' | xargs)
|
||
local db_host=$(php artisan tinker --execute="echo config('database.connections.mysql.host');" 2>/dev/null | grep -v ">>>" | grep -v "Psy" | tr -d '\n' | xargs)
|
||
|
||
if [ -z "$db_user" ] || [ -z "$db_name" ]; then
|
||
echo -e "${YELLOW}Warning: Could not determine database user or name. Skipping table permission sync.${NC}"
|
||
return 1
|
||
fi
|
||
|
||
# Immutable tables that should never get UPDATE/DELETE
|
||
local immutable_tables="transactions transaction_types"
|
||
|
||
# Determine host(s) to grant to
|
||
local grant_hosts=()
|
||
if [ "$db_host" = "127.0.0.1" ]; then
|
||
grant_hosts+=("127.0.0.1")
|
||
elif [ "$db_host" = "localhost" ]; then
|
||
grant_hosts+=("localhost")
|
||
else
|
||
grant_hosts+=("localhost" "127.0.0.1")
|
||
fi
|
||
|
||
echo -e "${BLUE}Checking for tables missing UPDATE/DELETE permissions for $db_user...${NC}"
|
||
|
||
# Get all tables in the database
|
||
local all_tables
|
||
if [ -n "$DB_DEPLOY_PASSWORD" ]; then
|
||
all_tables=$(mysql -u "$DB_DEPLOY_USERNAME" -p"$DB_DEPLOY_PASSWORD" -N -e "SHOW TABLES FROM \`$db_name\`;" 2>/dev/null)
|
||
else
|
||
all_tables=$(mysql -u "$DB_DEPLOY_USERNAME" -N -e "SHOW TABLES FROM \`$db_name\`;" 2>/dev/null)
|
||
fi
|
||
|
||
if [ -z "$all_tables" ]; then
|
||
echo -e "${YELLOW}Warning: Could not retrieve table list. Skipping table permission sync.${NC}"
|
||
return 1
|
||
fi
|
||
|
||
local granted_count=0
|
||
for table in $all_tables; do
|
||
# Skip immutable tables
|
||
if [[ " $immutable_tables " =~ " $table " ]]; then
|
||
continue
|
||
fi
|
||
|
||
# Grant UPDATE, DELETE for this table to all relevant hosts
|
||
for host in "${grant_hosts[@]}"; do
|
||
mysql_deploy_exec "GRANT UPDATE, DELETE ON \`$db_name\`.\`$table\` TO '$db_user'@'$host';" 2>/dev/null
|
||
done
|
||
granted_count=$((granted_count + 1))
|
||
done
|
||
|
||
mysql_deploy_exec "FLUSH PRIVILEGES;"
|
||
echo -e "${GREEN}✓ UPDATE/DELETE permissions synced for $granted_count tables (excluding immutable tables)${NC}"
|
||
}
|
||
|
||
# Function to revoke ALTER permission from application database user
|
||
revoke_alter_permission() {
|
||
echo -e "${BLUE}Revoking ALTER permission (principle of least privilege)...${NC}"
|
||
|
||
local db_user=$(php artisan tinker --execute="echo config('database.connections.mysql.username');" 2>/dev/null | grep -v ">>>" | grep -v "Psy" | tr -d '\n' | xargs)
|
||
local db_name=$(php artisan tinker --execute="echo config('database.connections.mysql.database');" 2>/dev/null | grep -v ">>>" | grep -v "Psy" | tr -d '\n' | xargs)
|
||
local db_host=$(php artisan tinker --execute="echo config('database.connections.mysql.host');" 2>/dev/null | grep -v ">>>" | grep -v "Psy" | tr -d '\n' | xargs)
|
||
|
||
if [ -z "$db_user" ] || [ -z "$db_name" ]; then
|
||
echo -e "${YELLOW}Warning: Could not determine database user or name. Skipping permission revoke.${NC}"
|
||
return 1
|
||
fi
|
||
|
||
# Determine which host(s) to revoke from based on Laravel config
|
||
local revoke_hosts=()
|
||
if [ "$db_host" = "127.0.0.1" ]; then
|
||
revoke_hosts+=("127.0.0.1")
|
||
elif [ "$db_host" = "localhost" ]; then
|
||
revoke_hosts+=("localhost")
|
||
else
|
||
revoke_hosts+=("localhost" "127.0.0.1")
|
||
fi
|
||
|
||
# Revoke ALTER from the appropriate host(s)
|
||
local revoked=0
|
||
for host in "${revoke_hosts[@]}"; do
|
||
if mysql_deploy_exec "REVOKE ALTER ON \`$db_name\`.* FROM '$db_user'@'$host';"; then
|
||
echo -e "${GREEN}✓ ALTER permission revoked from $db_user@$host${NC}"
|
||
revoked=1
|
||
fi
|
||
done
|
||
|
||
mysql_deploy_exec "FLUSH PRIVILEGES;"
|
||
|
||
if [ $revoked -eq 1 ]; then
|
||
return 0
|
||
else
|
||
echo -e "${YELLOW}Warning: Could not revoke ALTER permission. This is not critical but reduces security.${NC}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Load .env variables
|
||
set -a
|
||
source .env 2>/dev/null
|
||
set +a
|
||
|
||
# Parse command line arguments
|
||
skip_migrations=false
|
||
skip_npm=false
|
||
environment=""
|
||
dev_build=false
|
||
|
||
while getopts "mne:d" opt; do
|
||
case $opt in
|
||
m) skip_migrations=true ;;
|
||
n) skip_npm=true ;;
|
||
e) environment="$OPTARG" ;;
|
||
d) dev_build=true ;;
|
||
*) ;;
|
||
esac
|
||
done
|
||
|
||
# Determine environment from -e flag or DEPLOY_ENVIRONMENT
|
||
if [ -z "$environment" ]; then
|
||
# Check DEPLOY_ENVIRONMENT from .env
|
||
if [ -n "$DEPLOY_ENVIRONMENT" ]; then
|
||
environment="$DEPLOY_ENVIRONMENT"
|
||
else
|
||
# Default to local if not specified
|
||
environment="local"
|
||
fi
|
||
fi
|
||
|
||
# Configure environment-specific settings
|
||
if [ "$environment" = "server" ]; then
|
||
# Require .env variables for server environment
|
||
if [ -z "$DEPLOY_APP_DIR" ]; then
|
||
error_exit "DEPLOY_APP_DIR must be set in .env for server environment"
|
||
fi
|
||
|
||
APP_DIR="$DEPLOY_APP_DIR"
|
||
WEB_USER="${DEPLOY_WEB_USER:-www-data}"
|
||
WEB_GROUP="${DEPLOY_WEB_GROUP:-www-data}"
|
||
|
||
# Determine server type from .env (required for server environment)
|
||
if [ -n "$DEPLOY_SERVER_TYPE" ]; then
|
||
SERVER_TYPE="${DEPLOY_SERVER_TYPE^^}" # Convert to uppercase
|
||
else
|
||
error_exit "DEPLOY_SERVER_TYPE must be set in .env (dev or prod)"
|
||
fi
|
||
|
||
# APP_URL is required for server environment
|
||
if [ -z "$APP_URL" ]; then
|
||
error_exit "APP_URL must be set in .env for server environment"
|
||
fi
|
||
|
||
# DEPLOY_WS_URL is required for server environment
|
||
if [ -z "$DEPLOY_WS_URL" ]; then
|
||
error_exit "DEPLOY_WS_URL must be set in .env for server environment"
|
||
fi
|
||
WS_URL="$DEPLOY_WS_URL"
|
||
|
||
# Determine build mode - server environments always use production builds
|
||
# (use -d flag to override and force development build if needed)
|
||
if [ "$dev_build" = true ]; then
|
||
BUILD_MODE="dev"
|
||
ENV_NAME="${SERVER_TYPE}-SERVER (Development Build - forced with -d flag)"
|
||
else
|
||
BUILD_MODE="build"
|
||
ENV_NAME="${SERVER_TYPE}-SERVER (Production Build)"
|
||
fi
|
||
else
|
||
# Local environment settings
|
||
APP_DIR="${DEPLOY_APP_DIR:-$(pwd)}"
|
||
WEB_USER="${DEPLOY_WEB_USER:-$(whoami)}"
|
||
WEB_GROUP="${DEPLOY_WEB_GROUP:-$(whoami)}"
|
||
|
||
# Use APP_URL from .env if set, otherwise default to localhost
|
||
APP_URL="${APP_URL:-http://localhost:8000}"
|
||
|
||
# Use DEPLOY_WS_URL from .env if set, otherwise default to localhost WebSocket
|
||
WS_URL="${DEPLOY_WS_URL:-ws://localhost:8080}"
|
||
|
||
BUILD_MODE="dev"
|
||
ENV_NAME="LOCAL"
|
||
fi
|
||
|
||
# Start deployment
|
||
section_header "DEPLOYING APPLICATION ($ENV_NAME ENVIRONMENT)"
|
||
|
||
# Change to application directory
|
||
cd "$APP_DIR" || error_exit "Failed to change to project directory ($APP_DIR)"
|
||
|
||
# Check for uncommitted changes
|
||
if [[ -n $(git status -s) ]]; then
|
||
echo -e "${YELLOW}Warning: You have uncommitted changes${NC}"
|
||
echo "Continue anyway? (y/n)"
|
||
read -r response
|
||
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
||
echo "Deployment cancelled."
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
# Copy example config files if actual files don't exist
|
||
section_header "CHECKING CONFIGURATION FILES"
|
||
config_files_to_check=(
|
||
"config/themes.php"
|
||
"config/timebank-default.php"
|
||
"config/timebank_cc.php"
|
||
)
|
||
|
||
for config_file in "${config_files_to_check[@]}"; do
|
||
if [ ! -f "$config_file" ] && [ -f "${config_file}.example" ]; then
|
||
echo -e "${YELLOW}Creating $config_file from example template...${NC}"
|
||
cp "${config_file}.example" "$config_file"
|
||
echo -e "${GREEN}Created $config_file${NC}"
|
||
elif [ -f "$config_file" ]; then
|
||
echo -e "${GREEN}$config_file exists (custom installation config)${NC}"
|
||
else
|
||
echo -e "${YELLOW}Warning: Neither $config_file nor ${config_file}.example found${NC}"
|
||
fi
|
||
done
|
||
|
||
# Pull latest code
|
||
section_header "PULLING LATEST CODE"
|
||
# Attempt to pull and capture stderr to check for specific errors
|
||
pull_output=$(git pull origin main 2>&1)
|
||
pull_exit_code=$?
|
||
|
||
if [ $pull_exit_code -ne 0 ]; then
|
||
# Check if the error is due to local changes or untracked files that would be overwritten
|
||
if echo "$pull_output" | grep -q -E "overwritten by merge|untracked working tree files"; then
|
||
echo -e "${YELLOW}Git pull failed due to local file conflicts:${NC}"
|
||
echo "$pull_output" # Show the conflicting files
|
||
echo -e "${YELLOW}What would you like to do?${NC}"
|
||
echo " 1) Stash local changes and continue"
|
||
echo " 2) Discard local changes and remove untracked files to continue"
|
||
echo " 3) Cancel deployment"
|
||
read -p "Enter your choice (1-3): " choice
|
||
|
||
case $choice in
|
||
1)
|
||
echo "Stashing local changes (including untracked files)..."
|
||
git stash --include-untracked || error_exit "Failed to stash changes."
|
||
echo "Retrying git pull..."
|
||
git pull origin main || error_exit "Git pull failed after stashing."
|
||
;;
|
||
2)
|
||
echo "Discarding local changes and removing untracked files..."
|
||
git reset --hard HEAD || error_exit "Failed to discard changes."
|
||
git clean -fd || error_exit "Failed to remove untracked files."
|
||
echo "Retrying git pull..."
|
||
git pull origin main || error_exit "Git pull failed after cleaning."
|
||
;;
|
||
3)
|
||
echo "Deployment cancelled."
|
||
exit 0
|
||
;;
|
||
esac
|
||
else
|
||
# It's a different git error, so fail as before
|
||
echo "$pull_output"
|
||
error_exit "Git pull failed with an unexpected error."
|
||
fi
|
||
else
|
||
# Git pull was successful on the first try
|
||
echo "$pull_output"
|
||
fi
|
||
|
||
# Check for configuration updates (after pulling latest .example files)
|
||
section_header "CHECKING FOR CONFIGURATION UPDATES"
|
||
if php artisan config:merge --all --dry-run 2>/dev/null | grep -q "Found [0-9]"; then
|
||
echo -e "${YELLOW}New configuration keys available in .example files${NC}"
|
||
echo -e "${BLUE}Review changes with: php artisan config:merge --all --dry-run${NC}"
|
||
echo -e "${BLUE}Documentation: references/CONFIG_MANAGEMENT.md${NC}"
|
||
echo ""
|
||
read -p "Would you like to merge new keys now? (y/N) " -n 1 -r
|
||
echo
|
||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||
php artisan config:merge --all || echo -e "${YELLOW}Config merge skipped or failed${NC}"
|
||
else
|
||
echo -e "${YELLOW}Skipping config merge - you can run manually later with: php artisan config:merge --all${NC}"
|
||
echo -e "${BLUE}See references/CONFIG_MANAGEMENT.md for details on manual merge process${NC}"
|
||
fi
|
||
else
|
||
echo -e "${GREEN}Configuration is up to date${NC}"
|
||
fi
|
||
|
||
# Install dependencies
|
||
section_header "INSTALLING PHP DEPENDENCIES"
|
||
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-interaction --prefer-dist --optimize-autoloader || error_exit "Composer install failed"
|
||
|
||
# Clear caches
|
||
section_header "CLEARING CACHES"
|
||
php artisan optimize:clear || echo -e "${YELLOW}Warning: Failed to clear some caches${NC}"
|
||
|
||
# Restart queue workers
|
||
section_header "RESTARTING QUEUE WORKERS"
|
||
|
||
if [ "$environment" = "server" ]; then
|
||
# Server environment - restart systemd services
|
||
echo -e "${BLUE}Restarting systemd queue worker services...${NC}"
|
||
|
||
# Restart high priority worker
|
||
if sudo systemctl is-active --quiet timebank-high-priority-worker; then
|
||
echo -e "${BLUE}Restarting high priority worker...${NC}"
|
||
sudo systemctl restart timebank-high-priority-worker || echo -e "${YELLOW}Warning: Failed to restart high priority worker${NC}"
|
||
fi
|
||
|
||
# Restart main queue worker
|
||
if sudo systemctl is-active --quiet timebank-queue-worker; then
|
||
echo -e "${BLUE}Restarting main queue worker...${NC}"
|
||
sudo systemctl restart timebank-queue-worker || echo -e "${YELLOW}Warning: Failed to restart main queue worker${NC}"
|
||
fi
|
||
|
||
# Restart mailing workers (1-3)
|
||
for i in 1 2 3; do
|
||
if sudo systemctl is-active --quiet timebank-mailing-worker-$i; then
|
||
echo -e "${BLUE}Restarting mailing worker $i...${NC}"
|
||
sudo systemctl restart timebank-mailing-worker-$i || echo -e "${YELLOW}Warning: Failed to restart mailing worker $i${NC}"
|
||
fi
|
||
done
|
||
|
||
echo -e "${GREEN}All active queue workers have been restarted${NC}"
|
||
else
|
||
# Local environment - use queue:restart command
|
||
php artisan queue:restart || echo -e "${YELLOW}Warning: Failed to restart queue workers. If you have queue workers running, restart them manually.${NC}"
|
||
fi
|
||
|
||
# Link storage
|
||
section_header "LINKING STORAGE"
|
||
php artisan storage:link || echo -e "${YELLOW}Warning: Failed to link storage. This is normal if the link already exists.${NC}"
|
||
|
||
# Run migrations
|
||
if [ "$skip_migrations" = false ]; then
|
||
section_header "RUNNING MIGRATIONS"
|
||
|
||
# Grant ALTER permission temporarily for migrations
|
||
echo -e "${YELLOW}Note: ALTER permission is needed to modify database schema during migrations${NC}"
|
||
|
||
# Try to grant ALTER permission (with retry on failure)
|
||
GRANTED_ALTER=1
|
||
MAX_ATTEMPTS=3
|
||
ATTEMPT=1
|
||
|
||
while [ $GRANTED_ALTER -ne 0 ] && [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
|
||
if [ $ATTEMPT -gt 1 ]; then
|
||
echo -e "${YELLOW}Retry attempt $ATTEMPT of $MAX_ATTEMPTS${NC}"
|
||
fi
|
||
|
||
grant_alter_permission
|
||
GRANTED_ALTER=$?
|
||
|
||
if [ $GRANTED_ALTER -ne 0 ] && [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then
|
||
echo -e "${RED}Failed to grant ALTER permission${NC}"
|
||
read -p "Press Enter to retry, or Ctrl+C to abort deployment... "
|
||
fi
|
||
|
||
ATTEMPT=$((ATTEMPT + 1))
|
||
done
|
||
|
||
# Check if we successfully granted ALTER permission
|
||
if [ $GRANTED_ALTER -ne 0 ]; then
|
||
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${RED}DEPLOYMENT FAILED: Could not grant ALTER permission${NC}"
|
||
echo -e "${RED}Database migrations require ALTER permission to modify schema${NC}"
|
||
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo ""
|
||
echo -e "${YELLOW}TROUBLESHOOTING:${NC}"
|
||
echo -e "1. Verify MySQL credentials have GRANT privileges on the database"
|
||
echo -e "2. Add credentials to .env to avoid prompts:"
|
||
echo -e " ${BLUE}DB_DEPLOY_USERNAME=root${NC}"
|
||
echo -e " ${BLUE}DB_DEPLOY_PASSWORD=your_password${NC}"
|
||
echo -e "3. Or manually grant permission:"
|
||
echo -e " ${BLUE}mysql -u root -p -e \"GRANT ALTER, CREATE, DROP ON \\\`timebank_cc\\\`.* TO 'timebank_cc_dev'@'localhost';\"${NC}"
|
||
echo ""
|
||
exit 1
|
||
fi
|
||
|
||
echo -e "${GREEN}✓ ALTER permission granted - proceeding with migrations${NC}"
|
||
|
||
# Clear Laravel's database connection cache to ensure new permissions are used
|
||
echo -e "${BLUE}Clearing Laravel caches to apply new permissions...${NC}"
|
||
php artisan config:clear >/dev/null 2>&1
|
||
php artisan cache:clear >/dev/null 2>&1
|
||
echo ""
|
||
|
||
# Create database backup before migrations using the new backup system
|
||
echo -e "${YELLOW}Creating pre-deployment database backup...${NC}"
|
||
if [ -x "scripts/backup-database.sh" ]; then
|
||
# Use the new backup system with pre-restore type
|
||
# Note: Cleanup is handled automatically by scripts/cleanup-backups.sh
|
||
# which runs based on backup-retention.conf settings
|
||
if ./scripts/backup-database.sh pre-restore; then
|
||
echo -e "${GREEN}Database backup created successfully${NC}"
|
||
echo -e "${BLUE}(Retention managed by backup-retention.conf: keeps ${PRE_RESTORE_COUNT_LIMIT:-5} backups or ${PRE_RESTORE_RETENTION:-14} days)${NC}"
|
||
else
|
||
echo -e "${YELLOW}Warning: Database backup failed${NC}"
|
||
fi
|
||
else
|
||
# Fallback to old backup method if new script doesn't exist
|
||
DB_CONNECTION=$(php artisan tinker --execute="echo config('database.default');" | grep -v ">>>" | grep -v "Psy")
|
||
DB_DATABASE=$(php artisan tinker --execute="echo config('database.connections.$DB_CONNECTION.database');" | grep -v ">>>" | grep -v "Psy")
|
||
DB_USERNAME=$(php artisan tinker --execute="echo config('database.connections.$DB_CONNECTION.username');" | grep -v ">>>" | grep -v "Psy")
|
||
DB_PASSWORD=$(php artisan tinker --execute="echo config('database.connections.$DB_CONNECTION.password');" | grep -v ">>>" | grep -v "Psy")
|
||
|
||
mkdir -p storage/backups
|
||
BACKUP_FILE="storage/backups/db-backup-$(date +'%Y-%m-%d-%H%M%S').sql"
|
||
|
||
if mysqldump -u "$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" > "$BACKUP_FILE" 2>/dev/null; then
|
||
echo -e "${GREEN}Database backup created at $BACKUP_FILE${NC}"
|
||
|
||
# Clean up old deploy backups in legacy location (keep only last 3)
|
||
echo -e "${YELLOW}Cleaning up old legacy deploy backups (keeping last 3)...${NC}"
|
||
ls -t storage/backups/db-backup-*.sql 2>/dev/null | tail -n +4 | xargs rm -f 2>/dev/null || true
|
||
echo -e "${GREEN}Legacy backups cleaned up${NC}"
|
||
else
|
||
echo -e "${YELLOW}Warning: Database backup failed${NC}"
|
||
fi
|
||
fi
|
||
|
||
# Elasticsearch re-indexing
|
||
section_header "RE-INDEXING ELASTICSEARCH USING re-index-search.sh"
|
||
|
||
# Call the re-index-search script
|
||
bash scripts/re-index-search.sh || error_exit "Re-indexing failed"
|
||
|
||
# Migrations header with safety information
|
||
section_header "MIGRATIONS"
|
||
echo -e "${GREEN}ℹ Database migrations will modify the schema but will NOT remove existing data${NC}"
|
||
echo -e "${GREEN}ℹ A backup was created above and can be restored if needed${NC}"
|
||
echo ""
|
||
echo -e "${BLUE}To restore from backup if needed:${NC}"
|
||
echo -e " 1. Find backup: ${YELLOW}ls -lt storage/backups/ | head -5${NC}"
|
||
echo -e " 2. Restore: ${YELLOW}mysql -u [user] -p [database] < storage/backups/[backup-file].sql${NC}"
|
||
echo -e " 3. Or use: ${YELLOW}./scripts/restore-database.sh [backup-file]${NC} (if script exists)"
|
||
echo ""
|
||
|
||
# Run migrations with deploy credentials (CREATE TABLE requires privileges the app user may lack)
|
||
echo -e "${BLUE}Running: php artisan migrate${NC}"
|
||
echo -e "${YELLOW}This will execute any pending database migrations${NC}"
|
||
DB_USERNAME="$DB_DEPLOY_USERNAME" DB_PASSWORD="$DB_DEPLOY_PASSWORD" php artisan migrate || error_exit "Migrations failed"
|
||
|
||
# Run database updates (for schema changes and data migrations)
|
||
section_header "RUNNING DATABASE UPDATES"
|
||
echo -e "${BLUE}Running: php artisan database:update${NC}"
|
||
echo -e "${YELLOW}This will execute schema changes and data migrations${NC}"
|
||
DB_USERNAME="$DB_DEPLOY_USERNAME" DB_PASSWORD="$DB_DEPLOY_PASSWORD" php artisan database:update || error_exit "Database updates failed"
|
||
|
||
# Run category seeders
|
||
section_header "RUNNING CATEGORY SEEDERS"
|
||
echo -e "${BLUE}Running: php artisan db:seed --class=ExtendCategoriesSeeder${NC}"
|
||
echo -e "${YELLOW}This will seed/update category data${NC}"
|
||
php artisan db:seed --class=ExtendCategoriesSeeder || error_exit "ExtendCategoriesSeeder failed"
|
||
|
||
echo -e "${BLUE}Running: php artisan db:seed --class=ExtendCategoryTranslationsSeeder${NC}"
|
||
echo -e "${YELLOW}This will seed/update category translations${NC}"
|
||
php artisan db:seed --class=ExtendCategoryTranslationsSeeder || error_exit "ExtendCategoryTranslationsSeeder failed"
|
||
|
||
# Grant UPDATE/DELETE on any tables added since restricted user was created
|
||
section_header "SYNCING TABLE PERMISSIONS"
|
||
grant_missing_table_permissions
|
||
|
||
# Revoke ALTER permission after migrations complete
|
||
if [ $GRANTED_ALTER -eq 0 ]; then
|
||
revoke_alter_permission
|
||
fi
|
||
else
|
||
echo -e "${YELLOW}Skipping migrations (--skip-migrations flag provided)${NC}"
|
||
fi
|
||
|
||
# Verify transaction immutability (critical security check)
|
||
section_header "VERIFYING TRANSACTION IMMUTABILITY"
|
||
|
||
if [ -x "scripts/test-transaction-immutability.sh" ]; then
|
||
echo -e "${BLUE}Running transaction immutability security test...${NC}"
|
||
|
||
if ./scripts/test-transaction-immutability.sh; then
|
||
echo -e "${GREEN}✓ Transaction immutability verified - financial records are protected${NC}"
|
||
else
|
||
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${RED}DEPLOYMENT FAILED: Transaction immutability test failed${NC}"
|
||
echo -e "${RED}Financial transaction records are NOT properly protected!${NC}"
|
||
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo ""
|
||
echo -e "${YELLOW}CRITICAL SECURITY ISSUE:${NC}"
|
||
echo -e "Database user has permission to UPDATE or DELETE transaction records."
|
||
echo -e "This violates financial audit requirements for immutable transaction logs."
|
||
echo ""
|
||
echo -e "${YELLOW}RECOMMENDED ACTIONS:${NC}"
|
||
echo -e "${YELLOW}1.${NC} Run: ${BLUE}./scripts/create-restricted-db-user-safe.sh${NC}"
|
||
echo -e "${YELLOW}2.${NC} Update ${BLUE}.env${NC} with restricted database user credentials"
|
||
echo -e "${YELLOW}3.${NC} Clear config cache: ${BLUE}php artisan config:clear${NC}"
|
||
echo -e "${YELLOW}4.${NC} Re-run deployment: ${BLUE}./deploy.sh${NC}"
|
||
echo ""
|
||
echo -e "${YELLOW}For detailed instructions, see:${NC}"
|
||
echo -e " ${BLUE}references/TRANSACTION_IMMUTABILITY_FIX.md${NC}"
|
||
echo ""
|
||
exit 1
|
||
fi
|
||
elif [ -f "scripts/test-transaction-immutability.sh" ]; then
|
||
echo -e "${YELLOW}⚠ Warning: Transaction immutability test script exists but is not executable${NC}"
|
||
echo -e "${YELLOW} Run: chmod +x scripts/test-transaction-immutability.sh${NC}"
|
||
echo -e "${YELLOW} Skipping immutability verification${NC}"
|
||
else
|
||
echo -e "${YELLOW}⚠ Warning: Transaction immutability test script not found${NC}"
|
||
echo -e "${YELLOW} Expected location: scripts/test-transaction-immutability.sh${NC}"
|
||
echo -e "${YELLOW} Skipping immutability verification${NC}"
|
||
fi
|
||
|
||
# Build assets
|
||
if [ "$skip_npm" = false ]; then
|
||
section_header "BUILDING ASSETS (${BUILD_MODE} mode)"
|
||
npm install --no-audit || error_exit "NPM install failed"
|
||
|
||
# Use build mode determined earlier
|
||
npm run $BUILD_MODE || error_exit "NPM run $BUILD_MODE failed"
|
||
else
|
||
echo -e "${YELLOW}Skipping NPM build (--skip-npm flag provided)${NC}"
|
||
fi
|
||
|
||
# Set permissions (with environment-specific handling)
|
||
section_header "SETTING PERMISSIONS"
|
||
|
||
if [ "$environment" = "server" ]; then
|
||
# Server environment - use sudo
|
||
sudo chown -R $WEB_USER:$WEB_GROUP storage bootstrap/cache public/storage public/build || error_exit "Failed to set owner permissions"
|
||
sudo chmod -R 775 storage bootstrap/cache public/build || error_exit "Failed to set directory permissions"
|
||
else
|
||
# Local environment - no sudo needed
|
||
chmod -R 775 storage bootstrap/cache public/build || echo -e "${YELLOW}Warning: Permission change failed${NC}"
|
||
echo -e "${YELLOW}Note: Local environment - no ownership changes performed${NC}"
|
||
fi
|
||
|
||
# Set up HTMLPurifier cache directory
|
||
section_header "SETTING UP HTMLPURIFIER CACHE DIRECTORY"
|
||
echo -e "${BLUE}Configuring HTMLPurifier cache directory to prevent permission errors...${NC}"
|
||
|
||
if [ -f "deployment-htmlpurifier-fix.sh" ]; then
|
||
# Run the HTMLPurifier setup script with the same web user/group
|
||
bash deployment-htmlpurifier-fix.sh "$WEB_USER" "$WEB_GROUP" || echo -e "${YELLOW}Warning: HTMLPurifier setup encountered issues but deployment will continue${NC}"
|
||
else
|
||
echo -e "${YELLOW}Warning: deployment-htmlpurifier-fix.sh not found, skipping HTMLPurifier setup${NC}"
|
||
echo -e "${YELLOW}HTMLPurifier will auto-create cache directory on first use${NC}"
|
||
fi
|
||
|
||
# Set up Laravel scheduler cron job (server environment only)
|
||
if [ "$environment" = "server" ]; then
|
||
section_header "SETTING UP LARAVEL SCHEDULER"
|
||
|
||
# Create the cron job entry
|
||
CRON_ENTRY="* * * * * cd $APP_DIR && php artisan schedule:run >> /dev/null 2>&1"
|
||
|
||
# Check if the cron job already exists for www-data user
|
||
if sudo crontab -u $WEB_USER -l 2>/dev/null | grep -q "artisan schedule:run"; then
|
||
echo -e "${YELLOW}Laravel scheduler cron job already exists for $WEB_USER user${NC}"
|
||
else
|
||
echo -e "${BLUE}Adding Laravel scheduler cron job for $WEB_USER user...${NC}"
|
||
|
||
# Get existing crontab (if any) and add our entry
|
||
(sudo crontab -u $WEB_USER -l 2>/dev/null; echo "$CRON_ENTRY") | sudo crontab -u $WEB_USER -
|
||
|
||
if [ $? -eq 0 ]; then
|
||
echo -e "${GREEN}Laravel scheduler cron job added successfully${NC}"
|
||
else
|
||
echo -e "${YELLOW}Warning: Failed to add Laravel scheduler cron job${NC}"
|
||
echo -e "${YELLOW} You may need to manually add this cron job:${NC}"
|
||
echo -e "${YELLOW} $CRON_ENTRY${NC}"
|
||
fi
|
||
fi
|
||
else
|
||
echo -e "${YELLOW}Note: Scheduler cron job setup skipped for local environment${NC}"
|
||
fi
|
||
|
||
# Finish
|
||
section_header "APPLICATION DEPLOYED SUCCESSFULLY!"
|
||
echo -e "${GREEN}Deployment completed at: $(date)${NC}"
|
||
echo -e "${YELLOW}Don't forget to check your .env file for any needed changes${NC}"
|
||
|
||
# Print helpful post-deployment info
|
||
section_header "ENVIRONMENT INFO"
|
||
echo "App URL: $APP_URL"
|
||
echo "Laravel Version: $(php artisan --version)"
|
||
echo "PHP Version: $(php -v | head -n 1)"
|
||
echo "WebSocket Server: $WS_URL"
|
||
if [ "$environment" = "server" ]; then
|
||
echo "Build Mode: ${BUILD_MODE} (server always uses production builds)"
|
||
else
|
||
echo "Build Mode: ${BUILD_MODE} (local uses development builds)"
|
||
fi |