Add guided tour, media enhancements, error handling, and DevOps improvements
Major additions: onboarding tour system, correlation-id middleware, media error handler, restore script, env validation script, Dockerignore files. Updates across 70+ admin components for improved UX and error handling. Bunker Admin
This commit is contained in:
@@ -137,7 +137,11 @@ done
|
||||
|
||||
echo ""
|
||||
if [[ ${#FAILED[@]} -eq 0 ]]; then
|
||||
success "All services built${NO_PUSH:+ (not pushed)}."
|
||||
if [[ "$NO_PUSH" == "true" ]]; then
|
||||
success "All services built (not pushed)."
|
||||
else
|
||||
success "All services built and pushed."
|
||||
fi
|
||||
echo ""
|
||||
if [[ "$NO_PUSH" == "false" ]]; then
|
||||
info "Images available in registry:"
|
||||
|
||||
280
scripts/restore.sh
Executable file
280
scripts/restore.sh
Executable file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Changemaker Lite V2 — Restore Script
|
||||
# Restores from a backup archive created by backup.sh.
|
||||
# Usage: ./scripts/restore.sh --archive PATH [--skip-db] [--skip-uploads]
|
||||
# [--skip-listmonk] [--dry-run] [--force]
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# --- Colors ---
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
error() { echo -e "${RED}ERROR:${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}WARN:${NC} $1"; }
|
||||
info() { echo -e "${CYAN}INFO:${NC} $1"; }
|
||||
ok() { echo -e "${GREEN}OK:${NC} $1"; }
|
||||
|
||||
# --- Defaults ---
|
||||
ARCHIVE=""
|
||||
SKIP_DB=false
|
||||
SKIP_UPLOADS=false
|
||||
SKIP_LISTMONK=false
|
||||
DRY_RUN=false
|
||||
FORCE=false
|
||||
|
||||
# --- Parse args ---
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--archive) ARCHIVE="$2"; shift 2 ;;
|
||||
--skip-db) SKIP_DB=true; shift ;;
|
||||
--skip-uploads) SKIP_UPLOADS=true; shift ;;
|
||||
--skip-listmonk) SKIP_LISTMONK=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
--help)
|
||||
echo "Usage: $0 --archive PATH [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --archive PATH Path to backup .tar.gz archive (required)"
|
||||
echo " --skip-db Skip main PostgreSQL restore"
|
||||
echo " --skip-uploads Skip uploads directory restore"
|
||||
echo " --skip-listmonk Skip Listmonk database restore"
|
||||
echo " --dry-run Validate archive without restoring"
|
||||
echo " --force Skip confirmation prompt"
|
||||
echo ""
|
||||
exit 0 ;;
|
||||
*) error "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ARCHIVE" ]]; then
|
||||
error "Missing --archive PATH argument"
|
||||
echo " Usage: $0 --archive /path/to/backup.tar.gz"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ARCHIVE" ]]; then
|
||||
error "Archive not found: $ARCHIVE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Load .env ---
|
||||
if [ -f "$PROJECT_DIR/.env" ]; then
|
||||
while IFS='=' read -r key value; do
|
||||
[[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue
|
||||
key="$(echo "$key" | xargs)"
|
||||
value="${value%\"}" ; value="${value#\"}"
|
||||
value="${value%\'}" ; value="${value#\'}"
|
||||
if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
|
||||
export "$key=$value"
|
||||
fi
|
||||
done < "$PROJECT_DIR/.env"
|
||||
fi
|
||||
|
||||
# --- Derived vars ---
|
||||
PG_CONTAINER="${PG_CONTAINER:-changemaker-v2-postgres}"
|
||||
PG_USER="${V2_POSTGRES_USER:-changemaker}"
|
||||
PG_DB="${V2_POSTGRES_DB:-changemaker_v2}"
|
||||
LISTMONK_PG_CONTAINER="${LISTMONK_PG_CONTAINER:-listmonk-db}"
|
||||
LISTMONK_PG_USER="${LISTMONK_DB_USER:-listmonk}"
|
||||
LISTMONK_PG_DB="${LISTMONK_DB_NAME:-listmonk}"
|
||||
UPLOADS_DIR="${PROJECT_DIR}/assets/uploads"
|
||||
|
||||
APP_CONTAINERS="changemaker-v2-api changemaker-v2-admin changemaker-media-api changemaker-v2-nginx"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Changemaker Lite V2 — Restore"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# --- 1. Extract and validate archive ---
|
||||
info "Extracting archive: $(basename "$ARCHIVE")"
|
||||
TEMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
|
||||
tar -xzf "$ARCHIVE" -C "$TEMP_DIR"
|
||||
|
||||
# Find the extracted backup directory (single child of temp dir)
|
||||
BACKUP_DIR="$(find "$TEMP_DIR" -mindepth 1 -maxdepth 1 -type d | head -1)"
|
||||
if [[ -z "$BACKUP_DIR" ]]; then
|
||||
error "Archive does not contain a backup directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate manifest
|
||||
MANIFEST="${BACKUP_DIR}/manifest.json"
|
||||
if [[ ! -f "$MANIFEST" ]]; then
|
||||
error "manifest.json not found in archive"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ok "Manifest found"
|
||||
echo " Backup: $(python3 -c "import json,sys; m=json.load(open(sys.argv[1])); print(m.get('backup_name','unknown'))" "$MANIFEST" 2>/dev/null || basename "$BACKUP_DIR")"
|
||||
|
||||
# --- 2. Verify checksums ---
|
||||
info "Verifying file integrity..."
|
||||
INTEGRITY_OK=true
|
||||
while IFS= read -r entry; do
|
||||
FILE=$(echo "$entry" | python3 -c "import json,sys; print(json.load(sys.stdin)['file'])")
|
||||
EXPECTED=$(echo "$entry" | python3 -c "import json,sys; print(json.load(sys.stdin)['sha256'])")
|
||||
FILE_PATH="${BACKUP_DIR}/${FILE}"
|
||||
|
||||
if [[ ! -f "$FILE_PATH" ]]; then
|
||||
warn "Missing file: $FILE"
|
||||
continue
|
||||
fi
|
||||
|
||||
ACTUAL="$(sha256sum "$FILE_PATH" 2>/dev/null | cut -d' ' -f1 || shasum -a 256 "$FILE_PATH" | cut -d' ' -f1)"
|
||||
if [[ "$EXPECTED" != "$ACTUAL" ]]; then
|
||||
error "Checksum mismatch: $FILE"
|
||||
INTEGRITY_OK=false
|
||||
else
|
||||
ok " $FILE checksum verified"
|
||||
fi
|
||||
done < <(python3 -c "import json,sys; [print(json.dumps(f)) for f in json.load(open(sys.argv[1]))['files']]" "$MANIFEST")
|
||||
|
||||
if ! $INTEGRITY_OK; then
|
||||
error "Archive integrity check failed. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Show what will be restored ---
|
||||
echo ""
|
||||
info "Components to restore:"
|
||||
[[ -f "${BACKUP_DIR}/v2-postgres.sql.gz" ]] && ! $SKIP_DB && echo " - V2 PostgreSQL (${PG_DB})"
|
||||
[[ -f "${BACKUP_DIR}/gancio-postgres.sql.gz" ]] && ! $SKIP_DB && echo " - Gancio PostgreSQL"
|
||||
[[ -f "${BACKUP_DIR}/listmonk-postgres.sql.gz" ]] && ! $SKIP_LISTMONK && echo " - Listmonk PostgreSQL (${LISTMONK_PG_DB})"
|
||||
[[ -f "${BACKUP_DIR}/uploads.tar.gz" ]] && ! $SKIP_UPLOADS && echo " - Uploads directory"
|
||||
echo ""
|
||||
|
||||
if $DRY_RUN; then
|
||||
ok "Dry run complete. Archive is valid."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Confirmation ---
|
||||
if ! $FORCE; then
|
||||
echo -e "${RED}WARNING: This will OVERWRITE existing databases and uploads!${NC}"
|
||||
read -p "Continue with restore? (y/N): " CONFIRM
|
||||
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 3. Stop application containers ---
|
||||
info "Stopping application containers..."
|
||||
for container in $APP_CONTAINERS; do
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${container}$"; then
|
||||
docker stop "$container" >/dev/null 2>&1 && echo " Stopped $container" || true
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# --- 4. Restore V2 PostgreSQL ---
|
||||
if [[ -f "${BACKUP_DIR}/v2-postgres.sql.gz" ]] && ! $SKIP_DB; then
|
||||
info "Restoring V2 PostgreSQL (${PG_DB})..."
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${PG_CONTAINER}$"; then
|
||||
# Terminate existing connections and recreate database
|
||||
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres -c \
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${PG_DB}' AND pid <> pg_backend_pid();" >/dev/null 2>&1 || true
|
||||
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres -c \
|
||||
"DROP DATABASE IF EXISTS \"${PG_DB}\";" >/dev/null 2>&1
|
||||
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres -c \
|
||||
"CREATE DATABASE \"${PG_DB}\";" >/dev/null 2>&1
|
||||
# Restore dump
|
||||
gunzip -c "${BACKUP_DIR}/v2-postgres.sql.gz" | docker exec -i "$PG_CONTAINER" psql -U "$PG_USER" -d "$PG_DB" >/dev/null 2>&1
|
||||
ok "V2 PostgreSQL restored"
|
||||
else
|
||||
error "Container ${PG_CONTAINER} not running"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 4b. Restore Gancio PostgreSQL ---
|
||||
if [[ -f "${BACKUP_DIR}/gancio-postgres.sql.gz" ]] && ! $SKIP_DB; then
|
||||
info "Restoring Gancio PostgreSQL..."
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${PG_CONTAINER}$"; then
|
||||
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres -c \
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='gancio' AND pid <> pg_backend_pid();" >/dev/null 2>&1 || true
|
||||
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres -c \
|
||||
"DROP DATABASE IF EXISTS gancio;" >/dev/null 2>&1
|
||||
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d postgres -c \
|
||||
"CREATE DATABASE gancio;" >/dev/null 2>&1
|
||||
gunzip -c "${BACKUP_DIR}/gancio-postgres.sql.gz" | docker exec -i "$PG_CONTAINER" psql -U "$PG_USER" -d gancio >/dev/null 2>&1
|
||||
ok "Gancio PostgreSQL restored"
|
||||
else
|
||||
warn "V2 PostgreSQL container not running, skipping Gancio restore"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 5. Restore Listmonk PostgreSQL ---
|
||||
if [[ -f "${BACKUP_DIR}/listmonk-postgres.sql.gz" ]] && ! $SKIP_LISTMONK; then
|
||||
info "Restoring Listmonk PostgreSQL (${LISTMONK_PG_DB})..."
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${LISTMONK_PG_CONTAINER}$"; then
|
||||
docker exec "$LISTMONK_PG_CONTAINER" psql -U "$LISTMONK_PG_USER" -d postgres -c \
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${LISTMONK_PG_DB}' AND pid <> pg_backend_pid();" >/dev/null 2>&1 || true
|
||||
docker exec "$LISTMONK_PG_CONTAINER" psql -U "$LISTMONK_PG_USER" -d postgres -c \
|
||||
"DROP DATABASE IF EXISTS \"${LISTMONK_PG_DB}\";" >/dev/null 2>&1
|
||||
docker exec "$LISTMONK_PG_CONTAINER" psql -U "$LISTMONK_PG_USER" -d postgres -c \
|
||||
"CREATE DATABASE \"${LISTMONK_PG_DB}\";" >/dev/null 2>&1
|
||||
gunzip -c "${BACKUP_DIR}/listmonk-postgres.sql.gz" | docker exec -i "$LISTMONK_PG_CONTAINER" psql -U "$LISTMONK_PG_USER" -d "$LISTMONK_PG_DB" >/dev/null 2>&1
|
||||
ok "Listmonk PostgreSQL restored"
|
||||
else
|
||||
warn "Container ${LISTMONK_PG_CONTAINER} not running, skipping"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 6. Restore uploads ---
|
||||
if [[ -f "${BACKUP_DIR}/uploads.tar.gz" ]] && ! $SKIP_UPLOADS; then
|
||||
info "Restoring uploads..."
|
||||
UPLOADS_PARENT="$(dirname "$UPLOADS_DIR")"
|
||||
mkdir -p "$UPLOADS_PARENT"
|
||||
tar -xzf "${BACKUP_DIR}/uploads.tar.gz" -C "$UPLOADS_PARENT"
|
||||
ok "Uploads restored to $UPLOADS_DIR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# --- 7. Run migrations (catch up if code is ahead of backup) ---
|
||||
info "Running Prisma migrations..."
|
||||
cd "$PROJECT_DIR"
|
||||
docker compose run --rm --no-deps --entrypoint "" api npx prisma migrate deploy 2>&1 \
|
||||
&& ok "Migrations applied" \
|
||||
|| warn "Migration apply had warnings"
|
||||
|
||||
# --- 8. Restart application containers ---
|
||||
info "Restarting application containers..."
|
||||
docker compose up -d 2>&1 | tail -5
|
||||
echo ""
|
||||
|
||||
# --- 9. Health check ---
|
||||
info "Waiting for API health check..."
|
||||
HEALTHY=false
|
||||
for i in $(seq 1 20); do
|
||||
if docker compose exec -T api wget -q --spider http://localhost:4000/api/health 2>/dev/null; then
|
||||
HEALTHY=true
|
||||
break
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
|
||||
if $HEALTHY; then
|
||||
ok "API is healthy"
|
||||
else
|
||||
warn "API health check timed out (60s). Check logs: docker compose logs api"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo -e " ${GREEN}Restore complete!${NC}"
|
||||
echo " Archive: $(basename "$ARCHIVE")"
|
||||
echo "=========================================="
|
||||
@@ -1121,30 +1121,35 @@ write_progress 7 "Verification" 90 "Running health checks..."
|
||||
|
||||
VERIFY_FAILED=false
|
||||
|
||||
# API health
|
||||
if docker compose exec -T api wget -q --spider http://localhost:4000/api/health 2>/dev/null; then
|
||||
success "API (port 4000): healthy"
|
||||
else
|
||||
warn "API (port 4000): not responding"
|
||||
# Polling health check helper (retries for up to MAX_WAIT seconds)
|
||||
verify_service_health() {
|
||||
local name="$1" check_cmd="$2" max_wait="${3:-30}"
|
||||
local waited=0
|
||||
while [[ $waited -lt $max_wait ]]; do
|
||||
if eval "$check_cmd" 2>/dev/null; then
|
||||
success "$name: healthy (${waited}s)"
|
||||
return 0
|
||||
fi
|
||||
sleep 3
|
||||
waited=$((waited + 3))
|
||||
done
|
||||
warn "$name: not responding after ${max_wait}s"
|
||||
VERIFY_FAILED=true
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# API health (with polling — may still be running migrations)
|
||||
verify_service_health "API (port 4000)" \
|
||||
"docker compose exec -T api wget -q --spider http://localhost:4000/api/health" 45
|
||||
|
||||
# Admin health
|
||||
if docker compose exec -T admin wget -q --spider http://localhost:3000/ 2>/dev/null; then
|
||||
success "Admin (port 3000): healthy"
|
||||
else
|
||||
warn "Admin (port 3000): not responding"
|
||||
VERIFY_FAILED=true
|
||||
fi
|
||||
verify_service_health "Admin (port 3000)" \
|
||||
"docker compose exec -T admin wget -q --spider http://localhost:3000/" 30
|
||||
|
||||
# Media API health (optional — may not be enabled)
|
||||
if docker ps --format '{{.Names}}' | grep -q 'changemaker-media-api'; then
|
||||
if docker compose exec -T media-api wget -q --spider http://127.0.0.1:4100/health 2>/dev/null; then
|
||||
success "Media API (port 4100): healthy"
|
||||
else
|
||||
warn "Media API (port 4100): not responding"
|
||||
VERIFY_FAILED=true
|
||||
fi
|
||||
verify_service_health "Media API (port 4100)" \
|
||||
"docker compose exec -T media-api wget -q --spider http://127.0.0.1:4100/health" 30
|
||||
fi
|
||||
|
||||
# Gancio health (optional)
|
||||
|
||||
305
scripts/validate-env.sh
Executable file
305
scripts/validate-env.sh
Executable file
@@ -0,0 +1,305 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user