Sanitize codebase, reorganize docs, and add missing deploy files
Remove dead code identified in docs/SANITIZATION-REVIEW.md:
- app/blueprints/content_old.py and app/blueprints/playlist.py
- app/models/group.py, app/utils/nginx_config_reader.py
- orphaned templates (content_list, edit_content, upload_content,
player_page) and the related group/Template references
Result: 6 blueprints, 82 routes, no dead modules or orphan templates.
Add files that deploy.sh and docker-entrypoint.sh already require but
which were never tracked:
- https_manager.py (referenced by deploy.sh, migrate_network.sh,
docker-entrypoint.sh)
- Caddyfile.example (seeded by deploy.sh; its absence aborts deploy)
Relocate generated Graphify artifacts from graphify-out/ to
docs/graphify-out/ (110 files, no content change) and archive the
superseded docs under docs/.
Ignore hygiene:
- ignore ad-hoc .env backups (.env.bak*) — they contain live secrets
- keep the pre-sanitization snapshots (docs/legacy code/,
docs/old_code_documentation/) on disk but out of the repo
Fix .env.example: drop a duplicated config block, genericize the
hardcoded host IP, and document HOSTNAME_INTERNAL.
This commit is contained in:
+142
-56
@@ -25,19 +25,23 @@ FAILED=0
|
||||
WARNINGS=0
|
||||
|
||||
# Helper functions
|
||||
# NOTE: `((VAR++))` evaluates to the PRE-increment value, so the very first
|
||||
# increment returns 0 — a non-zero exit status that `set -e` treats as a fatal
|
||||
# error, aborting the whole script after the first check. Use `VAR=$((VAR+1))`
|
||||
# (always status 0) instead.
|
||||
pass() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
((PASSED++))
|
||||
PASSED=$((PASSED + 1))
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
((FAILED++))
|
||||
FAILED=$((FAILED + 1))
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
((WARNINGS++))
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
}
|
||||
|
||||
info() {
|
||||
@@ -99,19 +103,38 @@ else
|
||||
fail "Docker not installed"
|
||||
fi
|
||||
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
pass "Docker Compose installed"
|
||||
# Accept either the modern compose plugin or the standalone v1 binary.
|
||||
if docker compose version &> /dev/null; then
|
||||
COMPOSE="docker compose"
|
||||
pass "Docker Compose plugin installed"
|
||||
DC_VERSION=$(docker compose version --short 2>/dev/null || docker compose version | head -1)
|
||||
info "Compose version: $DC_VERSION"
|
||||
elif command -v docker-compose &> /dev/null; then
|
||||
COMPOSE="docker-compose"
|
||||
warn "Using legacy 'docker-compose' (v1); the 'docker compose' plugin is unavailable"
|
||||
DC_VERSION=$(docker-compose --version | cut -d' ' -f3 | tr -d ',')
|
||||
info "Docker Compose version: $DC_VERSION"
|
||||
|
||||
# Compose v1 builds require buildx >= 0.17; older buildx must use `docker build`.
|
||||
BUILDX_VER=$(docker buildx version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+' | head -1 || true)
|
||||
if [ -n "$BUILDX_VER" ]; then
|
||||
_maj="${BUILDX_VER%%.*}"; _min="${BUILDX_VER##*.}"
|
||||
if [ "$_maj" -eq 0 ] && [ "$_min" -lt 17 ]; then
|
||||
info "buildx $BUILDX_VER too old for compose v1 builds — deploy.sh falls back to 'docker build'"
|
||||
fi
|
||||
else
|
||||
info "buildx not available — deploy.sh falls back to 'docker build'"
|
||||
fi
|
||||
else
|
||||
fail "Docker Compose not installed"
|
||||
COMPOSE=""
|
||||
fi
|
||||
|
||||
if [ -f docker-compose.yml ]; then
|
||||
pass "docker-compose.yml exists"
|
||||
|
||||
|
||||
# Validate syntax
|
||||
if docker-compose config > /dev/null 2>&1; then
|
||||
if [ -n "$COMPOSE" ] && $COMPOSE config > /dev/null 2>&1; then
|
||||
pass "docker-compose.yml syntax valid"
|
||||
else
|
||||
fail "docker-compose.yml syntax error"
|
||||
@@ -169,7 +192,7 @@ if [ -f requirements.txt ]; then
|
||||
done
|
||||
|
||||
# Check for specific versions
|
||||
FLASK_VERSION=$(grep "^Flask==" requirements.txt | cut -d'=' -f3)
|
||||
FLASK_VERSION=$(grep "^Flask==" requirements.txt 2>/dev/null | cut -d'=' -f3 || true)
|
||||
SQLALCHEMY_VERSION=$(grep "^SQLAlchemy==" requirements.txt | cut -d'=' -f3)
|
||||
|
||||
if [ -n "$FLASK_VERSION" ]; then
|
||||
@@ -202,35 +225,41 @@ else
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
section "7. SSL/TLS Certificate"
|
||||
section "7. TLS Certificate (Caddy)"
|
||||
# ============================================================================
|
||||
|
||||
if [ -f data/nginx-ssl/cert.pem ]; then
|
||||
pass "SSL certificate found"
|
||||
|
||||
CERT_EXPIRY=$(openssl x509 -enddate -noout -in data/nginx-ssl/cert.pem 2>/dev/null | cut -d= -f2)
|
||||
EXPIRY_EPOCH=$(date -d "$CERT_EXPIRY" +%s 2>/dev/null || echo 0)
|
||||
NOW_EPOCH=$(date +%s)
|
||||
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
|
||||
|
||||
info "Certificate expires: $CERT_EXPIRY"
|
||||
info "Days remaining: $DAYS_LEFT days"
|
||||
|
||||
if [ "$DAYS_LEFT" -lt 0 ]; then
|
||||
fail "Certificate has expired!"
|
||||
elif [ "$DAYS_LEFT" -lt 30 ]; then
|
||||
warn "Certificate expires in less than 30 days"
|
||||
else
|
||||
pass "Certificate is valid"
|
||||
fi
|
||||
|
||||
if [ -f data/nginx-ssl/key.pem ]; then
|
||||
pass "SSL private key found"
|
||||
else
|
||||
warn "SSL private key not found"
|
||||
# Caddy stores its internal CA and issued certificates under data/caddy-data.
|
||||
# Those files are created by the (root) Caddy process, so read them through the
|
||||
# container rather than from the host filesystem.
|
||||
CADDY_CA_IN_CONTAINER="/data/caddy/pki/authorities/local/root.crt"
|
||||
|
||||
if $COMPOSE exec -T caddy sh -c "test -f $CADDY_CA_IN_CONTAINER" 2>/dev/null; then
|
||||
pass "Caddy internal CA root certificate found"
|
||||
|
||||
CERT_EXPIRY=$($COMPOSE exec -T caddy sh -c \
|
||||
"caddy version >/dev/null 2>&1; cat $CADDY_CA_IN_CONTAINER" 2>/dev/null \
|
||||
| openssl x509 -enddate -noout 2>/dev/null | cut -d= -f2)
|
||||
if [ -n "$CERT_EXPIRY" ]; then
|
||||
EXPIRY_EPOCH=$(date -d "$CERT_EXPIRY" +%s 2>/dev/null || echo 0)
|
||||
NOW_EPOCH=$(date +%s)
|
||||
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
|
||||
|
||||
info "Root CA expires: $CERT_EXPIRY"
|
||||
info "Days remaining: $DAYS_LEFT days"
|
||||
|
||||
if [ "$DAYS_LEFT" -lt 0 ]; then
|
||||
fail "Caddy internal CA has expired!"
|
||||
elif [ "$DAYS_LEFT" -lt 30 ]; then
|
||||
warn "Caddy internal CA expires in less than 30 days"
|
||||
else
|
||||
pass "Caddy internal CA is valid"
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Install this CA on client devices to trust the internal certificate:"
|
||||
info " $COMPOSE cp caddy:$CADDY_CA_IN_CONTAINER ./caddy-root.crt"
|
||||
else
|
||||
warn "SSL certificate not found (self-signed required)"
|
||||
info "No Caddy internal CA yet (created on first 'tls internal' issuance)"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
@@ -255,42 +284,92 @@ else
|
||||
fail "app/config.py not found"
|
||||
fi
|
||||
|
||||
if [ -f nginx.conf ]; then
|
||||
pass "nginx.conf exists"
|
||||
|
||||
if grep -q "ssl_protocols" nginx.conf; then
|
||||
pass "SSL protocols configured"
|
||||
# The reverse proxy is Caddy; data/Caddyfile is the live config.
|
||||
if [ -f data/Caddyfile ]; then
|
||||
pass "data/Caddyfile exists"
|
||||
|
||||
if grep -q "reverse_proxy" data/Caddyfile; then
|
||||
pass "Caddy reverse_proxy configured"
|
||||
else
|
||||
warn "SSL protocols not configured"
|
||||
warn "No reverse_proxy directive in Caddyfile"
|
||||
fi
|
||||
|
||||
if grep -q "access-control-allow" nginx.conf; then
|
||||
pass "CORS headers in nginx"
|
||||
|
||||
if grep -q "admin " data/Caddyfile; then
|
||||
pass "Caddy admin API configured (needed for live reloads)"
|
||||
else
|
||||
info "CORS headers may be handled by Flask only"
|
||||
warn "Caddy admin API not configured — HTTPS changes cannot hot-reload"
|
||||
fi
|
||||
|
||||
if grep -qE "tls internal" data/Caddyfile; then
|
||||
info "Using Caddy internal CA (intranet/non-public hostname)"
|
||||
elif grep -qE "^https://|^[a-zA-Z0-9.-]+ \{" data/Caddyfile; then
|
||||
info "TLS enabled (Let's Encrypt)"
|
||||
else
|
||||
info "HTTP-only configuration"
|
||||
fi
|
||||
elif [ -f Caddyfile.example ]; then
|
||||
info "data/Caddyfile not present yet; deploy.sh seeds it from Caddyfile.example"
|
||||
else
|
||||
warn "nginx.conf not found"
|
||||
fail "Neither data/Caddyfile nor Caddyfile.example found"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
section "9. Runtime Verification"
|
||||
# ============================================================================
|
||||
|
||||
if docker-compose ps 2>/dev/null | grep -q "Up"; then
|
||||
if [ -n "$COMPOSE" ] && $COMPOSE ps 2>/dev/null | grep -q "Up"; then
|
||||
pass "Docker containers are running"
|
||||
|
||||
# Check if app is healthy
|
||||
if docker-compose ps 2>/dev/null | grep -q "digiserver-app.*healthy"; then
|
||||
|
||||
if $COMPOSE ps 2>/dev/null | grep -q "digiserver-v2.*healthy"; then
|
||||
pass "DigiServer app container is healthy"
|
||||
else
|
||||
warn "DigiServer app container health status unknown"
|
||||
fi
|
||||
|
||||
if docker-compose ps 2>/dev/null | grep -q "digiserver-nginx.*healthy"; then
|
||||
pass "Nginx container is healthy"
|
||||
|
||||
if $COMPOSE ps 2>/dev/null | grep -q "digiserver-caddy.*healthy"; then
|
||||
pass "Caddy container is healthy"
|
||||
else
|
||||
warn "Nginx container health status unknown"
|
||||
warn "Caddy container health status unknown"
|
||||
fi
|
||||
|
||||
# Probe the actual endpoints rather than trusting status alone.
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
HTTP_PORT="${HTTP_PORT:-80}"
|
||||
HTTPS_PORT="${HTTPS_PORT:-443}"
|
||||
|
||||
HTTP_URL="http://localhost"
|
||||
[ "$HTTP_PORT" != "80" ] && HTTP_URL="http://localhost:$HTTP_PORT"
|
||||
HTTPS_URL="https://localhost"
|
||||
[ "$HTTPS_PORT" != "443" ] && HTTPS_URL="https://localhost:$HTTPS_PORT"
|
||||
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -m 5 "$HTTP_URL/" 2>/dev/null || echo 000)
|
||||
if [ "$HTTP_CODE" != "000" ]; then
|
||||
pass "HTTP endpoint responds ($HTTP_URL, status $HTTP_CODE)"
|
||||
else
|
||||
warn "HTTP endpoint not responding ($HTTP_URL)"
|
||||
fi
|
||||
|
||||
# TLS site blocks match on the requested name (SNI) or on default_sni.
|
||||
# localhost matches neither when the server is configured for an IP /
|
||||
# intranet hostname, so probe the address the config actually serves.
|
||||
# NOTE: default_sni is indented inside the global block, so anchor on
|
||||
# optional leading whitespace, not `^`.
|
||||
PROBE_HOST=$(awk '/^[[:space:]]*default_sni/{print $2; exit}' data/Caddyfile 2>/dev/null || true)
|
||||
if [ -z "$PROBE_HOST" ]; then
|
||||
PROBE_HOST=$(awk '/^[[:space:]]*https:\/\//{sub(/^[[:space:]]*https:\/\//,""); sub(/[[:space:]{].*$/,""); print; exit}' data/Caddyfile 2>/dev/null || true)
|
||||
fi
|
||||
if [ -z "$PROBE_HOST" ]; then
|
||||
PROBE_HOST="localhost"
|
||||
fi
|
||||
|
||||
if curl -sk -o /dev/null -m 5 --resolve "$PROBE_HOST:$HTTPS_PORT:127.0.0.1" \
|
||||
"https://$PROBE_HOST:$HTTPS_PORT/" 2>/dev/null; then
|
||||
pass "HTTPS endpoint responds (https://$PROBE_HOST:$HTTPS_PORT)"
|
||||
elif grep -qE "tls internal|^[[:space:]]*https://" data/Caddyfile 2>/dev/null; then
|
||||
warn "HTTPS configured but not responding on https://$PROBE_HOST:$HTTPS_PORT"
|
||||
else
|
||||
info "HTTPS not configured (HTTP-only deployment)"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
info "Docker containers not running (will start on deployment)"
|
||||
@@ -307,11 +386,18 @@ else
|
||||
warn "Possible hardcoded secrets detected (verify they use os.getenv)"
|
||||
fi
|
||||
|
||||
# Check for debug mode
|
||||
if grep -q "DEBUG.*=.*True" app/config.py 2>/dev/null; then
|
||||
fail "DEBUG mode is enabled"
|
||||
# Check for debug mode. Only ProductionConfig matters — config.py intentionally
|
||||
# sets DEBUG=True for DevelopmentConfig and TestingConfig.
|
||||
# NOTE: grep exits 1 when it matches nothing; under `set -e` that would abort
|
||||
# the script, so the whole pipeline must end with a command that always succeeds.
|
||||
DEBUG_LINE=$(awk '/class ProductionConfig/,/^class |^# Configuration/' app/config.py 2>/dev/null \
|
||||
| grep -E "^[[:space:]]*DEBUG[[:space:]]*=[[:space:]]*True" || true)
|
||||
if [ -n "$DEBUG_LINE" ]; then
|
||||
fail "DEBUG mode is enabled in ProductionConfig"
|
||||
elif grep -q "class ProductionConfig" app/config.py 2>/dev/null; then
|
||||
pass "Debug mode disabled in ProductionConfig"
|
||||
else
|
||||
pass "DEBUG mode is disabled"
|
||||
warn "ProductionConfig class not found — cannot verify debug mode"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user