changemaker.lite/api/docker-entrypoint.sh
bunker-admin 08bd1f92b0 Add unified analytics system with GeoIP geo-tracking
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
2026-04-03 08:47:44 -06:00

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 "$@"