Full analytics platform with MaxMind GeoLite2 IP-to-location resolution, cross-module dashboard (docs, video, photo), user drill-down, volunteer self-service stats, and ANALYTICS_ADMIN role with feature flag controls. - ANALYTICS_ADMIN role + ANALYTICS_ROLES group across backend and frontend - GeoIP service (MaxMind GeoLite2, lazy-loaded, graceful degradation) - Geo fields (country, region, city, lat/lng) on DocsPageView, VideoView, PhotoView - IP resolved to geo before SHA-256 hashing (privacy-preserving) - Unified analytics module: overview, geo, content, user engagement endpoints - 4 admin dashboard pages: Overview, Geography (Leaflet map), Content, Users - Volunteer MyAnalyticsPage for self-service activity stats - Settings UI: enableAnalytics, analyticsGeoEnabled, trackAuthenticatedUsers, retentionDays - Scheduled cleanup job respecting configurable retention period - config.sh: Analytics + MaxMind prompt in configure_features() - Control panel: enableAnalytics flag, template, discovery, wizard, detail page - Docker: geoip volume mount, MaxMind env vars, entrypoint auto-download - Nginx: X-Forwarded-For fix ($proxy_add_x_forwarded_for) for real client IP - Express trust proxy set to 2 for Pangolin/Newt tunnel chain - CORS updated for docs origin (cmlite.org + docs.cmlite.org) - Lander page: added docs-analytics tracking snippet - Prisma migration: 20260402100000_add_analytics_system Bunker Admin
86 lines
3.5 KiB
Bash
Executable File
86 lines
3.5 KiB
Bash
Executable File
#!/bin/sh
|
|
set -e
|
|
|
|
# Block NODE_TLS_REJECT_UNAUTHORIZED=0 in production
|
|
if [ "$NODE_ENV" = "production" ] && [ "$NODE_TLS_REJECT_UNAUTHORIZED" = "0" ]; then
|
|
echo "FATAL: NODE_TLS_REJECT_UNAUTHORIZED=0 is not allowed in production"
|
|
exit 1
|
|
fi
|
|
|
|
# Wait for PostgreSQL to be ready before running migrations
|
|
echo "Waiting for database..."
|
|
MAX_WAIT=30
|
|
WAITED=0
|
|
until echo "SELECT 1" | npx prisma db execute --stdin --schema ./prisma/schema.prisma 2>/dev/null; do
|
|
sleep 2
|
|
WAITED=$((WAITED + 2))
|
|
if [ $WAITED -ge $MAX_WAIT ]; then
|
|
echo "FATAL: Database not available after ${MAX_WAIT}s"
|
|
exit 1
|
|
fi
|
|
done
|
|
echo "Database ready (${WAITED}s)"
|
|
|
|
# Run migrations — fail hard on error (never fall back to db push, which causes drift)
|
|
echo "Running Prisma migrations..."
|
|
npx prisma migrate deploy 2>&1
|
|
echo "Migrations complete."
|
|
|
|
echo "Running database seed..."
|
|
npx prisma db seed 2>&1 || echo "WARNING: Seed failed (non-fatal — seed.ts may require source files not present in production image)"
|
|
echo "Seed step done."
|
|
|
|
# Download MaxMind GeoLite2 database if credentials are provided and DB is missing/stale
|
|
if [ -n "$MAXMIND_ACCOUNT_ID" ] && [ -n "$MAXMIND_LICENSE_KEY" ]; then
|
|
GEOIP_DIR="/data/geoip"
|
|
GEOIP_DB="$GEOIP_DIR/GeoLite2-City.mmdb"
|
|
mkdir -p "$GEOIP_DIR" 2>/dev/null || true
|
|
# Re-download weekly (if file is older than 7 days or missing)
|
|
if [ ! -f "$GEOIP_DB" ] || [ "$(find "$GEOIP_DB" -mtime +7 2>/dev/null | wc -l)" -gt 0 ]; then
|
|
echo "Downloading MaxMind GeoLite2-City database..."
|
|
DOWNLOAD_URL="https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"
|
|
# Use Node.js for download (BusyBox wget leaks auth header on redirects)
|
|
if node -e "
|
|
const https = require('https');
|
|
const fs = require('fs');
|
|
const auth = Buffer.from('$MAXMIND_ACCOUNT_ID:$MAXMIND_LICENSE_KEY').toString('base64');
|
|
const get = (url, cb) => https.get(url, { headers: url.includes('maxmind.com') ? { Authorization: 'Basic ' + auth } : {} }, (res) => {
|
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) return get(res.headers.location, cb);
|
|
if (res.statusCode !== 200) { cb(new Error('HTTP ' + res.statusCode)); return; }
|
|
const out = fs.createWriteStream('/tmp/geolite2.tar.gz');
|
|
res.pipe(out);
|
|
out.on('close', () => cb(null));
|
|
}).on('error', cb);
|
|
get('$DOWNLOAD_URL', (err) => { if (err) { console.error(err.message); process.exit(1); } });
|
|
"; then
|
|
tar -xzf /tmp/geolite2.tar.gz -C /tmp/ 2>/dev/null
|
|
MMDB_FILE=$(find /tmp -name 'GeoLite2-City.mmdb' -type f 2>/dev/null | head -1)
|
|
if [ -n "$MMDB_FILE" ]; then
|
|
mv "$MMDB_FILE" "$GEOIP_DB"
|
|
echo "GeoLite2-City database updated."
|
|
else
|
|
echo "WARNING: Downloaded archive but no .mmdb file found (non-fatal)"
|
|
fi
|
|
rm -rf /tmp/geolite2.tar.gz /tmp/GeoLite2-City_* 2>/dev/null
|
|
else
|
|
echo "WARNING: Failed to download GeoLite2 database (non-fatal — geo features disabled)"
|
|
fi
|
|
else
|
|
echo "GeoLite2-City database is up to date."
|
|
fi
|
|
else
|
|
echo "MaxMind credentials not set — GeoIP lookup disabled."
|
|
fi
|
|
|
|
# If running production mode (node dist/server.js) and dist is stale, recompile
|
|
if [ -f "src/server.ts" ] && echo "$@" | grep -q "npm.*start\|node.*dist"; then
|
|
if [ ! -f "dist/server.js" ] || [ "src/server.ts" -nt "dist/server.js" ]; then
|
|
echo "Compiling TypeScript (dist/ is missing or stale)..."
|
|
npx tsc 2>&1 || echo "WARNING: TypeScript compilation had errors"
|
|
echo "Compilation complete."
|
|
fi
|
|
fi
|
|
|
|
echo "Starting server..."
|
|
exec "$@"
|