#!/bin/bash # ============================================================================= # validate-env.sh — Validate .env for Changemaker Lite # # Checks required variables, secret strength, placeholder detection, # production-mode requirements, and port conflicts. # # Usage: ./scripts/validate-env.sh [--strict] # --strict: treat warnings as errors (for CI/pre-deploy) # # Exit codes: # 0 = all checks passed # 1 = errors found # 2 = warnings found (only with --strict) # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" ENV_FILE="${PROJECT_DIR}/.env" # Colors RED='\033[0;31m' YELLOW='\033[1;33m' GREEN='\033[0;32m' CYAN='\033[0;36m' NC='\033[0m' ERRORS=0 WARNINGS=0 STRICT=false [[ "${1:-}" == "--strict" ]] && STRICT=true # --- Helpers --- error() { echo -e " ${RED}ERROR${NC} $1" ERRORS=$((ERRORS + 1)) } warn() { echo -e " ${YELLOW}WARN${NC} $1" WARNINGS=$((WARNINGS + 1)) } ok() { echo -e " ${GREEN}OK${NC} $1" } info() { echo -e " ${CYAN}INFO${NC} $1" } # --- Load .env --- if [[ ! -f "$ENV_FILE" ]]; then echo -e "${RED}ERROR: .env file not found at ${ENV_FILE}${NC}" echo " Copy .env.example to .env and configure it:" echo " cp .env.example .env" exit 1 fi # Source .env safely (export all, ignore errors from complex values) set -a # shellcheck disable=SC1090 source "$ENV_FILE" 2>/dev/null || true set +a echo "" echo "=========================================" echo " Changemaker Lite — Environment Validator" echo "=========================================" echo "" # --- 1. Required Variables --- echo "1. Required Variables" echo "---------------------" REQUIRED_VARS=( "V2_POSTGRES_PASSWORD" "REDIS_PASSWORD" "JWT_ACCESS_SECRET" "JWT_REFRESH_SECRET" "JWT_INVITE_SECRET" "DOMAIN" "INITIAL_ADMIN_EMAIL" "INITIAL_ADMIN_PASSWORD" ) for var in "${REQUIRED_VARS[@]}"; do val="${!var:-}" if [[ -z "$val" ]]; then error "$var is not set" else ok "$var is set" fi done echo "" # --- 2. Secret Strength --- echo "2. Secret Strength" echo "------------------" check_secret_length() { local name="$1" local min_len="$2" local val="${!name:-}" if [[ -n "$val" ]] && [[ ${#val} -lt $min_len ]]; then error "$name is too short (${#val} chars, need $min_len+)" elif [[ -n "$val" ]]; then ok "$name length OK (${#val} chars)" fi } check_secret_length "JWT_ACCESS_SECRET" 32 check_secret_length "JWT_REFRESH_SECRET" 32 check_secret_length "JWT_INVITE_SECRET" 32 check_secret_length "V2_POSTGRES_PASSWORD" 8 check_secret_length "REDIS_PASSWORD" 8 # Password policy check (12+ chars, uppercase, lowercase, digit) ADMIN_PW="${INITIAL_ADMIN_PASSWORD:-}" if [[ -n "$ADMIN_PW" ]]; then PW_OK=true if [[ ${#ADMIN_PW} -lt 12 ]]; then error "INITIAL_ADMIN_PASSWORD too short (${#ADMIN_PW} chars, need 12+)" PW_OK=false fi if ! [[ "$ADMIN_PW" =~ [A-Z] ]]; then error "INITIAL_ADMIN_PASSWORD needs at least one uppercase letter" PW_OK=false fi if ! [[ "$ADMIN_PW" =~ [a-z] ]]; then error "INITIAL_ADMIN_PASSWORD needs at least one lowercase letter" PW_OK=false fi if ! [[ "$ADMIN_PW" =~ [0-9] ]]; then error "INITIAL_ADMIN_PASSWORD needs at least one digit" PW_OK=false fi if $PW_OK; then ok "INITIAL_ADMIN_PASSWORD meets password policy" fi fi echo "" # --- 3. Placeholder Detection --- echo "3. Placeholder Detection" echo "------------------------" PLACEHOLDER_PATTERNS=("CHANGE_THIS" "REQUIRED" "changeme" "password123" "secret123" "example.com" "your-") for var in JWT_ACCESS_SECRET JWT_REFRESH_SECRET JWT_INVITE_SECRET V2_POSTGRES_PASSWORD REDIS_PASSWORD ENCRYPTION_KEY; do val="${!var:-}" if [[ -z "$val" ]]; then continue; fi for pattern in "${PLACEHOLDER_PATTERNS[@]}"; do if echo "$val" | grep -qi "$pattern"; then error "$var contains placeholder value '$pattern'" fi done done # Check secrets are not reused if [[ -n "${JWT_ACCESS_SECRET:-}" ]] && [[ "${JWT_ACCESS_SECRET:-}" == "${JWT_REFRESH_SECRET:-}" ]]; then error "JWT_ACCESS_SECRET and JWT_REFRESH_SECRET must be different" fi if [[ -n "${ENCRYPTION_KEY:-}" ]] && [[ "${ENCRYPTION_KEY:-}" == "${JWT_ACCESS_SECRET:-}" ]]; then error "ENCRYPTION_KEY must not reuse JWT_ACCESS_SECRET" fi ok "Placeholder check complete" echo "" # --- 4. Production Checks --- echo "4. Production Checks" echo "--------------------" NODE_ENV="${NODE_ENV:-development}" info "NODE_ENV=$NODE_ENV" if [[ "$NODE_ENV" == "production" ]]; then if [[ -z "${ENCRYPTION_KEY:-}" ]]; then error "ENCRYPTION_KEY is required in production" else ok "ENCRYPTION_KEY is set" fi if [[ "${EMAIL_TEST_MODE:-}" == "true" ]]; then warn "EMAIL_TEST_MODE=true in production (emails go to MailHog, not SMTP)" fi CORS="${CORS_ORIGINS:-}" if echo "$CORS" | grep -q "localhost"; then warn "CORS_ORIGINS contains 'localhost' in production" fi if [[ -z "${CORS:-}" ]]; then warn "CORS_ORIGINS is not set — API will reject cross-origin requests" else ok "CORS_ORIGINS configured" fi else info "Skipping production-only checks (NODE_ENV=$NODE_ENV)" fi echo "" # --- 5. Port Conflict Detection --- echo "5. Port Conflict Detection" echo "--------------------------" # Collect all configured ports declare -A PORT_MAP PORT_VARS=( "ADMIN_PORT:3000" "API_PORT:4000" "MEDIA_API_PORT:4100" "V2_POSTGRES_PORT:5433" "GRAFANA_PORT:3001" "HOMEPAGE_PORT:3010" "GITEA_PORT:3030" "MKDOCS_DEV_PORT:4003" "NOCODB_PORT:8091" "MAILHOG_PORT:8025" "LISTMONK_PORT:9001" "N8N_PORT:5678" "CODE_SERVER_PORT:8888" "PROMETHEUS_PORT:9090" ) DUPLICATE_FOUND=false for entry in "${PORT_VARS[@]}"; do var="${entry%%:*}" default="${entry##*:}" port="${!var:-$default}" if [[ -n "${PORT_MAP[$port]:-}" ]]; then error "Port $port conflict: ${PORT_MAP[$port]} and $var" DUPLICATE_FOUND=true else PORT_MAP[$port]="$var" fi done if ! $DUPLICATE_FOUND; then ok "No port conflicts detected" fi echo "" # --- 6. Feature Flag Consistency --- echo "6. Feature Flag Consistency" echo "---------------------------" if [[ "${ENABLE_MEDIA_FEATURES:-}" == "true" ]]; then ok "Media features enabled" fi if [[ "${LISTMONK_SYNC_ENABLED:-}" == "true" ]]; then if [[ -z "${LISTMONK_ADMIN_USER:-}" ]] || [[ -z "${LISTMONK_ADMIN_PASSWORD:-}" ]]; then warn "LISTMONK_SYNC_ENABLED=true but LISTMONK_ADMIN_USER/PASSWORD not set" else ok "Listmonk sync credentials configured" fi fi if [[ "${ENABLE_PAYMENTS:-}" == "true" ]]; then if [[ -z "${STRIPE_SECRET_KEY:-}" ]]; then warn "ENABLE_PAYMENTS=true but STRIPE_SECRET_KEY not set" fi fi echo "" # --- Summary --- echo "=========================================" if [[ $ERRORS -gt 0 ]]; then echo -e " ${RED}FAILED${NC}: $ERRORS error(s), $WARNINGS warning(s)" echo "=========================================" exit 1 elif [[ $WARNINGS -gt 0 ]] && $STRICT; then echo -e " ${YELLOW}WARNINGS${NC}: $WARNINGS warning(s) (strict mode)" echo "=========================================" exit 2 elif [[ $WARNINGS -gt 0 ]]; then echo -e " ${YELLOW}PASSED${NC} with $WARNINGS warning(s)" echo "=========================================" exit 0 else echo -e " ${GREEN}PASSED${NC}: All checks OK" echo "=========================================" exit 0 fi