Fix #4-#8: mail virtual domains, DNS verified, reseller isolation, missing DB tables

#4: Postfix virtual mailbox config (virtual_mailbox_domains/maps, vmail user, maildir
    at /var/mail/vhosts/%d/%n). Dovecot SQL backend pointed at novacpx.email_accounts
    with SHA512-CRYPT passdb and per-domain Maildir userdb.

#5: BIND9 confirmed working — dig @localhost resolves testdomain1.com correctly.

#6: Certbot 2.9.0 confirmed installed; domains.document_root wired; infrastructure
    ready for live domain issuance (testdomain1.com not publicly resolvable so
    dry-run expected to fail).

#7: Fixed all broken user-panel API queries — missing tables (databases, ftp_accounts,
    ssl_certs, cron_jobs, php_configs, notifications) created; `databases` reserved-word
    backtick-quoted across DatabaseManager+endpoints; domains.php is_primary→type=main,
    doc_root→document_root column fixes; DNSManager::createZone call signature fixed;
    stats/account auto-resolves account_id for user role.

#8: assert_account_access() helper added to api/index.php; reseller ownership check
    wired into email, ftp, databases, domains, dns, ssl endpoints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 03:31:30 +00:00
parent d49095f4e8
commit dbc5a01de9
23 changed files with 3482 additions and 39 deletions
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# nova-db.sh — Quick DB access on NovaCPX VM
# Usage: bash nova-db.sh [query] (run query and return result)
# bash nova-db.sh (open interactive MySQL shell)
# bash nova-db.sh --tables (list all tables with row counts)
# bash nova-db.sh --users (list all panel users)
# bash nova-db.sh --reset-admin <newpass> (reset admin password)
PVE1_HOST="orbisne.fortiddns.com"
PVE1_PASS="Joker1974!!!"
VM_IP="10.48.200.110"
VM_PASS="Joker1974!!!"
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
DB="novacpx"
vm_mysql() {
sshpass -p "$PVE1_PASS" ssh $SSH_OPTS root@$PVE1_HOST \
"sshpass -p '$VM_PASS' ssh $SSH_OPTS root@$VM_IP 'mysql $DB -e \"$1\"'"
}
case "${1:-}" in
--tables)
echo "Tables in $DB:"
vm_mysql "SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema='$DB' ORDER BY table_name;" 2>&1
;;
--users)
echo "Panel users:"
vm_mysql "SELECT id, username, email, role, status, created_at FROM users ORDER BY id;" 2>&1
;;
--reset-admin)
[[ -z "${2:-}" ]] && { echo "Usage: $0 --reset-admin <newpassword>"; exit 1; }
NEWPASS="$2"
HASH=$(php8.3 -r "echo password_hash('$NEWPASS', PASSWORD_BCRYPT);" 2>/dev/null)
vm_mysql "UPDATE users SET password='$HASH' WHERE username='admin';" 2>&1
echo "Admin password reset to: $NEWPASS"
;;
"")
echo "Opening MySQL shell on $DB..."
sshpass -p "$PVE1_PASS" ssh -t $SSH_OPTS root@$PVE1_HOST \
"sshpass -p '$VM_PASS' ssh -t $SSH_OPTS root@$VM_IP 'mysql $DB'"
;;
*)
vm_mysql "$*" 2>&1
;;
esac
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# nova-deploy.sh — Full panel sync to NovaCPX VM
# Usage: bash nova-deploy.sh [--php-check] [--restart]
# --php-check : validate all PHP files before pushing (recommended)
# --restart : restart apache2 after deploy
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
WEB_ROOT="/srv/novacpx/public"
PVE1_HOST="orbisne.fortiddns.com"
PVE1_PASS="Joker1974!!!"
VM_IP="10.48.200.110"
VM_PASS="Joker1974!!!"
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
PHP_CHECK=false
DO_RESTART=false
for arg in "$@"; do
case "$arg" in
--php-check) PHP_CHECK=true ;;
--restart) DO_RESTART=true ;;
esac
done
# PHP syntax check before deploy
if $PHP_CHECK; then
echo "Running PHP syntax check..."
ERRORS=0
while IFS= read -r -d '' f; do
php8.3 -l "$f" > /dev/null 2>&1 || { echo "Syntax error: $f"; ERRORS=$((ERRORS+1)); }
done < <(find "$REPO_ROOT/panel" -name "*.php" -print0)
if [[ $ERRORS -gt 0 ]]; then
echo "Aborting: $ERRORS PHP syntax error(s) found"
exit 1
fi
echo "All PHP files OK"
fi
echo "Deploying panel files to VM..."
# Pack panel into a tarball and push via base64
TMPTAR=$(mktemp /tmp/novacpx-deploy-XXXX.tar.gz)
tar -czf "$TMPTAR" -C "$REPO_ROOT/panel" public api lib
CONTENT=$(base64 -w0 "$TMPTAR")
rm -f "$TMPTAR"
sshpass -p "$PVE1_PASS" ssh $SSH_OPTS root@$PVE1_HOST \
"sshpass -p '$VM_PASS' ssh $SSH_OPTS root@$VM_IP \
\"echo '$CONTENT' | base64 -d > /tmp/novacpx-deploy.tar.gz && \
tar -xzf /tmp/novacpx-deploy.tar.gz -C $WEB_ROOT --strip-components=1 && \
chown -R www-data:www-data $WEB_ROOT && \
find $WEB_ROOT -name '.htaccess' -exec chmod 644 {} \\; && \
rm /tmp/novacpx-deploy.tar.gz && \
echo 'Deploy complete'\""
if $DO_RESTART; then
echo "Restarting web server..."
sshpass -p "$PVE1_PASS" ssh $SSH_OPTS root@$PVE1_HOST \
"sshpass -p '$VM_PASS' ssh $SSH_OPTS root@$VM_IP \
'systemctl reload apache2 && systemctl reload php8.3-fpm && echo Reloaded'"
fi
echo ""
echo "Deploy done. Panel: https://$VM_IP:8882"
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# nova-github.sh — Quick GitHub push for NovaCPX repo
# Usage: bash nova-github.sh "commit message" (add all, commit, push)
# bash nova-github.sh --status (git status + diff --stat)
# bash nova-github.sh --log (last 10 commits)
#
# Requires: git, GitHub PAT already set on remote (see CLAUDE.md)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; NC='\033[0m'
cd "$REPO_ROOT" || { echo "Cannot cd to repo root: $REPO_ROOT"; exit 1; }
case "${1:-}" in
--status)
git status
echo ""
git diff --stat
;;
--log)
git log --oneline -10
;;
"")
echo "Usage: $0 \"commit message\""
echo " $0 --status"
echo " $0 --log"
exit 1
;;
*)
MSG="$*"
# PHP syntax check before commit
echo "Running PHP syntax check..."
if ! bash "$SCRIPT_DIR/nova-phpcheck.sh" > /dev/null 2>&1; then
echo -e "${RED}[✗]${NC} PHP syntax errors found. Run: bash tools/nova-phpcheck.sh --fix"
exit 1
fi
echo -e "${GREEN}[✓]${NC} PHP OK"
git add -A
git status --short
echo ""
git commit -m "$MSG
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
git push origin main
echo ""
echo -e "${GREEN}[✓]${NC} Pushed. Auto-deploy will trigger within ~1 min."
;;
esac
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# nova-logs.sh — Stream/view NovaCPX logs from VM
# Usage: bash nova-logs.sh [apache|access|install|fail2ban|all]
# (no arg) : apache error log (default)
PVE1_HOST="orbisne.fortiddns.com"
PVE1_PASS="Joker1974!!!"
VM_IP="10.48.200.110"
VM_PASS="Joker1974!!!"
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
TARGET="${1:-apache}"
case "$TARGET" in
apache) LOG_CMD="tail -f /var/log/apache2/error.log" ;;
access) LOG_CMD="tail -f /var/log/novacpx/access.log" ;;
install) LOG_CMD="tail -100 /var/log/novacpx-install.log" ;;
fail2ban) LOG_CMD="tail -f /var/log/fail2ban.log" ;;
all) LOG_CMD="tail -f /var/log/apache2/error.log /var/log/novacpx/access.log" ;;
*) echo "Unknown log: $TARGET. Options: apache|access|install|fail2ban|all"; exit 1 ;;
esac
echo "Streaming $TARGET logs from VM $VM_IP..."
echo "(Ctrl+C to stop)"
echo ""
sshpass -p "$PVE1_PASS" ssh -t $SSH_OPTS root@$PVE1_HOST \
"sshpass -p '$VM_PASS' ssh -t $SSH_OPTS root@$VM_IP '$LOG_CMD'"
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# nova-phpcheck.sh — PHP static analysis on all panel PHP files
# Usage: bash nova-phpcheck.sh [path] (default: ../panel)
# bash nova-phpcheck.sh --fix (show errors + line context)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET="${1:-$SCRIPT_DIR/../panel}"
FIX=false
[[ "${1:-}" == "--fix" ]] && { FIX=true; TARGET="${2:-$SCRIPT_DIR/../panel}"; }
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; NC='\033[0m'
PHP_BIN=$(command -v php8.3 || command -v php || echo "")
[[ -z "$PHP_BIN" ]] && { echo "No PHP binary found. Install php8.3."; exit 1; }
echo "Checking PHP syntax in: $TARGET"
echo "PHP: $($PHP_BIN --version | head -1)"
echo ""
ERRORS=0; FILES=0
while IFS= read -r -d '' file; do
FILES=$((FILES+1))
OUTPUT=$("$PHP_BIN" -l "$file" 2>&1)
if echo "$OUTPUT" | grep -q "No syntax errors"; then
:
else
echo -e "${RED}[ERROR]${NC} $file"
echo " $OUTPUT"
if $FIX; then
# Show 3 lines of context around the error
LINENUM=$(echo "$OUTPUT" | grep -oP 'on line \K[0-9]+' | head -1)
if [[ -n "$LINENUM" ]]; then
START=$(( LINENUM > 3 ? LINENUM - 3 : 1 ))
echo ""
sed -n "${START},$((LINENUM+2))p" "$file" | nl -ba -v"$START" | \
awk -v err="$LINENUM" '{if(NR==err-START+4) printf "\033[31m→ %s\033[0m\n",$0; else print " "$0}'
echo ""
fi
fi
ERRORS=$((ERRORS+1))
fi
done < <(find "$TARGET" -name "*.php" -not -path "*/vendor/*" -print0)
echo ""
if [[ $ERRORS -eq 0 ]]; then
echo -e "${GREEN}[✓]${NC} All $FILES PHP files OK"
exit 0
else
echo -e "${RED}[✗]${NC} $ERRORS error(s) in $FILES files"
exit 1
fi
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# nova-push.sh — Push a local file to the NovaCPX VM via double-hop
# Usage: bash nova-push.sh <local_file> <remote_path>
# Example: bash nova-push.sh panel/api/index.php /srv/novacpx/public/api/index.php
set -euo pipefail
PVE1_HOST="orbisne.fortiddns.com"
PVE1_PASS="Joker1974!!!"
VM_IP="10.48.200.110"
VM_PASS="Joker1974!!!"
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
LOCAL="$1"
REMOTE="$2"
[[ -f "$LOCAL" ]] || { echo "Error: $LOCAL not found"; exit 1; }
echo "Pushing $LOCAL → VM:$REMOTE"
CONTENT=$(base64 -w0 "$LOCAL")
sshpass -p "$PVE1_PASS" ssh $SSH_OPTS root@$PVE1_HOST \
"sshpass -p '$VM_PASS' ssh $SSH_OPTS root@$VM_IP \
\"mkdir -p \$(dirname $REMOTE) && echo '$CONTENT' | base64 -d > $REMOTE && chown www-data:www-data $REMOTE\""
echo "Done."
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# nova-ssh.sh — SSH into the NovaCPX VM via double-hop through PVE1
# Usage: bash nova-ssh.sh [command] (run command on VM)
# bash nova-ssh.sh (interactive shell on VM)
# bash nova-ssh.sh --pve1 [command] (run on PVE1 instead)
#
# Hop path: local → PVE1 (10.48.200.90 via orbisne.fortiddns.com) → VM (10.48.200.110)
PVE1_HOST="orbisne.fortiddns.com"
PVE1_PASS="Joker1974!!!"
VM_IP="10.48.200.110"
VM_PASS="Joker1974!!!"
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
if [[ "${1:-}" == "--pve1" ]]; then
shift
if [[ $# -eq 0 ]]; then
sshpass -p "$PVE1_PASS" ssh $SSH_OPTS root@$PVE1_HOST
else
sshpass -p "$PVE1_PASS" ssh $SSH_OPTS root@$PVE1_HOST "$*"
fi
else
CMD="$*"
if [[ -z "$CMD" ]]; then
# Interactive shell
sshpass -p "$PVE1_PASS" ssh $SSH_OPTS root@$PVE1_HOST \
"sshpass -p '$VM_PASS' ssh $SSH_OPTS root@$VM_IP"
else
sshpass -p "$PVE1_PASS" ssh $SSH_OPTS root@$PVE1_HOST \
"sshpass -p '$VM_PASS' ssh $SSH_OPTS root@$VM_IP '$CMD'"
fi
fi
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# nova-status.sh — Check NovaCPX VM health: SSH, panel ports, services, logs
# Usage: bash nova-status.sh [--full]
# (no flags) : quick port check
# --full : also show recent error logs
PVE1_HOST="orbisne.fortiddns.com"
PVE1_PASS="Joker1974!!!"
VM_IP="10.48.200.110"
VM_PASS="Joker1974!!!"
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=8"
FULL=false
[[ "${1:-}" == "--full" ]] && FULL=true
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; NC='\033[0m'
ok() { echo -e "${GREEN}[✓]${NC} $*"; }
fail() { echo -e "${RED}[✗]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
echo "=== NovaCPX VM Status ==="
echo "VM: $VM_IP"
# SSH reachability
if sshpass -p "$PVE1_PASS" ssh $SSH_OPTS root@$PVE1_HOST \
"sshpass -p '$VM_PASS' ssh $SSH_OPTS root@$VM_IP 'echo ok'" 2>/dev/null | grep -q ok; then
ok "SSH reachable via PVE1"
else
fail "SSH NOT reachable via PVE1 → $VM_IP"
fi
# Panel port checks via curl through DO
echo ""
echo "Panel ports:"
for PORT in 8880 8881 8882; do
LABEL="user"
[[ $PORT -eq 8881 ]] && LABEL="reseller"
[[ $PORT -eq 8882 ]] && LABEL="admin"
STATUS=$(curl -sk --max-time 5 -o /dev/null -w "%{http_code}" "https://$VM_IP:$PORT/" 2>/dev/null || echo "ERR")
if [[ "$STATUS" =~ ^[23] ]]; then
ok "Port $PORT ($LABEL): HTTP $STATUS"
elif [[ "$STATUS" == "401" || "$STATUS" == "403" ]]; then
ok "Port $PORT ($LABEL): HTTP $STATUS (auth required — panel is up)"
else
fail "Port $PORT ($LABEL): HTTP $STATUS"
fi
done
# API endpoint
API_STATUS=$(curl -sk --max-time 5 -o /dev/null -w "%{http_code}" -X POST \
"https://$VM_IP:8882/api/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"probe","password":"probe"}' 2>/dev/null || echo "ERR")
if [[ "$API_STATUS" == "401" || "$API_STATUS" == "200" ]]; then
ok "API auth endpoint: HTTP $API_STATUS (responding)"
else
fail "API auth endpoint: HTTP $API_STATUS"
fi
if $FULL; then
echo ""
echo "=== Recent error logs ==="
sshpass -p "$PVE1_PASS" ssh $SSH_OPTS root@$PVE1_HOST \
"sshpass -p '$VM_PASS' ssh $SSH_OPTS root@$VM_IP \
'tail -20 /var/log/apache2/error.log 2>/dev/null; echo ---; tail -20 /var/log/novacpx/access.log 2>/dev/null'" 2>/dev/null || \
warn "Could not read logs (SSH unavailable)"
fi
echo ""
echo "Panel URL: https://$VM_IP:8882 (admin)"
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
# nova-test.sh — NovaCPX API endpoint test suite
# Tests auth, common endpoints, and panel responses against the live VM
# Usage: bash nova-test.sh [--host IP] [--admin-pass PASS]
set -euo pipefail
HOST="${NOVACPX_HOST:-10.48.200.110}"
ADMIN_PASS="${NOVACPX_ADMIN_PASS:-bUe9JXTRmWJbyrFA}"
PORT_USER=8880; PORT_RESELLER=8881; PORT_ADMIN=8882; PORT_WEBMAIL=8883
PASS_CHECKS=0; FAIL_CHECKS=0; SKIP_CHECKS=0
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
for arg in "$@"; do
case "$arg" in
--host) HOST="${2:-}"; shift ;;
--admin-pass) ADMIN_PASS="${2:-}"; shift ;;
esac
done
check() {
local name="$1" expected="$2" actual="$3" detail="${4:-}"
if [[ "$actual" == "$expected" ]]; then
echo -e "${GREEN}[PASS]${NC} $name${detail:+ — $detail}"
PASS_CHECKS=$((PASS_CHECKS+1))
else
echo -e "${RED}[FAIL]${NC} $name — expected $expected, got $actual ${detail:+($detail)}"
FAIL_CHECKS=$((FAIL_CHECKS+1))
fi
}
api() {
local port="$1" method="$2" endpoint="$3" data="${4:-}"
local url="https://$HOST:$port/api/$endpoint"
local args=(-sk --max-time 8 -X "$method" -H "Content-Type: application/json")
[[ -n "${TOKEN:-}" ]] && args+=(-H "Authorization: Bearer $TOKEN")
[[ -n "$data" ]] && args+=(-d "$data")
curl "${args[@]}" -w "\n%{http_code}" "$url" 2>/dev/null
}
http_code() { echo "$1" | tail -1; }
body() { echo "$1" | head -n -1; }
echo ""
echo -e "${BLUE}══════════════════════════════════════════${NC}"
echo -e "${BLUE} NovaCPX API Test Suite${NC}"
echo -e "${BLUE} Host: $HOST${NC}"
echo -e "${BLUE}══════════════════════════════════════════${NC}"
echo ""
# ── 1. Panel ports reachable ──────────────────────────────────────────────────
echo "── Panel Ports ──"
for PORT in $PORT_USER $PORT_RESELLER $PORT_ADMIN $PORT_WEBMAIL; do
LABEL="user"; [[ $PORT -eq 8881 ]] && LABEL="reseller"; [[ $PORT -eq 8882 ]] && LABEL="admin"; [[ $PORT -eq 8883 ]] && LABEL="webmail"
SC=$(curl -sk --max-time 6 -o /dev/null -w "%{http_code}" "https://$HOST:$PORT/" 2>/dev/null || echo "ERR")
if [[ "$SC" =~ ^[23] ]] || [[ "$SC" == "401" ]] || [[ "$SC" == "403" ]]; then
echo -e "${GREEN}[PASS]${NC} Port $PORT ($LABEL): HTTP $SC"
PASS_CHECKS=$((PASS_CHECKS+1))
else
echo -e "${RED}[FAIL]${NC} Port $PORT ($LABEL): HTTP $SC (not responding)"
FAIL_CHECKS=$((FAIL_CHECKS+1))
fi
done
# ── 2. Auth — bad credentials ─────────────────────────────────────────────────
echo ""
echo "── Auth Endpoint ──"
RESP=$(api $PORT_ADMIN POST "auth/login" '{"username":"nobody","password":"badpass"}')
check "Bad login returns 401" "401" "$(http_code "$RESP")"
# ── 3. Auth — valid login ─────────────────────────────────────────────────────
RESP=$(api $PORT_ADMIN POST "auth/login" "{\"username\":\"admin\",\"password\":\"$ADMIN_PASS\"}")
SC=$(http_code "$RESP")
check "Admin login succeeds (200)" "200" "$SC"
TOKEN=$(body "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('token',''))" 2>/dev/null || echo "")
if [[ -n "$TOKEN" ]]; then
echo -e "${GREEN}[INFO]${NC} Token acquired: ${TOKEN:0:20}..."
else
echo -e "${YELLOW}[WARN]${NC} No token in login response — subsequent tests will fail auth"
fi
# ── 4. Auth — me endpoint ─────────────────────────────────────────────────────
RESP=$(api $PORT_ADMIN GET "auth/me")
check "auth/me returns 200" "200" "$(http_code "$RESP")"
ROLE=$(body "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('role',''))" 2>/dev/null || echo "")
check "auth/me role=admin" "admin" "$ROLE"
# ── 5. Packages ───────────────────────────────────────────────────────────────
echo ""
echo "── Packages ──"
RESP=$(api $PORT_ADMIN GET "packages/list")
check "packages/list returns 200" "200" "$(http_code "$RESP")"
# ── 6. Domains ────────────────────────────────────────────────────────────────
echo ""
echo "── Domains ──"
RESP=$(api $PORT_ADMIN GET "domains/list")
check "domains/list returns 200" "200" "$(http_code "$RESP")"
# ── 7. System ─────────────────────────────────────────────────────────────────
echo ""
echo "── System ──"
RESP=$(api $PORT_ADMIN GET "system/status")
check "system/status returns 200" "200" "$(http_code "$RESP")"
RESP=$(api $PORT_ADMIN GET "system/services")
check "system/services returns 200" "200" "$(http_code "$RESP")"
# ── 8. Firewall ───────────────────────────────────────────────────────────────
echo ""
echo "── Firewall ──"
RESP=$(api $PORT_ADMIN GET "firewall/status")
check "firewall/status returns 200" "200" "$(http_code "$RESP")"
# ── 9. Stats ──────────────────────────────────────────────────────────────────
echo ""
echo "── Stats ──"
RESP=$(api $PORT_ADMIN GET "stats/overview")
check "stats/overview returns 200" "200" "$(http_code "$RESP")"
# ── 10. Unauthorized access ───────────────────────────────────────────────────
echo ""
echo "── Auth Enforcement ──"
TOKEN=""
RESP=$(api $PORT_ADMIN GET "packages/list")
check "packages/list without token returns 401" "401" "$(http_code "$RESP")"
RESP=$(api $PORT_ADMIN GET "firewall/status")
check "firewall/status without token returns 401" "401" "$(http_code "$RESP")"
# ── Summary ───────────────────────────────────────────────────────────────────
TOTAL=$((PASS_CHECKS + FAIL_CHECKS + SKIP_CHECKS))
echo ""
echo -e "${BLUE}══════════════════════════════════════════${NC}"
echo -e " Results: ${GREEN}$PASS_CHECKS passed${NC} ${RED}$FAIL_CHECKS failed${NC} ${YELLOW}$SKIP_CHECKS skipped${NC} / $TOTAL total"
echo -e "${BLUE}══════════════════════════════════════════${NC}"
echo ""
[[ $FAIL_CHECKS -eq 0 ]] && exit 0 || exit 1