diff --git a/agents/social-listening/dashboard/server.ts b/agents/social-listening/dashboard/server.ts index 9ec9e9c..dbe9407 100644 --- a/agents/social-listening/dashboard/server.ts +++ b/agents/social-listening/dashboard/server.ts @@ -168,7 +168,54 @@ const server = createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } - // ─── Login routes ─── + // ─── Auth API (JSON-based, for static frontend) ─── + + if (url.pathname === '/api/auth' && req.method === 'GET') { + if (isAuthenticated(req)) { + sendJSON(res, 200, { ok: true }); + } else { + sendJSON(res, 401, { ok: false, error: 'Not authenticated' }); + } + return; + } + + if (url.pathname === '/api/login' && req.method === 'POST') { + const body = await parseBody(req); + let username = '', password = ''; + try { + const json = JSON.parse(body); + username = json.username || ''; + password = json.password || ''; + } catch { + const params = new URLSearchParams(body); + username = params.get('username') || ''; + password = params.get('password') || ''; + } + + if (username === DASH_USER && password === DASH_PASS) { + const payload = JSON.stringify({ user: username, exp: Date.now() + SESSION_MAX_AGE * 1000 }); + const token = signSession(payload); + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Set-Cookie': `sl_session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_MAX_AGE}`, + }); + res.end(JSON.stringify({ ok: true })); + } else { + sendJSON(res, 401, { ok: false, error: 'Invalid username or password' }); + } + return; + } + + if (url.pathname === '/api/logout' && req.method === 'GET') { + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Set-Cookie': 'sl_session=; Path=/; HttpOnly; Max-Age=0', + }); + res.end(JSON.stringify({ ok: true })); + return; + } + + // ─── Legacy form login (backward compat for standalone Docker mode) ─── if (url.pathname === '/login' && req.method === 'GET') { res.writeHead(200, { 'Content-Type': 'text/html' }); @@ -186,7 +233,7 @@ const server = createServer(async (req, res) => { const payload = JSON.stringify({ user: username, exp: Date.now() + SESSION_MAX_AGE * 1000 }); const token = signSession(payload); res.writeHead(302, { - 'Set-Cookie': `sl_session=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${SESSION_MAX_AGE}`, + 'Set-Cookie': `sl_session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_MAX_AGE}`, 'Location': '/', }); res.end(); @@ -208,8 +255,12 @@ const server = createServer(async (req, res) => { // ─── Auth gate (everything below requires login) ─── if (!isAuthenticated(req)) { - res.writeHead(302, { 'Location': '/login' }); - res.end(); + if (req.headers.accept?.includes('application/json') || url.pathname.startsWith('/api/')) { + sendJSON(res, 401, { error: 'Not authenticated' }); + } else { + res.writeHead(302, { 'Location': '/login' }); + res.end(); + } return; } diff --git a/deploy/apache-social-reports.conf b/deploy/apache-social-reports.conf new file mode 100644 index 0000000..daf2fc6 --- /dev/null +++ b/deploy/apache-social-reports.conf @@ -0,0 +1,59 @@ +# Social Reporting — Apache config +# Add this inside your existing VirtualHost for optical-dev.oliver.solutions +# or include it via: Include /opt/social-reporting/deploy/apache-social-reports.conf + +# Enable required modules (run once): +# sudo a2enmod proxy proxy_http proxy_wstunnel headers rewrite + +# ─── Static frontend ─── +Alias /social-reports /var/www/html/social-reporting + + Options -Indexes + AllowOverride None + Require all granted + + # SPA fallback — serve index.html for unknown paths + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^ /social-reports/index.html [L] + + +# ─── Proxy API + SSE + dynamic routes to Node backend ─── +ProxyPreserveHost On +ProxyTimeout 600 + +# Auth API +ProxyPass /social-reports/api/ http://127.0.0.1:3456/api/ +ProxyPassReverse /social-reports/api/ http://127.0.0.1:3456/api/ + +# SSE (long-lived connection — needs no buffering) +ProxyPass /social-reports/events http://127.0.0.1:3456/events +ProxyPassReverse /social-reports/events http://127.0.0.1:3456/events + + # Disable buffering for SSE + SetEnv proxy-sendcl 1 + SetEnv proxy-nokeepalive 1 + Header set Cache-Control "no-cache" + Header set X-Accel-Buffering "no" + + +# Pipeline run trigger +ProxyPass /social-reports/run http://127.0.0.1:3456/run +ProxyPassReverse /social-reports/run http://127.0.0.1:3456/run + +# Status check +ProxyPass /social-reports/status http://127.0.0.1:3456/status +ProxyPassReverse /social-reports/status http://127.0.0.1:3456/status + +# Legacy form login (standalone mode fallback) +ProxyPass /social-reports/login http://127.0.0.1:3456/login +ProxyPassReverse /social-reports/login http://127.0.0.1:3456/login + +# Legacy logout +ProxyPass /social-reports/logout http://127.0.0.1:3456/logout +ProxyPassReverse /social-reports/logout http://127.0.0.1:3456/logout + +# Report viewer +ProxyPassMatch ^/social-reports/report/(.*)$ http://127.0.0.1:3456/report/$1 +ProxyPassReverse /social-reports/report/ http://127.0.0.1:3456/report/ diff --git a/deploy/setup.sh b/deploy/setup.sh new file mode 100755 index 0000000..51ecdbd --- /dev/null +++ b/deploy/setup.sh @@ -0,0 +1,144 @@ +#!/bin/bash +set -euo pipefail + +# ═══════════════════════════════════════════════════════ +# Social Reporting — Server Deployment Script +# Target: Ubuntu + Apache + Docker +# URL: https://optical-dev.oliver.solutions/social-reports +# ═══════════════════════════════════════════════════════ + +REPO_URL="${REPO_URL:-}" # Set before running: export REPO_URL="https://x-token-auth:TOKEN@bitbucket.org/zlalani/social-reporting-tool.git" +BACKEND_DIR="/opt/social-reporting" +FRONTEND_DIR="/var/www/html/social-reporting" +APACHE_CONF="/etc/apache2/conf-available/social-reports.conf" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log() { echo -e "${GREEN}[+]${NC} $1"; } +warn() { echo -e "${YELLOW}[!]${NC} $1"; } +err() { echo -e "${RED}[x]${NC} $1"; exit 1; } + +# ─── Pre-checks ─── +[[ -z "$REPO_URL" ]] && err "REPO_URL not set. Run: export REPO_URL='https://x-token-auth:YOUR_TOKEN@bitbucket.org/zlalani/social-reporting-tool.git'" +command -v docker >/dev/null || err "Docker not installed" +command -v docker compose >/dev/null 2>&1 || command -v docker-compose >/dev/null || err "Docker Compose not installed" +command -v apache2ctl >/dev/null || err "Apache not installed" + +# ─── 1. Clone or pull repo ─── +if [[ -d "$BACKEND_DIR/.git" ]]; then + log "Updating existing repo at $BACKEND_DIR..." + cd "$BACKEND_DIR" + git remote set-url origin "$REPO_URL" + git pull origin main +else + log "Cloning repo to $BACKEND_DIR..." + sudo mkdir -p "$BACKEND_DIR" + sudo chown "$(whoami):$(whoami)" "$BACKEND_DIR" + git clone "$REPO_URL" "$BACKEND_DIR" +fi + +cd "$BACKEND_DIR" + +# ─── 2. Create .env if missing ─── +if [[ ! -f "$BACKEND_DIR/.env" ]]; then + warn ".env file not found — creating template" + cat > "$BACKEND_DIR/.env" << 'ENVEOF' +APIFY_TOKEN=your_apify_token_here +ANTHROPIC_API_KEY=your_anthropic_key_here +APIFY_LIVE_APPROVED=true +TEST_MODE=false +DASHBOARD_PORT=3456 +DATABASE_URL=postgresql://sl_user:sl_pass@db:5432/social_listening +APIFY_COST_LIMIT=5 +DASH_USER=admin +DASH_PASS=changeme +SESSION_SECRET= +ENVEOF + # Generate a random session secret + SESSION_SECRET=$(openssl rand -hex 32) + sed -i "s/^SESSION_SECRET=$/SESSION_SECRET=${SESSION_SECRET}/" "$BACKEND_DIR/.env" + warn "Edit $BACKEND_DIR/.env with your API keys and credentials!" + warn " APIFY_TOKEN, ANTHROPIC_API_KEY, DASH_USER, DASH_PASS" +fi + +# ─── 3. Deploy frontend ─── +log "Deploying frontend to $FRONTEND_DIR..." +sudo mkdir -p "$FRONTEND_DIR" +sudo cp "$BACKEND_DIR/frontend/index.html" "$FRONTEND_DIR/" +sudo cp "$BACKEND_DIR/frontend/login.html" "$FRONTEND_DIR/" +sudo cp "$BACKEND_DIR/frontend/config.js" "$FRONTEND_DIR/" +sudo chown -R www-data:www-data "$FRONTEND_DIR" +log "Frontend deployed: index.html, login.html, config.js" + +# ─── 4. Apache config ─── +log "Setting up Apache config..." +sudo cp "$BACKEND_DIR/deploy/apache-social-reports.conf" "$APACHE_CONF" + +# Enable required modules +for mod in proxy proxy_http headers rewrite; do + if ! apache2ctl -M 2>/dev/null | grep -q "${mod}_module"; then + log "Enabling Apache module: $mod" + sudo a2enmod "$mod" + fi +done + +# Enable the config +sudo a2enconf social-reports 2>/dev/null || true + +# Test Apache config +log "Testing Apache config..." +if sudo apache2ctl configtest 2>&1; then + log "Apache config OK" +else + err "Apache config test failed — check $APACHE_CONF" +fi + +# ─── 5. Docker Compose ─── +log "Starting Docker containers..." +cd "$BACKEND_DIR" + +# Use the correct docker compose command +if command -v "docker compose" >/dev/null 2>&1; then + COMPOSE="docker compose" +else + COMPOSE="docker-compose" +fi + +$COMPOSE -f docker-compose.yml -f docker-compose.prod.yml build +$COMPOSE -f docker-compose.yml -f docker-compose.prod.yml up -d + +# Wait for health +log "Waiting for services to be healthy..." +sleep 5 +if curl -sf http://127.0.0.1:3456/status > /dev/null 2>&1; then + log "Backend is running on port 3456" +else + warn "Backend not responding yet — check: $COMPOSE logs social-listening" +fi + +# ─── 6. Reload Apache ─── +log "Reloading Apache..." +sudo systemctl reload apache2 + +# ─── Done ─── +echo "" +echo "════════════════════════════════════════════════════" +echo -e "${GREEN} Deployment complete!${NC}" +echo "" +echo " Frontend: https://optical-dev.oliver.solutions/social-reports/" +echo " Backend: http://127.0.0.1:3456 (Docker)" +echo " Login: https://optical-dev.oliver.solutions/social-reports/login.html" +echo "" +echo " Backend dir: $BACKEND_DIR" +echo " Frontend dir: $FRONTEND_DIR" +echo " Apache conf: $APACHE_CONF" +echo "" +echo " To update later:" +echo " cd $BACKEND_DIR && git pull" +echo " $COMPOSE -f docker-compose.yml -f docker-compose.prod.yml build && $COMPOSE -f docker-compose.yml -f docker-compose.prod.yml up -d" +echo " sudo cp frontend/* $FRONTEND_DIR/ && sudo systemctl reload apache2" +echo "" +echo "════════════════════════════════════════════════════" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..5f499a3 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,13 @@ +# Production overrides — use with: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d +services: + db: + ports: + - "127.0.0.1:5435:5432" + restart: unless-stopped + + social-listening: + ports: + - "127.0.0.1:3456:3456" + restart: unless-stopped + environment: + - SESSION_SECRET=${SESSION_SECRET:-} diff --git a/frontend/config.js b/frontend/config.js new file mode 100644 index 0000000..2df75fe --- /dev/null +++ b/frontend/config.js @@ -0,0 +1,4 @@ +// ─── Frontend config (injected before app scripts) ─── +// API base points to the proxied backend path +window.__API_BASE = '/social-reports'; +window.__SSE_BASE = '/social-reports'; diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9062427 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,456 @@ + + + + + +Social Listening Pipeline + + + + +
+
+
+

Social Listening Pipeline

+

Automated social media research → client-ready reports

+
+Sign Out +
+ +
+
Pipeline
+
Run History
+
+ + +
+ +
+

Load Brief from JSON

+
+ + + No file selected +
+ +
+ +
+

Client Brief

+
+
+
+
+
+
+

Platforms

+
+ + + +
+

Influencers

+
+
+
+
+ + + + + + + + +
+ + +
+
Loading...
+
+ +
+ + + + + diff --git a/frontend/login.html b/frontend/login.html new file mode 100644 index 0000000..1bb86a1 --- /dev/null +++ b/frontend/login.html @@ -0,0 +1,69 @@ + + + + + +Login — Social Listening + + + + +
+

Social Listening

+
Sign in to access the dashboard
+
+
+
+
+ +
+
+ + + +