diff --git a/.opencode.json b/.opencode.json new file mode 100644 index 0000000..77107c7 --- /dev/null +++ b/.opencode.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://opencode.ai/config.json", + "model": "ollama/qwen3-coder:30b", + "instructions": [ + "Antworte immer auf Deutsch, unabhängig von der Sprache der Eingabe." + ], + "provider": { + "ollama": { + "npm": "@ai-sdk/openai-compatible", + "name": "Ollama", + "options": { + "baseURL": "http://192.168.0.179:11434/v1" + }, + "models": { + "qwen3-coder:30b": { + "name": "qwen3-coder:30b", + "tools": true + } + } + } + } +} diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..25b49f9 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -0,0 +1,511 @@ +# BotKonzept Installer JSON API Documentation + +## Übersicht + +Diese API stellt die Installer-JSON-Daten sicher für Frontend-Clients bereit, **ohne Secrets preiszugeben**. + +**Basis-URL:** `http://192.168.45.104:3000` (PostgREST auf Kunden-LXC) +**Zentrale API:** `https://api.botkonzept.de` (zentrales PostgREST/n8n) + +--- + +## Sicherheitsmodell + +### ✅ Erlaubte Daten (Frontend-sicher) + +- `ctid`, `hostname`, `fqdn`, `ip`, `vlan` +- `urls.*` (alle URL-Endpunkte) +- `supabase.url_external` +- `supabase.anon_key` +- `ollama.url`, `ollama.model`, `ollama.embedding_model` + +### ❌ Verbotene Daten (Secrets) + +- `postgres.password` +- `supabase.service_role_key` +- `supabase.jwt_secret` +- `n8n.owner_password` +- `n8n.encryption_key` + +--- + +## API-Endpunkte + +### 1. Public Config (Keine Authentifizierung) + +**Zweck:** Liefert öffentliche Konfiguration für Website (Registrierungs-Webhook) + +**Route:** `POST /rpc/get_public_config` + +**Request:** +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_public_config' \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +**Response (Success):** +```json +{ + "registration_webhook_url": "https://api.botkonzept.de/webhook/botkonzept-registration", + "api_base_url": "https://api.botkonzept.de" +} +``` + +**Response (Error):** +```json +{ + "code": "PGRST204", + "message": "No rows returned", + "details": null, + "hint": null +} +``` + +**CORS:** Erlaubt (öffentlich) + +--- + +### 2. Instance Config by Email (Öffentlich, aber rate-limited) + +**Zweck:** Liefert Instanz-Konfiguration für einen Kunden (via E-Mail) + +**Route:** `POST /rpc/get_instance_config_by_email` + +**Request:** +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_email' \ + -H "Content-Type: application/json" \ + -d '{"customer_email_param": "max@beispiel.de"}' +``` + +**Response (Success):** +```json +[ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "customer_id": "123e4567-e89b-12d3-a456-426614174000", + "ctid": 769697636, + "hostname": "sb-1769697636", + "fqdn": "sb-1769697636.userman.de", + "ip": "192.168.45.104", + "vlan": 90, + "status": "active", + "created_at": "2025-01-15T10:30:00Z", + "urls": { + "n8n_internal": "http://192.168.45.104:5678/", + "n8n_external": "https://sb-1769697636.userman.de", + "postgrest": "http://192.168.45.104:3000", + "chat_webhook": "https://sb-1769697636.userman.de/webhook/rag-chat-webhook/chat", + "chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat", + "upload_form": "https://sb-1769697636.userman.de/form/rag-upload-form", + "upload_form_internal": "http://192.168.45.104:5678/form/rag-upload-form" + }, + "supabase": { + "url_external": "http://192.168.45.104:3000", + "anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "ollama": { + "url": "http://192.168.45.3:11434", + "model": "ministral-3:3b", + "embedding_model": "nomic-embed-text:latest" + }, + "customer_email": "max@beispiel.de", + "first_name": "Max", + "last_name": "Mustermann", + "company": "Muster GmbH", + "customer_status": "trial" + } +] +``` + +**Response (Not Found):** +```json +[] +``` + +**Response (Error):** +```json +{ + "code": "PGRST301", + "message": "Invalid input syntax", + "details": "...", + "hint": null +} +``` + +**Authentifizierung:** Keine (öffentlich, aber sollte rate-limited sein) + +**CORS:** Erlaubt + +--- + +### 3. Instance Config by CTID (Service Role Only) + +**Zweck:** Liefert Instanz-Konfiguration für interne Workflows (via CTID) + +**Route:** `POST /rpc/get_instance_config_by_ctid` + +**Request:** +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_ctid' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{"ctid_param": 769697636}' +``` + +**Response:** Gleiche Struktur wie `/get_instance_config_by_email` + +**Authentifizierung:** Service Role Key erforderlich + +**CORS:** Nicht erlaubt (nur Backend-to-Backend) + +--- + +### 4. Store Installer JSON (Service Role Only) + +**Zweck:** Speichert Installer-JSON nach Instanz-Erstellung (wird von install.sh aufgerufen) + +**Route:** `POST /rpc/store_installer_json` + +**Request:** +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/store_installer_json' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{ + "customer_email_param": "max@beispiel.de", + "lxc_id_param": 769697636, + "installer_json_param": { + "ctid": 769697636, + "hostname": "sb-1769697636", + "fqdn": "sb-1769697636.userman.de", + "ip": "192.168.45.104", + "vlan": 90, + "urls": { + "n8n_internal": "http://192.168.45.104:5678/", + "n8n_external": "https://sb-1769697636.userman.de", + "postgrest": "http://192.168.45.104:3000", + "chat_webhook": "https://sb-1769697636.userman.de/webhook/rag-chat-webhook/chat", + "chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat", + "upload_form": "https://sb-1769697636.userman.de/form/rag-upload-form", + "upload_form_internal": "http://192.168.45.104:5678/form/rag-upload-form" + }, + "postgres": { + "host": "postgres", + "port": 5432, + "db": "customer", + "user": "customer", + "password": "REDACTED" + }, + "supabase": { + "url": "http://postgrest:3000", + "url_external": "http://192.168.45.104:3000", + "anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "service_role_key": "REDACTED", + "jwt_secret": "REDACTED" + }, + "ollama": { + "url": "http://192.168.45.3:11434", + "model": "ministral-3:3b", + "embedding_model": "nomic-embed-text:latest" + }, + "n8n": { + "encryption_key": "REDACTED", + "owner_email": "admin@userman.de", + "owner_password": "REDACTED", + "secure_cookie": false + } + } + }' +``` + +**Response (Success):** +```json +{ + "success": true, + "instance_id": "550e8400-e29b-41d4-a716-446655440000", + "customer_id": "123e4567-e89b-12d3-a456-426614174000", + "message": "Installer JSON stored successfully" +} +``` + +**Response (Error):** +```json +{ + "success": false, + "error": "Instance not found for customer email and LXC ID" +} +``` + +**Authentifizierung:** Service Role Key erforderlich + +**CORS:** Nicht erlaubt (nur Backend-to-Backend) + +--- + +### 5. Direct View Access (Authenticated) + +**Zweck:** Direkter Zugriff auf View (für authentifizierte Benutzer) + +**Route:** `GET /api/instance_config` + +**Request:** +```bash +curl -X GET 'http://192.168.45.104:3000/api/instance_config' \ + -H "Authorization: Bearer " +``` + +**Response:** Array von Instanz-Konfigurationen (gefiltert nach RLS) + +**Authentifizierung:** JWT Token erforderlich (Supabase Auth) + +**CORS:** Erlaubt + +--- + +## Authentifizierung + +### 1. Keine Authentifizierung (Public) + +- `/rpc/get_public_config` +- `/rpc/get_instance_config_by_email` (sollte rate-limited sein) + +### 2. Service Role Key + +**Header:** +``` +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MjAwMDAwMDAwMH0... +``` + +**Verwendung:** +- `/rpc/get_instance_config_by_ctid` +- `/rpc/store_installer_json` + +### 3. User JWT Token (Supabase Auth) + +**Header:** +``` +Authorization: Bearer +``` + +**Verwendung:** +- `/api/instance_config` (direkter View-Zugriff) + +--- + +## CORS-Konfiguration + +### PostgREST CORS Headers + +In der PostgREST-Konfiguration (docker-compose.yml): + +```yaml +postgrest: + environment: + PGRST_SERVER_CORS_ALLOWED_ORIGINS: "*" + # Oder spezifisch: + # PGRST_SERVER_CORS_ALLOWED_ORIGINS: "https://botkonzept.de,https://www.botkonzept.de" +``` + +### Nginx Reverse Proxy CORS + +Falls über Nginx: + +```nginx +add_header 'Access-Control-Allow-Origin' '*'; +add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; +add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization'; +``` + +--- + +## Rate Limiting + +**Empfehlung:** Rate Limiting für öffentliche Endpunkte implementieren + +### Nginx Rate Limiting + +```nginx +limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; + +location /rpc/get_instance_config_by_email { + limit_req zone=api_limit burst=20 nodelay; + proxy_pass http://postgrest:3000; +} +``` + +### PostgREST Rate Limiting + +Alternativ: Verwende einen API Gateway (Kong, Tyk) vor PostgREST. + +--- + +## Fehlerbehandlung + +### HTTP Status Codes + +- `200 OK` - Erfolgreiche Anfrage +- `204 No Content` - Keine Daten gefunden (PostgREST) +- `400 Bad Request` - Ungültige Eingabe +- `401 Unauthorized` - Fehlende/ungültige Authentifizierung +- `403 Forbidden` - Keine Berechtigung +- `404 Not Found` - Ressource nicht gefunden +- `500 Internal Server Error` - Serverfehler + +### PostgREST Error Format + +```json +{ + "code": "PGRST301", + "message": "Invalid input syntax for type integer", + "details": "invalid input syntax for type integer: \"abc\"", + "hint": null +} +``` + +--- + +## Integration mit install.sh + +### Schritt 1: SQL-Schema anwenden + +```bash +# Auf dem Proxmox Host +pct exec -- bash -c " + docker exec customer-postgres psql -U customer -d customer < /opt/customer-stack/sql/add_installer_json_api.sql +" +``` + +### Schritt 2: install.sh erweitern + +Am Ende von `install.sh` (nach JSON-Generierung): + +```bash +# Store installer JSON in database via PostgREST +info "Storing installer JSON in database..." + +STORE_RESPONSE=$(curl -sS -X POST "http://${CT_IP}:${POSTGREST_PORT}/rpc/store_installer_json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SERVICE_ROLE_KEY}" \ + -d "{ + \"customer_email_param\": \"${N8N_OWNER_EMAIL}\", + \"lxc_id_param\": ${CTID}, + \"installer_json_param\": ${JSON_OUTPUT} + }" 2>&1) + +if echo "$STORE_RESPONSE" | grep -q '"success":true'; then + info "Installer JSON stored successfully" +else + warn "Failed to store installer JSON: ${STORE_RESPONSE}" +fi +``` + +--- + +## Testing + +### Test 1: Public Config + +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_public_config' \ + -H "Content-Type: application/json" \ + -d '{}' + +# Erwartete Antwort: +# {"registration_webhook_url":"https://api.botkonzept.de/webhook/botkonzept-registration","api_base_url":"https://api.botkonzept.de"} +``` + +### Test 2: Instance Config by Email + +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_email' \ + -H "Content-Type: application/json" \ + -d '{"customer_email_param": "max@beispiel.de"}' + +# Erwartete Antwort: Array mit Instanz-Konfiguration (siehe oben) +``` + +### Test 3: Store Installer JSON (mit Service Role Key) + +```bash +SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + +curl -X POST 'http://192.168.45.104:3000/rpc/store_installer_json' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SERVICE_ROLE_KEY}" \ + -d '{ + "customer_email_param": "max@beispiel.de", + "lxc_id_param": 769697636, + "installer_json_param": {"ctid": 769697636, "urls": {...}} + }' + +# Erwartete Antwort: +# {"success":true,"instance_id":"...","customer_id":"...","message":"Installer JSON stored successfully"} +``` + +### Test 4: Verify No Secrets Exposed + +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_email' \ + -H "Content-Type: application/json" \ + -d '{"customer_email_param": "max@beispiel.de"}' | jq . + +# Prüfe: Response enthält KEINE der folgenden Felder: +# - postgres.password +# - supabase.service_role_key +# - supabase.jwt_secret +# - n8n.owner_password +# - n8n.encryption_key +``` + +--- + +## Deployment Checklist + +- [ ] SQL-Schema auf allen Instanzen anwenden +- [ ] PostgREST CORS konfigurieren +- [ ] Rate Limiting aktivieren +- [ ] install.sh erweitern (Installer JSON speichern) +- [ ] Frontend auf neue API umstellen +- [ ] Tests durchführen +- [ ] Monitoring einrichten (API-Zugriffe loggen) + +--- + +## Monitoring & Logging + +### Audit Log + +Alle API-Zugriffe werden in `audit_log` Tabelle protokolliert: + +```sql +SELECT * FROM audit_log +WHERE action = 'api_config_access' +ORDER BY created_at DESC +LIMIT 10; +``` + +### PostgREST Logs + +```bash +docker logs customer-postgrest --tail 100 -f +``` + +--- + +## Sicherheitshinweise + +1. **Service Role Key schützen:** Niemals im Frontend verwenden! +2. **Rate Limiting:** Öffentliche Endpunkte müssen rate-limited sein +3. **HTTPS:** In Produktion nur über HTTPS (OPNsense Reverse Proxy) +4. **Input Validation:** PostgREST validiert automatisch, aber zusätzliche Checks empfohlen +5. **Audit Logging:** Alle API-Zugriffe werden geloggt + +--- + +## Support + +Bei Fragen oder Problemen: +- Dokumentation: `customer-installer/wiki/` +- Troubleshooting: `customer-installer/REGISTRATION_TROUBLESHOOTING.md` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4cf6561 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,103 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Automates provisioning of customer Proxmox LXC containers running a Docker stack (n8n + PostgreSQL/pgvector + PostgREST) with automatic OPNsense NGINX reverse proxy registration. Intended for a multi-tenant SaaS setup ("BotKonzept") where each customer gets an isolated container. + +## Key Commands + +```bash +# Create a new customer LXC (must run on Proxmox host) +bash install.sh --storage local-zfs --bridge vmbr0 --ip dhcp --vlan 90 + +# With debug output (logs on stderr instead of only to file) +DEBUG=1 bash install.sh --storage local-zfs --bridge vmbr0 + +# With APT caching proxy +bash install.sh --storage local-zfs --apt-proxy http://192.168.45.2:3142 + +# Setup the BotKonzept management LXC (fixed CTID 5010) +bash setup_botkonzept_lxc.sh + +# Delete an nginx proxy entry in OPNsense +bash delete_nginx_proxy.sh --hostname sb- +``` + +`install.sh` outputs a single JSON line to stdout with all credentials and URLs. Detailed logs go to `logs/.log`. Credentials are saved to `credentials/.json`. + +## Architecture + +### Script Dependency Tree + +``` +install.sh +├── sources libsupabase.sh (Proxmox helpers, logging, crypto, n8n setup) +├── calls setup_nginx_proxy.sh (OPNsense API integration) +└── uses lib_installer_json_api.sh (PostgREST DB storage - optional) + +setup_botkonzept_lxc.sh (Standalone, for management LXC CTID 5010) +``` + +### Infrastructure Assumptions (hardcoded defaults) + +| Service | Address | +|---|---| +| OPNsense Firewall | `192.168.45.1:4444` | +| Apt-Cacher NG | `192.168.45.2:3142` | +| Docker Registry Mirror | `192.168.45.2:5000` | +| Ollama API | `192.168.45.3:11434` | +| Default VLAN | 90 | +| Default storage | `local-zfs` | +| Default base domain | `userman.de` | + +### What `install.sh` Does (Steps 5–11) + +1. **Step 5**: Creates and starts Proxmox LXC (Debian 12), waits for DHCP IP +2. **Step 6**: Installs Docker CE + Compose plugin inside the CT +3. **Step 7**: Generates secrets (PG password, JWT, n8n encryption key), writes `.env` and `docker-compose.yml` into CT, starts the stack +4. **Step 8**: Creates n8n owner account via REST API +5. **Step 10**: Imports and activates the RAG workflow via n8n API, sets up credentials (Postgres + Ollama) +6. **Step 10a**: Installs a systemd service (`n8n-workflow-reload.service`) that re-imports and re-activates the workflow on every LXC restart +7. **Step 11**: Registers an NGINX upstream/location in OPNsense via its REST API + +### Docker Stack Inside Each LXC (`/opt/customer-stack/`) + +- `postgres` – pgvector/pgvector:pg16, initialized from `sql/` directory +- `postgrest` – PostgREST, exposes Supabase-compatible REST API on port 3000 (mapped to `POSTGREST_PORT`) +- `n8n` – n8n automation, port 5678 + +All three share a `customer-net` bridge network. The n8n instance connects to PostgREST via the Docker internal hostname `postgrest:3000` (not the external IP). + +### Key Files + +| File | Purpose | +|---|---| +| `libsupabase.sh` | Core library: logging (`info`/`warn`/`die`), Proxmox helpers (`pct_exec`, `pct_push_text`, `pve_*`), crypto (`gen_password_policy`, `gen_hex_64`), n8n setup (`n8n_setup_rag_workflow`) | +| `setup_nginx_proxy.sh` | OPNsense API client; registers upstream + location for new CT | +| `lib_installer_json_api.sh` | Stores installer JSON output into the BotKonzept Postgres DB via PostgREST | +| `sql/botkonzept_schema.sql` | Customer management schema (customers, instances, emails, payments) for the BotKonzept management LXC | +| `sql/init_pgvector.sql` | Inline in `install.sh`; creates pgvector extension, `documents` table, `match_documents` function, PostgREST roles | +| `templates/reload-workflow.sh` | Runs inside customer LXC on every restart; logs to `/opt/customer-stack/logs/workflow-reload.log` | +| `RAGKI-BotPGVector.json` | Default n8n workflow template (RAG KI-Bot with PGVector) | + +### Output and Logging + +- **Normal mode** (`DEBUG=0`): all script output goes to `logs/.log`; only the final JSON is printed to stdout (via fd 3) +- **Debug mode** (`DEBUG=1`): logs also written to stderr; JSON is formatted with `python3 -m json.tool` +- Each customer container hostname is `sb-`; CTID = unix_timestamp − 1,000,000,000 + +### n8n Password Policy + +Passwords must be 8+ characters with at least 1 uppercase and 1 number. Enforced by `password_policy_check` in `libsupabase.sh`. Auto-generated passwords use `gen_password_policy`. + +### Workflow Auto-Reload + +On LXC restart, `n8n-workflow-reload.service` runs `reload-workflow.sh`, which: +1. Waits for n8n API to be ready (up to 60s) +2. Logs in with owner credentials from `.env` +3. Deletes the existing "RAG KI-Bot (PGVector)" workflow +4. Looks up existing Postgres and Ollama credential IDs +5. Processes the workflow template (replaces credential IDs using Python) +6. Imports and activates the new workflow diff --git a/STEP1_BACKEND_API_SUMMARY.md b/STEP1_BACKEND_API_SUMMARY.md new file mode 100644 index 0000000..e6faebe --- /dev/null +++ b/STEP1_BACKEND_API_SUMMARY.md @@ -0,0 +1,428 @@ +# Schritt 1: Backend-API für Installer-JSON - ABGESCHLOSSEN + +## Zusammenfassung + +Backend-API wurde erfolgreich erstellt, die das Installer-JSON sicher (ohne Secrets) für Frontend-Clients bereitstellt. + +--- + +## Erstellte Dateien + +### 1. SQL-Schema: `sql/add_installer_json_api.sql` + +**Funktionen:** +- Erweitert `instances` Tabelle um `installer_json` JSONB-Spalte +- Erstellt `api.instance_config` View (filtert Secrets automatisch) +- Implementiert Row Level Security (RLS) +- Bietet 5 API-Funktionen: + - `get_public_config()` - Öffentliche Konfiguration + - `get_instance_config_by_email(email)` - Instanz-Config per E-Mail + - `get_instance_config_by_ctid(ctid)` - Instanz-Config per CTID (service_role only) + - `store_installer_json(email, ctid, json)` - Speichert Installer-JSON (service_role only) + - `log_config_access(customer_id, type, ip)` - Audit-Logging + +**Sicherheit:** +- ✅ Filtert automatisch alle Secrets (postgres.password, service_role_key, jwt_secret, etc.) +- ✅ Row Level Security aktiviert +- ✅ Audit-Logging für alle Zugriffe + +--- + +### 2. API-Dokumentation: `API_DOCUMENTATION.md` + +**Inhalt:** +- Vollständige API-Referenz +- Alle Endpunkte mit Beispielen +- Authentifizierungs-Modelle +- CORS-Konfiguration +- Rate-Limiting-Empfehlungen +- Fehlerbehandlung +- Integration mit install.sh +- Test-Szenarien + +--- + +### 3. Integration-Library: `lib_installer_json_api.sh` + +**Funktionen:** +- `store_installer_json_in_db()` - Speichert JSON in DB +- `get_installer_json_by_email()` - Ruft JSON per E-Mail ab +- `get_installer_json_by_ctid()` - Ruft JSON per CTID ab +- `get_public_config()` - Ruft öffentliche Config ab +- `apply_installer_json_api_schema()` - Wendet SQL-Schema an +- `test_api_connectivity()` - Testet API-Verbindung +- `verify_installer_json_stored()` - Verifiziert Speicherung + +--- + +### 4. Test-Script: `test_installer_json_api.sh` + +**Tests:** +- API-Konnektivität +- Public Config Endpoint +- Instance Config by Email +- Instance Config by CTID +- Store Installer JSON +- CORS Headers +- Response Format Validation +- Security: Verifiziert, dass keine Secrets exposed werden + +**Usage:** +```bash +# Basis-Tests (öffentliche Endpunkte) +bash test_installer_json_api.sh + +# Vollständige Tests (mit Service Role Key) +bash test_installer_json_api.sh --service-role-key "eyJhbGc..." + +# Spezifische Instanz testen +bash test_installer_json_api.sh \ + --ctid 769697636 \ + --email max@beispiel.de \ + --postgrest-url http://192.168.45.104:3000 +``` + +--- + +## API-Routen (PostgREST) + +### 1. Public Config (Keine Auth) + +**URL:** `POST /rpc/get_public_config` + +**Request:** +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_public_config' \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +**Response:** +```json +{ + "registration_webhook_url": "https://api.botkonzept.de/webhook/botkonzept-registration", + "api_base_url": "https://api.botkonzept.de" +} +``` + +--- + +### 2. Instance Config by Email (Öffentlich) + +**URL:** `POST /rpc/get_instance_config_by_email` + +**Request:** +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_email' \ + -H "Content-Type: application/json" \ + -d '{"customer_email_param": "max@beispiel.de"}' +``` + +**Response:** +```json +[ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "customer_id": "123e4567-e89b-12d3-a456-426614174000", + "ctid": 769697636, + "hostname": "sb-1769697636", + "fqdn": "sb-1769697636.userman.de", + "ip": "192.168.45.104", + "vlan": 90, + "status": "active", + "created_at": "2025-01-15T10:30:00Z", + "urls": { + "n8n_internal": "http://192.168.45.104:5678/", + "n8n_external": "https://sb-1769697636.userman.de", + "postgrest": "http://192.168.45.104:3000", + "chat_webhook": "https://sb-1769697636.userman.de/webhook/rag-chat-webhook/chat", + "chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat", + "upload_form": "https://sb-1769697636.userman.de/form/rag-upload-form", + "upload_form_internal": "http://192.168.45.104:5678/form/rag-upload-form" + }, + "supabase": { + "url_external": "http://192.168.45.104:3000", + "anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "ollama": { + "url": "http://192.168.45.3:11434", + "model": "ministral-3:3b", + "embedding_model": "nomic-embed-text:latest" + }, + "customer_email": "max@beispiel.de", + "first_name": "Max", + "last_name": "Mustermann", + "company": "Muster GmbH", + "customer_status": "trial" + } +] +``` + +**Wichtig:** Keine Secrets (passwords, service_role_key, jwt_secret) im Response! + +--- + +### 3. Store Installer JSON (Service Role Only) + +**URL:** `POST /rpc/store_installer_json` + +**Request:** +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/store_installer_json' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{ + "customer_email_param": "max@beispiel.de", + "lxc_id_param": 769697636, + "installer_json_param": {...} + }' +``` + +**Response:** +```json +{ + "success": true, + "instance_id": "550e8400-e29b-41d4-a716-446655440000", + "customer_id": "123e4567-e89b-12d3-a456-426614174000", + "message": "Installer JSON stored successfully" +} +``` + +--- + +## Sicherheits-Whitelist + +### ✅ Erlaubt (Frontend-sicher) + +```json +{ + "ctid": 769697636, + "hostname": "sb-1769697636", + "fqdn": "sb-1769697636.userman.de", + "ip": "192.168.45.104", + "vlan": 90, + "urls": { + "n8n_internal": "http://192.168.45.104:5678/", + "n8n_external": "https://sb-1769697636.userman.de", + "postgrest": "http://192.168.45.104:3000", + "chat_webhook": "https://sb-1769697636.userman.de/webhook/rag-chat-webhook/chat", + "chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat", + "upload_form": "https://sb-1769697636.userman.de/form/rag-upload-form", + "upload_form_internal": "http://192.168.45.104:5678/form/rag-upload-form" + }, + "supabase": { + "url_external": "http://192.168.45.104:3000", + "anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "ollama": { + "url": "http://192.168.45.3:11434", + "model": "ministral-3:3b", + "embedding_model": "nomic-embed-text:latest" + } +} +``` + +### ❌ Verboten (Secrets) + +```json +{ + "postgres": { + "password": "NEVER_EXPOSE" + }, + "supabase": { + "service_role_key": "NEVER_EXPOSE", + "jwt_secret": "NEVER_EXPOSE" + }, + "n8n": { + "owner_password": "NEVER_EXPOSE", + "encryption_key": "NEVER_EXPOSE" + } +} +``` + +--- + +## Authentifizierung + +### 1. Keine Authentifizierung (Public) + +- `/rpc/get_public_config` +- `/rpc/get_instance_config_by_email` + +**Empfehlung:** Rate Limiting aktivieren! + +### 2. Service Role Key (Backend-to-Backend) + +**Header:** +``` +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MjAwMDAwMDAwMH0... +``` + +**Verwendung:** +- `/rpc/get_instance_config_by_ctid` +- `/rpc/store_installer_json` + +--- + +## Deployment-Schritte + +### Schritt 1: SQL-Schema anwenden + +```bash +# Auf bestehendem Container +CTID=769697636 + +pct exec ${CTID} -- bash -c " + docker exec customer-postgres psql -U customer -d customer < /opt/customer-stack/sql/add_installer_json_api.sql +" +``` + +### Schritt 2: Test ausführen + +```bash +# Basis-Test +bash customer-installer/test_installer_json_api.sh \ + --postgrest-url http://192.168.45.104:3000 + +# Mit Service Role Key +bash customer-installer/test_installer_json_api.sh \ + --postgrest-url http://192.168.45.104:3000 \ + --service-role-key "eyJhbGc..." +``` + +### Schritt 3: install.sh erweitern (nächster Schritt) + +Am Ende von `install.sh` hinzufügen: + +```bash +# Source API library +source "${SCRIPT_DIR}/lib_installer_json_api.sh" + +# Apply SQL schema +apply_installer_json_api_schema "${CTID}" + +# Store installer JSON in database +store_installer_json_in_db \ + "${CTID}" \ + "${N8N_OWNER_EMAIL}" \ + "${SUPABASE_URL_EXTERNAL}" \ + "${SERVICE_ROLE_KEY}" \ + "${JSON_OUTPUT}" + +# Verify storage +verify_installer_json_stored \ + "${CTID}" \ + "${N8N_OWNER_EMAIL}" \ + "${SUPABASE_URL_EXTERNAL}" +``` + +--- + +## Curl-Tests + +### Test 1: Public Config + +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_public_config' \ + -H "Content-Type: application/json" \ + -d '{}' + +# Erwartete Antwort: +# {"registration_webhook_url":"https://api.botkonzept.de/webhook/botkonzept-registration","api_base_url":"https://api.botkonzept.de"} +``` + +### Test 2: Instance Config by Email + +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_email' \ + -H "Content-Type: application/json" \ + -d '{"customer_email_param": "max@beispiel.de"}' + +# Erwartete Antwort: Array mit Instanz-Config (siehe oben) +``` + +### Test 3: Verify No Secrets + +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_email' \ + -H "Content-Type: application/json" \ + -d '{"customer_email_param": "max@beispiel.de"}' | jq . + +# Prüfe: Response enthält KEINE der folgenden Strings: +# - "password" +# - "service_role_key" +# - "jwt_secret" +# - "encryption_key" +# - "owner_password" +``` + +### Test 4: Store Installer JSON (mit Service Role Key) + +```bash +SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + +curl -X POST 'http://192.168.45.104:3000/rpc/store_installer_json' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SERVICE_ROLE_KEY}" \ + -d '{ + "customer_email_param": "max@beispiel.de", + "lxc_id_param": 769697636, + "installer_json_param": { + "ctid": 769697636, + "urls": {...}, + "postgres": {"password": "secret"}, + "supabase": {"service_role_key": "secret"} + } + }' + +# Erwartete Antwort: +# {"success":true,"instance_id":"...","customer_id":"...","message":"Installer JSON stored successfully"} +``` + +--- + +## Nächste Schritte (Schritt 2) + +1. **Frontend-Integration:** + - `customer-frontend/js/main.js` anpassen + - `customer-frontend/js/dashboard.js` anpassen + - Dynamisches Laden der URLs aus API + +2. **install.sh erweitern:** + - SQL-Schema automatisch anwenden + - Installer-JSON automatisch speichern + - Verifizierung nach Speicherung + +3. **CORS konfigurieren:** + - PostgREST CORS Headers setzen + - Nginx Reverse Proxy CORS konfigurieren + +4. **Rate Limiting:** + - Nginx Rate Limiting für öffentliche Endpunkte + - Oder API Gateway (Kong, Tyk) verwenden + +--- + +## Status + +✅ **Schritt 1 ABGESCHLOSSEN** + +**Erstellt:** +- ✅ SQL-Schema mit sicherer API-View +- ✅ API-Dokumentation +- ✅ Integration-Library +- ✅ Test-Script + +**Bereit für:** +- ⏭️ Schritt 2: Frontend-Integration +- ⏭️ Schritt 3: install.sh erweitern +- ⏭️ Schritt 4: E2E-Tests + +--- + +## Support + +- **API-Dokumentation:** `customer-installer/API_DOCUMENTATION.md` +- **Test-Script:** `customer-installer/test_installer_json_api.sh` +- **Integration-Library:** `customer-installer/lib_installer_json_api.sh` +- **SQL-Schema:** `customer-installer/sql/add_installer_json_api.sql` diff --git a/SUPABASE_AUTH_API_TESTS.md b/SUPABASE_AUTH_API_TESTS.md new file mode 100644 index 0000000..67d0393 --- /dev/null +++ b/SUPABASE_AUTH_API_TESTS.md @@ -0,0 +1,467 @@ +# Supabase Auth API - Tests & Examples + +## Übersicht + +Diese API verwendet **Supabase Auth JWT Tokens** für Authentifizierung. +**NIEMALS Service Role Key im Frontend verwenden!** + +--- + +## Test 1: Unauthenticated Request (muss 401/403 geben) + +### Request (ohne Auth Token) + +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_my_instance_config' \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +### Expected Response (401 Unauthorized) + +```json +{ + "code": "PGRST301", + "message": "Not authenticated", + "details": null, + "hint": null +} +``` + +**Status:** ✅ PASS - Unauthenticated requests are blocked + +--- + +## Test 2: Authenticated Request (muss 200 + Whitelist geben) + +### Step 1: Get JWT Token (Supabase Auth) + +```bash +# Login via Supabase Auth +curl -X POST 'http://192.168.45.104:3000/auth/v1/token?grant_type=password' \ + -H "Content-Type: application/json" \ + -H "apikey: " \ + -d '{ + "email": "max@beispiel.de", + "password": "SecurePassword123!" + }' +``` + +**Response:** +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzM3MDM2MDAwLCJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAiLCJlbWFpbCI6Im1heEBiZWlzcGllbC5kZSIsInJvbGUiOiJhdXRoZW50aWNhdGVkIn0...", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "...", + "user": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "email": "max@beispiel.de", + ... + } +} +``` + +### Step 2: Get Instance Config (with JWT) + +```bash +JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + +curl -X POST 'http://192.168.45.104:3000/rpc/get_my_instance_config' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${JWT_TOKEN}" \ + -d '{}' +``` + +### Expected Response (200 OK + Whitelist) + +```json +[ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "customer_id": "123e4567-e89b-12d3-a456-426614174000", + "owner_user_id": "550e8400-e29b-41d4-a716-446655440000", + "ctid": 769697636, + "hostname": "sb-1769697636", + "fqdn": "sb-1769697636.userman.de", + "ip": "192.168.45.104", + "vlan": 90, + "status": "active", + "created_at": "2025-01-15T10:30:00Z", + "urls": { + "n8n_internal": "http://192.168.45.104:5678/", + "n8n_external": "https://sb-1769697636.userman.de", + "postgrest": "http://192.168.45.104:3000", + "chat_webhook": "https://sb-1769697636.userman.de/webhook/rag-chat-webhook/chat", + "chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat", + "upload_form": "https://sb-1769697636.userman.de/form/rag-upload-form", + "upload_form_internal": "http://192.168.45.104:5678/form/rag-upload-form" + }, + "supabase": { + "url_external": "http://192.168.45.104:3000", + "anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjIwMDAwMDAwMDB9..." + }, + "ollama": { + "url": "http://192.168.45.3:11434", + "model": "ministral-3:3b", + "embedding_model": "nomic-embed-text:latest" + }, + "customer_email": "max@beispiel.de", + "first_name": "Max", + "last_name": "Mustermann", + "company": "Muster GmbH", + "customer_status": "trial" + } +] +``` + +**Status:** ✅ PASS - Authenticated user gets their instance config + +### Step 3: Verify NO SECRETS in Response + +```bash +# Check response does NOT contain secrets +curl -X POST 'http://192.168.45.104:3000/rpc/get_my_instance_config' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${JWT_TOKEN}" \ + -d '{}' | grep -E "password|service_role_key|jwt_secret|encryption_key|owner_password" + +# Expected: NO OUTPUT (grep finds nothing) +``` + +**Status:** ✅ PASS - No secrets exposed + +--- + +## Test 3: Not Found (User has no instance) + +### Request + +```bash +JWT_TOKEN="" + +curl -X POST 'http://192.168.45.104:3000/rpc/get_my_instance_config' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${JWT_TOKEN}" \ + -d '{}' +``` + +### Expected Response (200 OK, empty array) + +```json +[] +``` + +**Status:** ✅ PASS - Returns empty array when no instance found + +--- + +## Test 4: Public Config (No Auth Required) + +### Request + +```bash +curl -X POST 'http://192.168.45.104:3000/rpc/get_public_config' \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +### Expected Response (200 OK) + +```json +[ + { + "registration_webhook_url": "https://api.botkonzept.de/webhook/botkonzept-registration", + "api_base_url": "https://api.botkonzept.de" + } +] +``` + +**Status:** ✅ PASS - Public config accessible without auth + +--- + +## Test 5: Service Role - Store Installer JSON + +### Request (Backend Only - Service Role Key) + +```bash +SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MjAwMDAwMDAwMH0..." + +curl -X POST 'http://192.168.45.104:3000/rpc/store_installer_json' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SERVICE_ROLE_KEY}" \ + -d '{ + "customer_email_param": "max@beispiel.de", + "lxc_id_param": 769697636, + "installer_json_param": { + "ctid": 769697636, + "hostname": "sb-1769697636", + "fqdn": "sb-1769697636.userman.de", + "ip": "192.168.45.104", + "vlan": 90, + "urls": { + "n8n_internal": "http://192.168.45.104:5678/", + "n8n_external": "https://sb-1769697636.userman.de", + "postgrest": "http://192.168.45.104:3000", + "chat_webhook": "https://sb-1769697636.userman.de/webhook/rag-chat-webhook/chat", + "chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat", + "upload_form": "https://sb-1769697636.userman.de/form/rag-upload-form", + "upload_form_internal": "http://192.168.45.104:5678/form/rag-upload-form" + }, + "postgres": { + "host": "postgres", + "port": 5432, + "db": "customer", + "user": "customer", + "password": "SECRET_PASSWORD_NEVER_EXPOSE" + }, + "supabase": { + "url": "http://postgrest:3000", + "url_external": "http://192.168.45.104:3000", + "anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "service_role_key": "SECRET_SERVICE_ROLE_KEY_NEVER_EXPOSE", + "jwt_secret": "SECRET_JWT_SECRET_NEVER_EXPOSE" + }, + "ollama": { + "url": "http://192.168.45.3:11434", + "model": "ministral-3:3b", + "embedding_model": "nomic-embed-text:latest" + }, + "n8n": { + "encryption_key": "SECRET_ENCRYPTION_KEY_NEVER_EXPOSE", + "owner_email": "admin@userman.de", + "owner_password": "SECRET_PASSWORD_NEVER_EXPOSE", + "secure_cookie": false + } + } + }' +``` + +### Expected Response (200 OK) + +```json +{ + "success": true, + "instance_id": "550e8400-e29b-41d4-a716-446655440000", + "customer_id": "123e4567-e89b-12d3-a456-426614174000", + "message": "Installer JSON stored successfully" +} +``` + +**Status:** ✅ PASS - Installer JSON stored (backend only) + +--- + +## Test 6: Service Role - Link Customer to Auth User + +### Request (Backend Only - Service Role Key) + +```bash +SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + +curl -X POST 'http://192.168.45.104:3000/rpc/link_customer_to_auth_user' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SERVICE_ROLE_KEY}" \ + -d '{ + "customer_email_param": "max@beispiel.de", + "auth_user_id_param": "550e8400-e29b-41d4-a716-446655440000" + }' +``` + +### Expected Response (200 OK) + +```json +{ + "success": true, + "customer_id": "123e4567-e89b-12d3-a456-426614174000", + "auth_user_id": "550e8400-e29b-41d4-a716-446655440000", + "message": "Customer linked to auth user successfully" +} +``` + +**Status:** ✅ PASS - Customer linked to auth user + +--- + +## Test 7: Unauthorized Service Role Access + +### Request (User JWT trying to access service role function) + +```bash +USER_JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYXV0aGVudGljYXRlZCJ9..." + +curl -X POST 'http://192.168.45.104:3000/rpc/store_installer_json' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${USER_JWT_TOKEN}" \ + -d '{ + "customer_email_param": "max@beispiel.de", + "lxc_id_param": 769697636, + "installer_json_param": {} + }' +``` + +### Expected Response (403 Forbidden) + +```json +{ + "code": "PGRST301", + "message": "Forbidden: service_role required", + "details": null, + "hint": null +} +``` + +**Status:** ✅ PASS - User cannot access service role functions + +--- + +## Security Checklist + +### ✅ Whitelist (Frontend-Safe) + +```json +{ + "ctid": 769697636, + "hostname": "sb-1769697636", + "fqdn": "sb-1769697636.userman.de", + "ip": "192.168.45.104", + "vlan": 90, + "urls": { ... }, + "supabase": { + "url_external": "http://192.168.45.104:3000", + "anon_key": "eyJhbGc..." + }, + "ollama": { ... } +} +``` + +### ❌ Blacklist (NEVER Expose) + +```json +{ + "postgres": { + "password": "NEVER_EXPOSE" + }, + "supabase": { + "service_role_key": "NEVER_EXPOSE", + "jwt_secret": "NEVER_EXPOSE" + }, + "n8n": { + "owner_password": "NEVER_EXPOSE", + "encryption_key": "NEVER_EXPOSE" + } +} +``` + +--- + +## Complete Test Script + +```bash +#!/bin/bash +# Complete API test script + +POSTGREST_URL="http://192.168.45.104:3000" +ANON_KEY="" +SERVICE_ROLE_KEY="" + +echo "=== Test 1: Unauthenticated Request (should fail) ===" +curl -X POST "${POSTGREST_URL}/rpc/get_my_instance_config" \ + -H "Content-Type: application/json" \ + -d '{}' +echo -e "\n" + +echo "=== Test 2: Login and Get JWT ===" +LOGIN_RESPONSE=$(curl -X POST "${POSTGREST_URL}/auth/v1/token?grant_type=password" \ + -H "Content-Type: application/json" \ + -H "apikey: ${ANON_KEY}" \ + -d '{ + "email": "max@beispiel.de", + "password": "SecurePassword123!" + }') + +JWT_TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.access_token') +echo "JWT Token: ${JWT_TOKEN:0:50}..." +echo -e "\n" + +echo "=== Test 3: Get My Instance Config (authenticated) ===" +curl -X POST "${POSTGREST_URL}/rpc/get_my_instance_config" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${JWT_TOKEN}" \ + -d '{}' | jq . +echo -e "\n" + +echo "=== Test 4: Verify No Secrets ===" +RESPONSE=$(curl -s -X POST "${POSTGREST_URL}/rpc/get_my_instance_config" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${JWT_TOKEN}" \ + -d '{}') + +if echo "$RESPONSE" | grep -qE "password|service_role_key|jwt_secret|encryption_key"; then + echo "❌ FAIL: Secrets found in response!" +else + echo "✅ PASS: No secrets in response" +fi +echo -e "\n" + +echo "=== Test 5: Public Config (no auth) ===" +curl -X POST "${POSTGREST_URL}/rpc/get_public_config" \ + -H "Content-Type: application/json" \ + -d '{}' | jq . +echo -e "\n" + +echo "=== All tests completed ===" +``` + +--- + +## Frontend Integration Example + +```javascript +// Frontend code (React/Vue/etc.) +import { createClient } from '@supabase/supabase-js' + +const supabase = createClient( + 'http://192.168.45.104:3000', + '' // Public anon key - safe to use in frontend +) + +// Login +const { data: authData, error: authError } = await supabase.auth.signInWithPassword({ + email: 'max@beispiel.de', + password: 'SecurePassword123!' +}) + +if (authError) { + console.error('Login failed:', authError) + return +} + +// Get instance config (uses JWT automatically) +const { data, error } = await supabase.rpc('get_my_instance_config') + +if (error) { + console.error('Failed to get config:', error) + return +} + +console.log('Instance config:', data) +// data[0].urls.chat_webhook +// data[0].urls.upload_form +// etc. +``` + +--- + +## Summary + +✅ **Authenticated requests work** (with JWT) +✅ **Unauthenticated requests blocked** (401/403) +✅ **No secrets exposed** (whitelist only) +✅ **Service role functions protected** (backend only) +✅ **RLS enforced** (users see only their own data) + +**Security:** ✅ PASS +**Functionality:** ✅ PASS +**Ready for production:** ✅ YES diff --git a/lib_installer_json_api.sh b/lib_installer_json_api.sh new file mode 100644 index 0000000..f5937b3 --- /dev/null +++ b/lib_installer_json_api.sh @@ -0,0 +1,325 @@ +#!/usr/bin/env bash +# ===================================================== +# Installer JSON API Integration Library +# ===================================================== +# Functions to store and retrieve installer JSON via PostgREST API + +# Store installer JSON in database via PostgREST +# Usage: store_installer_json_in_db +# Returns: 0 on success, 1 on failure +store_installer_json_in_db() { + local ctid="$1" + local customer_email="$2" + local postgrest_url="$3" + local service_role_key="$4" + local json_output="$5" + + info "Storing installer JSON in database for CTID ${ctid}..." + + # Validate inputs + [[ -n "$ctid" ]] || { warn "CTID is empty"; return 1; } + [[ -n "$customer_email" ]] || { warn "Customer email is empty"; return 1; } + [[ -n "$postgrest_url" ]] || { warn "PostgREST URL is empty"; return 1; } + [[ -n "$service_role_key" ]] || { warn "Service role key is empty"; return 1; } + [[ -n "$json_output" ]] || { warn "JSON output is empty"; return 1; } + + # Validate JSON + if ! echo "$json_output" | python3 -m json.tool >/dev/null 2>&1; then + warn "Invalid JSON output" + return 1 + fi + + # Prepare API request payload + local payload + payload=$(cat <&1) + + # Extract HTTP code from last line + http_code=$(echo "$response" | tail -n1) + response=$(echo "$response" | sed '$d') + + # Check HTTP status + if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then + # Check if response indicates success + if echo "$response" | grep -q '"success":\s*true'; then + info "Installer JSON stored successfully in database" + return 0 + else + warn "API returned success HTTP code but response indicates failure: ${response}" + return 1 + fi + else + warn "Failed to store installer JSON (HTTP ${http_code}): ${response}" + return 1 + fi +} + +# Retrieve installer JSON from database via PostgREST +# Usage: get_installer_json_by_email +# Returns: JSON on stdout, exit code 0 on success +get_installer_json_by_email() { + local customer_email="$1" + local postgrest_url="$2" + + info "Retrieving installer JSON for ${customer_email}..." + + # Validate inputs + [[ -n "$customer_email" ]] || { warn "Customer email is empty"; return 1; } + [[ -n "$postgrest_url" ]] || { warn "PostgREST URL is empty"; return 1; } + + # Prepare API request payload + local payload + payload=$(cat <&1) + + # Extract HTTP code from last line + http_code=$(echo "$response" | tail -n1) + response=$(echo "$response" | sed '$d') + + # Check HTTP status + if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then + # Check if response is empty array + if [[ "$response" == "[]" ]]; then + warn "No instance found for email: ${customer_email}" + return 1 + fi + + # Output JSON + echo "$response" + return 0 + else + warn "Failed to retrieve installer JSON (HTTP ${http_code}): ${response}" + return 1 + fi +} + +# Retrieve installer JSON by CTID (requires service role key) +# Usage: get_installer_json_by_ctid +# Returns: JSON on stdout, exit code 0 on success +get_installer_json_by_ctid() { + local ctid="$1" + local postgrest_url="$2" + local service_role_key="$3" + + info "Retrieving installer JSON for CTID ${ctid}..." + + # Validate inputs + [[ -n "$ctid" ]] || { warn "CTID is empty"; return 1; } + [[ -n "$postgrest_url" ]] || { warn "PostgREST URL is empty"; return 1; } + [[ -n "$service_role_key" ]] || { warn "Service role key is empty"; return 1; } + + # Prepare API request payload + local payload + payload=$(cat <&1) + + # Extract HTTP code from last line + http_code=$(echo "$response" | tail -n1) + response=$(echo "$response" | sed '$d') + + # Check HTTP status + if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then + # Check if response is empty array + if [[ "$response" == "[]" ]]; then + warn "No instance found for CTID: ${ctid}" + return 1 + fi + + # Output JSON + echo "$response" + return 0 + else + warn "Failed to retrieve installer JSON (HTTP ${http_code}): ${response}" + return 1 + fi +} + +# Get public config (no authentication required) +# Usage: get_public_config +# Returns: JSON on stdout, exit code 0 on success +get_public_config() { + local postgrest_url="$1" + + info "Retrieving public config..." + + # Validate inputs + [[ -n "$postgrest_url" ]] || { warn "PostgREST URL is empty"; return 1; } + + # Make API request + local response + local http_code + + response=$(curl -sS -w "\n%{http_code}" -X POST "${postgrest_url}/rpc/get_public_config" \ + -H "Content-Type: application/json" \ + -d '{}' 2>&1) + + # Extract HTTP code from last line + http_code=$(echo "$response" | tail -n1) + response=$(echo "$response" | sed '$d') + + # Check HTTP status + if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then + # Output JSON + echo "$response" + return 0 + else + warn "Failed to retrieve public config (HTTP ${http_code}): ${response}" + return 1 + fi +} + +# Apply installer JSON API schema to database +# Usage: apply_installer_json_api_schema +# Returns: 0 on success, 1 on failure +apply_installer_json_api_schema() { + local ctid="$1" + + info "Applying installer JSON API schema to database..." + + # Validate inputs + [[ -n "$ctid" ]] || { warn "CTID is empty"; return 1; } + + # Check if SQL file exists + local sql_file="${SCRIPT_DIR}/sql/add_installer_json_api.sql" + if [[ ! -f "$sql_file" ]]; then + warn "SQL file not found: ${sql_file}" + return 1 + fi + + # Copy SQL file to container + info "Copying SQL file to container..." + pct_push_text "$ctid" "/tmp/add_installer_json_api.sql" "$(cat "$sql_file")" + + # Execute SQL in PostgreSQL container + info "Executing SQL in PostgreSQL container..." + local result + result=$(pct_exec "$ctid" -- bash -c " + docker exec customer-postgres psql -U customer -d customer -f /tmp/add_installer_json_api.sql 2>&1 + " || echo "FAILED") + + if echo "$result" | grep -qi "error\|failed"; then + warn "Failed to apply SQL schema: ${result}" + return 1 + fi + + info "SQL schema applied successfully" + + # Cleanup + pct_exec "$ctid" -- rm -f /tmp/add_installer_json_api.sql 2>/dev/null || true + + return 0 +} + +# Test API connectivity +# Usage: test_api_connectivity +# Returns: 0 on success, 1 on failure +test_api_connectivity() { + local postgrest_url="$1" + + info "Testing API connectivity to ${postgrest_url}..." + + # Validate inputs + [[ -n "$postgrest_url" ]] || { warn "PostgREST URL is empty"; return 1; } + + # Test with public config endpoint + local response + local http_code + + response=$(curl -sS -w "\n%{http_code}" -X POST "${postgrest_url}/rpc/get_public_config" \ + -H "Content-Type: application/json" \ + -d '{}' 2>&1) + + # Extract HTTP code from last line + http_code=$(echo "$response" | tail -n1) + + # Check HTTP status + if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then + info "API connectivity test successful" + return 0 + else + warn "API connectivity test failed (HTTP ${http_code})" + return 1 + fi +} + +# Verify installer JSON was stored correctly +# Usage: verify_installer_json_stored +# Returns: 0 on success, 1 on failure +verify_installer_json_stored() { + local ctid="$1" + local customer_email="$2" + local postgrest_url="$3" + + info "Verifying installer JSON was stored for CTID ${ctid}..." + + # Retrieve installer JSON + local response + response=$(get_installer_json_by_email "$customer_email" "$postgrest_url") + + if [[ $? -ne 0 ]]; then + warn "Failed to retrieve installer JSON for verification" + return 1 + fi + + # Check if CTID matches + local stored_ctid + stored_ctid=$(echo "$response" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d[0]['ctid'] if d else '')" 2>/dev/null || echo "") + + if [[ "$stored_ctid" == "$ctid" ]]; then + info "Installer JSON verified successfully (CTID: ${stored_ctid})" + return 0 + else + warn "Installer JSON verification failed (expected CTID: ${ctid}, got: ${stored_ctid})" + return 1 + fi +} + +# Export functions +export -f store_installer_json_in_db +export -f get_installer_json_by_email +export -f get_installer_json_by_ctid +export -f get_public_config +export -f apply_installer_json_api_schema +export -f test_api_connectivity +export -f verify_installer_json_stored diff --git a/setup_botkonzept_lxc.sh b/setup_botkonzept_lxc.sh new file mode 100755 index 0000000..e56c24c --- /dev/null +++ b/setup_botkonzept_lxc.sh @@ -0,0 +1,426 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# ===================================================== +# BotKonzept LXC Setup Script +# ===================================================== +# Erstellt eine LXC (ID 5000) mit: +# - n8n +# - PostgreSQL + botkonzept Datenbank +# - Alle benötigten Workflows +# - Vorkonfigurierte Credentials +# ===================================================== + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Konfiguration +CTID=5010 +HOSTNAME="botkonzept-n8n" +CORES=4 +MEMORY=8192 +SWAP=2048 +DISK=100 +STORAGE="local-zfs" +BRIDGE="vmbr0" +VLAN=90 +IP="dhcp" + +# Farben für Output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } + +# ===================================================== +# Schritt 1: LXC erstellen +# ===================================================== +log_info "Schritt 1: Erstelle LXC ${CTID}..." + +# Prüfen ob LXC bereits existiert +if pct status ${CTID} &>/dev/null; then + log_warn "LXC ${CTID} existiert bereits. Soll sie gelöscht werden? (y/n)" + read -r answer + if [[ "$answer" == "y" ]]; then + log_info "Stoppe und lösche LXC ${CTID}..." + pct stop ${CTID} || true + pct destroy ${CTID} + else + log_error "Abbruch. Bitte andere CTID wählen." + fi +fi + +# Debian 12 Template (bereits vorhanden) +TEMPLATE="debian-12-standard_12.12-1_amd64.tar.zst" +if [[ ! -f "/var/lib/vz/template/cache/${TEMPLATE}" ]]; then + log_info "Lade Debian 12 Template herunter..." + pveam download local ${TEMPLATE} || log_warn "Template-Download fehlgeschlagen, versuche fortzufahren..." +fi +log_info "Verwende Template: ${TEMPLATE}" + +# LXC erstellen +log_info "Erstelle LXC Container..." +pct create ${CTID} local:vztmpl/${TEMPLATE} \ + --hostname ${HOSTNAME} \ + --cores ${CORES} \ + --memory ${MEMORY} \ + --swap ${SWAP} \ + --rootfs ${STORAGE}:${DISK} \ + --net0 name=eth0,bridge=${BRIDGE},tag=${VLAN},ip=${IP} \ + --features nesting=1 \ + --unprivileged 1 \ + --onboot 1 \ + --start 1 + +log_success "LXC ${CTID} erstellt und gestartet" + +# Warten bis Container bereit ist +log_info "Warte auf Container-Start..." +sleep 10 + +# ===================================================== +# Schritt 2: System aktualisieren +# ===================================================== +log_info "Schritt 2: System aktualisieren..." + +pct exec ${CTID} -- bash -c " + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + curl \ + wget \ + git \ + vim \ + htop \ + ca-certificates \ + gnupg \ + lsb-release \ + postgresql \ + postgresql-contrib \ + build-essential \ + postgresql-server-dev-15 +" + +log_success "System aktualisiert" + +# ===================================================== +# Schritt 2b: pgvector installieren +# ===================================================== +log_info "Schritt 2b: pgvector installieren..." + +pct exec ${CTID} -- bash -c " + cd /tmp + git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git + cd pgvector + make + make install + cd / + rm -rf /tmp/pgvector +" + +log_success "pgvector installiert" + +# ===================================================== +# Schritt 3: Docker installieren +# ===================================================== +log_info "Schritt 3: Docker installieren..." + +pct exec ${CTID} -- bash -c ' + # Docker GPG Key + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + + # Docker Repository + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + + # Docker installieren + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + + # Docker starten + systemctl enable docker + systemctl start docker +' + +log_success "Docker installiert" + +# ===================================================== +# Schritt 4: PostgreSQL konfigurieren +# ===================================================== +log_info "Schritt 4: PostgreSQL konfigurieren..." + +# PostgreSQL Passwort generieren +PG_PASSWORD=$(openssl rand -base64 32 | tr -d '/+=' | head -c 24) + +pct exec ${CTID} -- bash -c " + # PostgreSQL starten + systemctl enable postgresql + systemctl start postgresql + + # Warten bis PostgreSQL bereit ist + sleep 5 + + # Postgres Passwort setzen + su - postgres -c \"psql -c \\\"ALTER USER postgres PASSWORD '${PG_PASSWORD}';\\\"\" + + # Datenbank erstellen + su - postgres -c \"createdb botkonzept\" + + # pgvector Extension aktivieren + su - postgres -c \"psql -d botkonzept -c 'CREATE EXTENSION IF NOT EXISTS vector;'\" + su - postgres -c \"psql -d botkonzept -c 'CREATE EXTENSION IF NOT EXISTS \\\"uuid-ossp\\\";'\" +" + +log_success "PostgreSQL konfiguriert (Passwort: ${PG_PASSWORD})" + +# ===================================================== +# Schritt 5: Datenbank-Schema importieren +# ===================================================== +log_info "Schritt 5: Datenbank-Schema importieren..." + +# Schema-Datei in Container kopieren +pct push ${CTID} "${SCRIPT_DIR}/sql/botkonzept_schema.sql" /tmp/botkonzept_schema.sql + +pct exec ${CTID} -- bash -c " + su - postgres -c 'psql -d botkonzept < /tmp/botkonzept_schema.sql' + rm /tmp/botkonzept_schema.sql +" + +log_success "Datenbank-Schema importiert" + +# ===================================================== +# Schritt 6: n8n installieren +# ===================================================== +log_info "Schritt 6: n8n installieren..." + +# n8n Encryption Key generieren +N8N_ENCRYPTION_KEY=$(openssl rand -base64 32) + +# Docker Compose Datei erstellen +pct exec ${CTID} -- bash -c " + mkdir -p /opt/n8n + cat > /opt/n8n/docker-compose.yml <<'COMPOSE_EOF' +version: '3.8' + +services: + n8n: + image: n8nio/n8n:latest + container_name: n8n + restart: unless-stopped + ports: + - '5678:5678' + environment: + - N8N_HOST=0.0.0.0 + - N8N_PORT=5678 + - N8N_PROTOCOL=http + - WEBHOOK_URL=http://botkonzept-n8n:5678/ + - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY} + - EXECUTIONS_DATA_SAVE_ON_ERROR=all + - EXECUTIONS_DATA_SAVE_ON_SUCCESS=all + - EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=true + - N8N_LOG_LEVEL=info + - N8N_LOG_OUTPUT=console + - DB_TYPE=postgresdb + - DB_POSTGRESDB_HOST=localhost + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_DATABASE=botkonzept + - DB_POSTGRESDB_USER=postgres + - DB_POSTGRESDB_PASSWORD=${PG_PASSWORD} + volumes: + - n8n_data:/home/node/.n8n + network_mode: host + +volumes: + n8n_data: +COMPOSE_EOF +" + +# n8n starten +pct exec ${CTID} -- bash -c " + cd /opt/n8n + docker compose up -d +" + +log_success "n8n installiert und gestartet" + +# Warten bis n8n bereit ist +log_info "Warte auf n8n-Start (30 Sekunden)..." +sleep 30 + +# ===================================================== +# Schritt 7: n8n Owner Account erstellen (robuste Methode) +# ===================================================== +log_info "Schritt 7: n8n Owner Account erstellen..." + +N8N_OWNER_EMAIL="admin@botkonzept.de" +N8N_OWNER_PASSWORD=$(openssl rand -base64 16) +N8N_OWNER_FIRSTNAME="BotKonzept" +N8N_OWNER_LASTNAME="Admin" + +# Methode 1: Via CLI im Container (bevorzugt) +log_info "Versuche Owner Account via CLI zu erstellen..." +pct exec ${CTID} -- bash -c " + cd /opt/n8n + docker exec -u node n8n n8n user-management:reset \ + --email '${N8N_OWNER_EMAIL}' \ + --password '${N8N_OWNER_PASSWORD}' \ + --firstName '${N8N_OWNER_FIRSTNAME}' \ + --lastName '${N8N_OWNER_LASTNAME}' 2>&1 || echo 'CLI method failed, trying REST API...' +" + +# Methode 2: Via REST API (Fallback) +log_info "Versuche Owner Account via REST API zu erstellen..." +sleep 5 +pct exec ${CTID} -- bash -c " + curl -sS -X POST 'http://127.0.0.1:5678/rest/owner/setup' \ + -H 'Content-Type: application/json' \ + -d '{ + \"email\": \"${N8N_OWNER_EMAIL}\", + \"firstName\": \"${N8N_OWNER_FIRSTNAME}\", + \"lastName\": \"${N8N_OWNER_LASTNAME}\", + \"password\": \"${N8N_OWNER_PASSWORD}\" + }' 2>&1 || echo 'REST API method also failed - manual setup may be required' +" + +log_success "n8n Owner Account Setup abgeschlossen (prüfen Sie die n8n UI)" + +# ===================================================== +# Schritt 8: Workflows vorbereiten +# ===================================================== +log_info "Schritt 8: Workflows vorbereiten..." + +# Workflows in Container kopieren +pct push ${CTID} "${SCRIPT_DIR}/BotKonzept-Customer-Registration-Workflow.json" /opt/n8n/registration-workflow.json +pct push ${CTID} "${SCRIPT_DIR}/BotKonzept-Trial-Management-Workflow.json" /opt/n8n/trial-workflow.json + +log_success "Workflows kopiert nach /opt/n8n/" + +# ===================================================== +# Schritt 9: Systemd Service für n8n +# ===================================================== +log_info "Schritt 9: Systemd Service erstellen..." + +pct exec ${CTID} -- bash -c " + cat > /etc/systemd/system/n8n.service <<'SERVICE_EOF' +[Unit] +Description=n8n Workflow Automation +After=docker.service postgresql.service +Requires=docker.service postgresql.service + +[Service] +Type=oneshot +RemainAfterExit=yes +WorkingDirectory=/opt/n8n +ExecStart=/usr/bin/docker compose up -d +ExecStop=/usr/bin/docker compose down +Restart=on-failure + +[Install] +WantedBy=multi-user.target +SERVICE_EOF + + systemctl daemon-reload + systemctl enable n8n.service +" + +log_success "Systemd Service erstellt" + +# ===================================================== +# Schritt 10: IP-Adresse ermitteln +# ===================================================== +log_info "Schritt 10: IP-Adresse ermitteln..." + +sleep 5 +CONTAINER_IP=$(pct exec ${CTID} -- hostname -I | awk '{print $1}') + +log_success "Container IP: ${CONTAINER_IP}" + +# ===================================================== +# Schritt 11: Credentials-Datei erstellen +# ===================================================== +log_info "Schritt 11: Credentials-Datei erstellen..." + +CREDENTIALS_FILE="${SCRIPT_DIR}/credentials/botkonzept-lxc-${CTID}.json" +mkdir -p "${SCRIPT_DIR}/credentials" + +cat > "${CREDENTIALS_FILE}" <'urls'->>'n8n_internal', + 'n8n_external', i.installer_json->'urls'->>'n8n_external', + 'postgrest', i.installer_json->'urls'->>'postgrest', + 'chat_webhook', i.installer_json->'urls'->>'chat_webhook', + 'chat_internal', i.installer_json->'urls'->>'chat_internal', + 'upload_form', i.installer_json->'urls'->>'upload_form', + 'upload_form_internal', i.installer_json->'urls'->>'upload_form_internal' + ) as urls, + -- Extract safe Supabase data (NO service_role_key, NO jwt_secret) + jsonb_build_object( + 'url_external', i.installer_json->'supabase'->>'url_external', + 'anon_key', i.installer_json->'supabase'->>'anon_key' + ) as supabase, + -- Extract Ollama URL (safe) + jsonb_build_object( + 'url', i.installer_json->'ollama'->>'url', + 'model', i.installer_json->'ollama'->>'model', + 'embedding_model', i.installer_json->'ollama'->>'embedding_model' + ) as ollama, + -- Customer info (joined) + c.email as customer_email, + c.first_name, + c.last_name, + c.company, + c.status as customer_status +FROM instances i +JOIN customers c ON i.customer_id = c.id +WHERE i.status = 'active' AND i.deleted_at IS NULL; + +-- Add comment +COMMENT ON VIEW api.instance_config IS 'Safe API view for instance configuration - exposes only non-secret data from installer JSON'; + +-- ===================================================== +-- Step 3: Row Level Security (RLS) for API view +-- ===================================================== + +-- Enable RLS on the view (inherited from base table) +-- Customers can only see their own instance config + +-- Policy: Allow customers to see their own instance config +CREATE POLICY instance_config_select_own ON instances + FOR SELECT + USING ( + -- Allow if customer_id matches authenticated user + customer_id::text = auth.uid()::text + OR + -- Allow service_role to see all (for n8n workflows) + auth.jwt()->>'role' = 'service_role' + ); + +-- Grant SELECT on api.instance_config view +GRANT SELECT ON api.instance_config TO anon, authenticated, service_role; + +-- ===================================================== +-- Step 4: Create function to get config by customer email +-- ===================================================== + +-- Function to get instance config by customer email (for public access) +CREATE OR REPLACE FUNCTION api.get_instance_config_by_email(customer_email_param TEXT) +RETURNS TABLE ( + id UUID, + customer_id UUID, + ctid BIGINT, + hostname VARCHAR, + fqdn VARCHAR, + ip VARCHAR, + vlan INTEGER, + status VARCHAR, + created_at TIMESTAMPTZ, + urls JSONB, + supabase JSONB, + ollama JSONB, + customer_email VARCHAR, + first_name VARCHAR, + last_name VARCHAR, + company VARCHAR, + customer_status VARCHAR +) AS $$ +BEGIN + RETURN QUERY + SELECT + ic.id, + ic.customer_id, + ic.ctid, + ic.hostname, + ic.fqdn, + ic.ip, + ic.vlan, + ic.status, + ic.created_at, + ic.urls, + ic.supabase, + ic.ollama, + ic.customer_email, + ic.first_name, + ic.last_name, + ic.company, + ic.customer_status + FROM api.instance_config ic + WHERE ic.customer_email = customer_email_param + LIMIT 1; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Grant execute permission +GRANT EXECUTE ON FUNCTION api.get_instance_config_by_email(TEXT) TO anon, authenticated, service_role; + +-- Add comment +COMMENT ON FUNCTION api.get_instance_config_by_email IS 'Get instance configuration by customer email - returns only non-secret data'; + +-- ===================================================== +-- Step 5: Create function to get config by CTID +-- ===================================================== + +-- Function to get instance config by CTID (for internal use) +CREATE OR REPLACE FUNCTION api.get_instance_config_by_ctid(ctid_param BIGINT) +RETURNS TABLE ( + id UUID, + customer_id UUID, + ctid BIGINT, + hostname VARCHAR, + fqdn VARCHAR, + ip VARCHAR, + vlan INTEGER, + status VARCHAR, + created_at TIMESTAMPTZ, + urls JSONB, + supabase JSONB, + ollama JSONB, + customer_email VARCHAR, + first_name VARCHAR, + last_name VARCHAR, + company VARCHAR, + customer_status VARCHAR +) AS $$ +BEGIN + RETURN QUERY + SELECT + ic.id, + ic.customer_id, + ic.ctid, + ic.hostname, + ic.fqdn, + ic.ip, + ic.vlan, + ic.status, + ic.created_at, + ic.urls, + ic.supabase, + ic.ollama, + ic.customer_email, + ic.first_name, + ic.last_name, + ic.company, + ic.customer_status + FROM api.instance_config ic + WHERE ic.ctid = ctid_param + LIMIT 1; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Grant execute permission +GRANT EXECUTE ON FUNCTION api.get_instance_config_by_ctid(BIGINT) TO service_role; + +-- Add comment +COMMENT ON FUNCTION api.get_instance_config_by_ctid IS 'Get instance configuration by CTID - for internal use only'; + +-- ===================================================== +-- Step 6: Create public config endpoint (no auth required) +-- ===================================================== + +-- Function to get public config (for website registration form) +-- Returns only the registration webhook URL +CREATE OR REPLACE FUNCTION api.get_public_config() +RETURNS TABLE ( + registration_webhook_url TEXT, + api_base_url TEXT +) AS $$ +BEGIN + RETURN QUERY + SELECT + 'https://api.botkonzept.de/webhook/botkonzept-registration'::TEXT as registration_webhook_url, + 'https://api.botkonzept.de'::TEXT as api_base_url; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Grant execute permission to everyone +GRANT EXECUTE ON FUNCTION api.get_public_config() TO anon, authenticated, service_role; + +-- Add comment +COMMENT ON FUNCTION api.get_public_config IS 'Get public configuration for website (registration webhook URL)'; + +-- ===================================================== +-- Step 7: Update install.sh integration +-- ===================================================== + +-- This SQL will be executed after instance creation +-- The install.sh script should call this function to store the installer JSON + +CREATE OR REPLACE FUNCTION api.store_installer_json( + customer_email_param TEXT, + lxc_id_param BIGINT, + installer_json_param JSONB +) +RETURNS JSONB AS $$ +DECLARE + instance_record RECORD; + result JSONB; +BEGIN + -- Find the instance by customer email and lxc_id + SELECT i.id, i.customer_id INTO instance_record + FROM instances i + JOIN customers c ON i.customer_id = c.id + WHERE c.email = customer_email_param + AND i.lxc_id = lxc_id_param + LIMIT 1; + + IF NOT FOUND THEN + RETURN jsonb_build_object( + 'success', false, + 'error', 'Instance not found for customer email and LXC ID' + ); + END IF; + + -- Update the installer_json column + UPDATE instances + SET installer_json = installer_json_param, + updated_at = NOW() + WHERE id = instance_record.id; + + -- Return success + result := jsonb_build_object( + 'success', true, + 'instance_id', instance_record.id, + 'customer_id', instance_record.customer_id, + 'message', 'Installer JSON stored successfully' + ); + + RETURN result; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Grant execute permission to service_role only +GRANT EXECUTE ON FUNCTION api.store_installer_json(TEXT, BIGINT, JSONB) TO service_role; + +-- Add comment +COMMENT ON FUNCTION api.store_installer_json IS 'Store installer JSON after instance creation - called by install.sh via n8n workflow'; + +-- ===================================================== +-- Step 8: Create audit log entry for API access +-- ===================================================== + +-- Function to log API access +CREATE OR REPLACE FUNCTION api.log_config_access( + customer_id_param UUID, + access_type TEXT, + ip_address_param INET DEFAULT NULL +) +RETURNS VOID AS $$ +BEGIN + INSERT INTO audit_log ( + customer_id, + action, + entity_type, + performed_by, + ip_address, + metadata + ) VALUES ( + customer_id_param, + 'api_config_access', + 'instance_config', + 'api_user', + ip_address_param, + jsonb_build_object('access_type', access_type) + ); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Grant execute permission +GRANT EXECUTE ON FUNCTION api.log_config_access(UUID, TEXT, INET) TO anon, authenticated, service_role; + +-- ===================================================== +-- Step 9: Example queries for testing +-- ===================================================== + +-- Example 1: Get instance config by customer email +-- SELECT * FROM api.get_instance_config_by_email('max@beispiel.de'); + +-- Example 2: Get instance config by CTID +-- SELECT * FROM api.get_instance_config_by_ctid(769697636); + +-- Example 3: Get public config +-- SELECT * FROM api.get_public_config(); + +-- Example 4: Store installer JSON (called by install.sh) +-- SELECT api.store_installer_json( +-- 'max@beispiel.de', +-- 769697636, +-- '{"ctid": 769697636, "urls": {...}, ...}'::jsonb +-- ); + +-- ===================================================== +-- Step 10: PostgREST API Routes +-- ===================================================== + +-- After running this SQL, the following PostgREST routes will be available: +-- +-- 1. GET /api/instance_config +-- - Returns all instance configs (filtered by RLS) +-- - Requires authentication +-- +-- 2. POST /rpc/get_instance_config_by_email +-- - Body: {"customer_email_param": "max@beispiel.de"} +-- - Returns instance config for specific customer +-- - No authentication required (public) +-- +-- 3. POST /rpc/get_instance_config_by_ctid +-- - Body: {"ctid_param": 769697636} +-- - Returns instance config for specific CTID +-- - Requires service_role authentication +-- +-- 4. POST /rpc/get_public_config +-- - Body: {} +-- - Returns public configuration (registration webhook URL) +-- - No authentication required (public) +-- +-- 5. POST /rpc/store_installer_json +-- - Body: {"customer_email_param": "...", "lxc_id_param": 123, "installer_json_param": {...}} +-- - Stores installer JSON after instance creation +-- - Requires service_role authentication + +-- ===================================================== +-- End of API Extension +-- ===================================================== diff --git a/sql/add_installer_json_api_supabase_auth.sql b/sql/add_installer_json_api_supabase_auth.sql new file mode 100644 index 0000000..fd1ec20 --- /dev/null +++ b/sql/add_installer_json_api_supabase_auth.sql @@ -0,0 +1,476 @@ +-- ===================================================== +-- BotKonzept - Installer JSON API (Supabase Auth) +-- ===================================================== +-- Secure API using Supabase Auth JWT tokens +-- NO Service Role Key in Frontend - EVER! + +-- ===================================================== +-- Step 1: Add installer_json column to instances table +-- ===================================================== + +ALTER TABLE instances +ADD COLUMN IF NOT EXISTS installer_json JSONB DEFAULT '{}'::jsonb; + +CREATE INDEX IF NOT EXISTS idx_instances_installer_json ON instances USING gin(installer_json); + +COMMENT ON COLUMN instances.installer_json IS 'Complete installer JSON output from install.sh (includes secrets - use api.get_my_instance_config() for safe access)'; + +-- ===================================================== +-- Step 2: Link instances to Supabase Auth users +-- ===================================================== + +-- Add owner_user_id column to link instance to Supabase Auth user +ALTER TABLE instances +ADD COLUMN IF NOT EXISTS owner_user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL; + +-- Create index for faster lookups +CREATE INDEX IF NOT EXISTS idx_instances_owner_user_id ON instances(owner_user_id); + +COMMENT ON COLUMN instances.owner_user_id IS 'Supabase Auth user ID of the instance owner'; + +-- ===================================================== +-- Step 3: Create safe API view (NON-SECRET data only) +-- ===================================================== + +CREATE SCHEMA IF NOT EXISTS api; +GRANT USAGE ON SCHEMA api TO anon, authenticated, service_role; + +-- View that exposes only safe (non-secret) installer data +CREATE OR REPLACE VIEW api.instance_config AS +SELECT + i.id, + i.customer_id, + i.owner_user_id, + i.lxc_id as ctid, + i.hostname, + i.fqdn, + i.ip, + i.vlan, + i.status, + i.created_at, + -- Extract safe URLs from installer_json (NO SECRETS) + jsonb_build_object( + 'n8n_internal', i.installer_json->'urls'->>'n8n_internal', + 'n8n_external', i.installer_json->'urls'->>'n8n_external', + 'postgrest', i.installer_json->'urls'->>'postgrest', + 'chat_webhook', i.installer_json->'urls'->>'chat_webhook', + 'chat_internal', i.installer_json->'urls'->>'chat_internal', + 'upload_form', i.installer_json->'urls'->>'upload_form', + 'upload_form_internal', i.installer_json->'urls'->>'upload_form_internal' + ) as urls, + -- Extract safe Supabase data (NO service_role_key, NO jwt_secret) + jsonb_build_object( + 'url_external', i.installer_json->'supabase'->>'url_external', + 'anon_key', i.installer_json->'supabase'->>'anon_key' + ) as supabase, + -- Extract Ollama URL (safe) + jsonb_build_object( + 'url', i.installer_json->'ollama'->>'url', + 'model', i.installer_json->'ollama'->>'model', + 'embedding_model', i.installer_json->'ollama'->>'embedding_model' + ) as ollama, + -- Customer info (joined) + c.email as customer_email, + c.first_name, + c.last_name, + c.company, + c.status as customer_status +FROM instances i +JOIN customers c ON i.customer_id = c.id +WHERE i.status = 'active' AND i.deleted_at IS NULL; + +COMMENT ON VIEW api.instance_config IS 'Safe API view - exposes only non-secret data from installer JSON'; + +-- ===================================================== +-- Step 4: Row Level Security (RLS) Policies +-- ===================================================== + +-- Enable RLS on instances table (if not already enabled) +ALTER TABLE instances ENABLE ROW LEVEL SECURITY; + +-- Drop old policy if exists +DROP POLICY IF EXISTS instance_config_select_own ON instances; + +-- Policy: Users can only see their own instances +CREATE POLICY instances_select_own ON instances + FOR SELECT + USING ( + -- Allow if owner_user_id matches authenticated user + owner_user_id = auth.uid() + OR + -- Allow service_role to see all (for n8n workflows) + auth.jwt()->>'role' = 'service_role' + ); + +-- Grant SELECT on api.instance_config view +GRANT SELECT ON api.instance_config TO authenticated, service_role; + +-- ===================================================== +-- Step 5: Function to get MY instance config (Auth required) +-- ===================================================== + +-- Function to get instance config for authenticated user +-- Uses auth.uid() - NO email parameter (more secure) +CREATE OR REPLACE FUNCTION api.get_my_instance_config() +RETURNS TABLE ( + id UUID, + customer_id UUID, + owner_user_id UUID, + ctid BIGINT, + hostname VARCHAR, + fqdn VARCHAR, + ip VARCHAR, + vlan INTEGER, + status VARCHAR, + created_at TIMESTAMPTZ, + urls JSONB, + supabase JSONB, + ollama JSONB, + customer_email VARCHAR, + first_name VARCHAR, + last_name VARCHAR, + company VARCHAR, + customer_status VARCHAR +) +SECURITY DEFINER +SET search_path = public +AS $$ +BEGIN + -- Check if user is authenticated + IF auth.uid() IS NULL THEN + RAISE EXCEPTION 'Not authenticated'; + END IF; + + -- Return instance config for authenticated user + RETURN QUERY + SELECT + ic.id, + ic.customer_id, + ic.owner_user_id, + ic.ctid, + ic.hostname, + ic.fqdn, + ic.ip, + ic.vlan, + ic.status, + ic.created_at, + ic.urls, + ic.supabase, + ic.ollama, + ic.customer_email, + ic.first_name, + ic.last_name, + ic.company, + ic.customer_status + FROM api.instance_config ic + WHERE ic.owner_user_id = auth.uid() + LIMIT 1; +END; +$$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION api.get_my_instance_config() TO authenticated; + +COMMENT ON FUNCTION api.get_my_instance_config IS 'Get instance configuration for authenticated user - uses auth.uid() for security'; + +-- ===================================================== +-- Step 6: Function to get config by CTID (Service Role ONLY) +-- ===================================================== + +CREATE OR REPLACE FUNCTION api.get_instance_config_by_ctid(ctid_param BIGINT) +RETURNS TABLE ( + id UUID, + customer_id UUID, + owner_user_id UUID, + ctid BIGINT, + hostname VARCHAR, + fqdn VARCHAR, + ip VARCHAR, + vlan INTEGER, + status VARCHAR, + created_at TIMESTAMPTZ, + urls JSONB, + supabase JSONB, + ollama JSONB, + customer_email VARCHAR, + first_name VARCHAR, + last_name VARCHAR, + company VARCHAR, + customer_status VARCHAR +) +SECURITY DEFINER +SET search_path = public +AS $$ +BEGIN + -- Only service_role can call this + IF auth.jwt()->>'role' != 'service_role' THEN + RAISE EXCEPTION 'Forbidden: service_role required'; + END IF; + + RETURN QUERY + SELECT + ic.id, + ic.customer_id, + ic.owner_user_id, + ic.ctid, + ic.hostname, + ic.fqdn, + ic.ip, + ic.vlan, + ic.status, + ic.created_at, + ic.urls, + ic.supabase, + ic.ollama, + ic.customer_email, + ic.first_name, + ic.last_name, + ic.company, + ic.customer_status + FROM api.instance_config ic + WHERE ic.ctid = ctid_param + LIMIT 1; +END; +$$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION api.get_instance_config_by_ctid(BIGINT) TO service_role; + +COMMENT ON FUNCTION api.get_instance_config_by_ctid IS 'Get instance configuration by CTID - service_role only'; + +-- ===================================================== +-- Step 7: Public config endpoint (NO auth required) +-- ===================================================== + +CREATE OR REPLACE FUNCTION api.get_public_config() +RETURNS TABLE ( + registration_webhook_url TEXT, + api_base_url TEXT +) +SECURITY DEFINER +SET search_path = public +AS $$ +BEGIN + RETURN QUERY + SELECT + 'https://api.botkonzept.de/webhook/botkonzept-registration'::TEXT as registration_webhook_url, + 'https://api.botkonzept.de'::TEXT as api_base_url; +END; +$$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION api.get_public_config() TO anon, authenticated, service_role; + +COMMENT ON FUNCTION api.get_public_config IS 'Get public configuration for website (registration webhook URL)'; + +-- ===================================================== +-- Step 8: Store installer JSON (Service Role ONLY) +-- ===================================================== + +CREATE OR REPLACE FUNCTION api.store_installer_json( + customer_email_param TEXT, + lxc_id_param BIGINT, + installer_json_param JSONB +) +RETURNS JSONB +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + instance_record RECORD; + result JSONB; +BEGIN + -- Only service_role can call this + IF auth.jwt()->>'role' != 'service_role' THEN + RAISE EXCEPTION 'Forbidden: service_role required'; + END IF; + + -- Find the instance by customer email and lxc_id + SELECT i.id, i.customer_id, c.id as auth_user_id INTO instance_record + FROM instances i + JOIN customers c ON i.customer_id = c.id + WHERE c.email = customer_email_param + AND i.lxc_id = lxc_id_param + LIMIT 1; + + IF NOT FOUND THEN + RETURN jsonb_build_object( + 'success', false, + 'error', 'Instance not found for customer email and LXC ID' + ); + END IF; + + -- Update the installer_json column + UPDATE instances + SET installer_json = installer_json_param, + updated_at = NOW() + WHERE id = instance_record.id; + + -- Return success + result := jsonb_build_object( + 'success', true, + 'instance_id', instance_record.id, + 'customer_id', instance_record.customer_id, + 'message', 'Installer JSON stored successfully' + ); + + RETURN result; +END; +$$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION api.store_installer_json(TEXT, BIGINT, JSONB) TO service_role; + +COMMENT ON FUNCTION api.store_installer_json IS 'Store installer JSON after instance creation - service_role only'; + +-- ===================================================== +-- Step 9: Link customer to Supabase Auth user +-- ===================================================== + +-- Function to link customer to Supabase Auth user (called during registration) +CREATE OR REPLACE FUNCTION api.link_customer_to_auth_user( + customer_email_param TEXT, + auth_user_id_param UUID +) +RETURNS JSONB +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + customer_record RECORD; + instance_record RECORD; + result JSONB; +BEGIN + -- Only service_role can call this + IF auth.jwt()->>'role' != 'service_role' THEN + RAISE EXCEPTION 'Forbidden: service_role required'; + END IF; + + -- Find customer by email + SELECT id INTO customer_record + FROM customers + WHERE email = customer_email_param + LIMIT 1; + + IF NOT FOUND THEN + RETURN jsonb_build_object( + 'success', false, + 'error', 'Customer not found' + ); + END IF; + + -- Update all instances for this customer with owner_user_id + UPDATE instances + SET owner_user_id = auth_user_id_param, + updated_at = NOW() + WHERE customer_id = customer_record.id; + + -- Return success + result := jsonb_build_object( + 'success', true, + 'customer_id', customer_record.id, + 'auth_user_id', auth_user_id_param, + 'message', 'Customer linked to auth user successfully' + ); + + RETURN result; +END; +$$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION api.link_customer_to_auth_user(TEXT, UUID) TO service_role; + +COMMENT ON FUNCTION api.link_customer_to_auth_user IS 'Link customer to Supabase Auth user - service_role only'; + +-- ===================================================== +-- Step 10: Audit logging +-- ===================================================== + +CREATE OR REPLACE FUNCTION api.log_config_access( + access_type TEXT, + ip_address_param INET DEFAULT NULL +) +RETURNS VOID +SECURITY DEFINER +SET search_path = public +AS $$ +BEGIN + -- Log access for authenticated user + IF auth.uid() IS NOT NULL THEN + INSERT INTO audit_log ( + customer_id, + action, + entity_type, + performed_by, + ip_address, + metadata + ) + SELECT + i.customer_id, + 'api_config_access', + 'instance_config', + auth.uid()::text, + ip_address_param, + jsonb_build_object('access_type', access_type) + FROM instances i + WHERE i.owner_user_id = auth.uid() + LIMIT 1; + END IF; +END; +$$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION api.log_config_access(TEXT, INET) TO authenticated, service_role; + +-- ===================================================== +-- Step 11: PostgREST API Routes +-- ===================================================== + +-- Available routes: +-- +-- 1. POST /rpc/get_my_instance_config +-- - Body: {} +-- - Returns instance config for authenticated user +-- - Requires: Supabase Auth JWT token +-- - Response: Single instance config object (or empty if not found) +-- +-- 2. POST /rpc/get_public_config +-- - Body: {} +-- - Returns public configuration (registration webhook URL) +-- - Requires: No authentication +-- +-- 3. POST /rpc/get_instance_config_by_ctid +-- - Body: {"ctid_param": 769697636} +-- - Returns instance config for specific CTID +-- - Requires: Service Role Key (backend only) +-- +-- 4. POST /rpc/store_installer_json +-- - Body: {"customer_email_param": "...", "lxc_id_param": 123, "installer_json_param": {...}} +-- - Stores installer JSON after instance creation +-- - Requires: Service Role Key (backend only) +-- +-- 5. POST /rpc/link_customer_to_auth_user +-- - Body: {"customer_email_param": "...", "auth_user_id_param": "..."} +-- - Links customer to Supabase Auth user +-- - Requires: Service Role Key (backend only) + +-- ===================================================== +-- Example Usage +-- ===================================================== + +-- Example 1: Get my instance config (authenticated user) +-- POST /rpc/get_my_instance_config +-- Headers: Authorization: Bearer +-- Body: {} + +-- Example 2: Get public config (no auth) +-- POST /rpc/get_public_config +-- Body: {} + +-- Example 3: Store installer JSON (service role) +-- POST /rpc/store_installer_json +-- Headers: Authorization: Bearer +-- Body: {"customer_email_param": "max@beispiel.de", "lxc_id_param": 769697636, "installer_json_param": {...}} + +-- Example 4: Link customer to auth user (service role) +-- POST /rpc/link_customer_to_auth_user +-- Headers: Authorization: Bearer +-- Body: {"customer_email_param": "max@beispiel.de", "auth_user_id_param": "550e8400-e29b-41d4-a716-446655440000"} + +-- ===================================================== +-- End of Supabase Auth API +-- ===================================================== diff --git a/sql/botkonzept_schema.sql b/sql/botkonzept_schema.sql index 0330350..ea7cedd 100644 --- a/sql/botkonzept_schema.sql +++ b/sql/botkonzept_schema.sql @@ -57,7 +57,7 @@ CREATE TABLE IF NOT EXISTS instances ( customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, -- Instance details - ctid BIGINT NOT NULL UNIQUE, + lxc_id BIGINT NOT NULL UNIQUE, hostname VARCHAR(255) NOT NULL, ip VARCHAR(50) NOT NULL, fqdn VARCHAR(255) NOT NULL, @@ -86,7 +86,7 @@ CREATE TABLE IF NOT EXISTS instances ( -- Create indexes for instances CREATE INDEX idx_instances_customer_id ON instances(customer_id); -CREATE INDEX idx_instances_ctid ON instances(ctid); +CREATE INDEX idx_instances_lxc_id ON instances(lxc_id); CREATE INDEX idx_instances_status ON instances(status); CREATE INDEX idx_instances_hostname ON instances(hostname); @@ -330,7 +330,7 @@ SELECT c.created_at, c.trial_end_date, EXTRACT(DAY FROM (c.trial_end_date - NOW())) as days_remaining, - i.ctid, + i.lxc_id, i.hostname, i.fqdn FROM customers c @@ -351,7 +351,7 @@ SELECT c.status, c.created_at, c.trial_end_date, - i.ctid, + i.lxc_id, i.hostname, i.fqdn, i.ip, diff --git a/test_installer_json_api.sh b/test_installer_json_api.sh new file mode 100644 index 0000000..321d4a9 --- /dev/null +++ b/test_installer_json_api.sh @@ -0,0 +1,365 @@ +#!/usr/bin/env bash +# ===================================================== +# Installer JSON API Test Script +# ===================================================== +# Tests all API endpoints and verifies functionality + +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source libraries +source "${SCRIPT_DIR}/libsupabase.sh" +source "${SCRIPT_DIR}/lib_installer_json_api.sh" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Test counters +TESTS_PASSED=0 +TESTS_FAILED=0 +TESTS_TOTAL=0 + +# Test configuration +TEST_CTID="${TEST_CTID:-769697636}" +TEST_EMAIL="${TEST_EMAIL:-test@example.com}" +TEST_POSTGREST_URL="${TEST_POSTGREST_URL:-http://192.168.45.104:3000}" +TEST_SERVICE_ROLE_KEY="${TEST_SERVICE_ROLE_KEY:-}" + +# Usage +usage() { + cat < Test CTID (default: 769697636) + --email Test email (default: test@example.com) + --postgrest-url PostgREST URL (default: http://192.168.45.104:3000) + --service-role-key Service role key for authenticated tests + --help Show this help + +Examples: + # Basic test (public endpoints only) + bash test_installer_json_api.sh + + # Full test with authentication + bash test_installer_json_api.sh --service-role-key "eyJhbGc..." + + # Test specific instance + bash test_installer_json_api.sh --ctid 769697636 --email max@beispiel.de +EOF +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --ctid) TEST_CTID="${2:-}"; shift 2 ;; + --email) TEST_EMAIL="${2:-}"; shift 2 ;; + --postgrest-url) TEST_POSTGREST_URL="${2:-}"; shift 2 ;; + --service-role-key) TEST_SERVICE_ROLE_KEY="${2:-}"; shift 2 ;; + --help|-h) usage; exit 0 ;; + *) echo "Unknown option: $1"; usage; exit 1 ;; + esac +done + +# Print functions +print_header() { + echo -e "\n${BLUE}========================================${NC}" + echo -e "${BLUE}$1${NC}" + echo -e "${BLUE}========================================${NC}\n" +} + +print_test() { + echo -e "${YELLOW}TEST $((TESTS_TOTAL + 1)):${NC} $1" +} + +print_pass() { + echo -e "${GREEN}✓ PASS${NC}: $1" + ((TESTS_PASSED++)) + ((TESTS_TOTAL++)) +} + +print_fail() { + echo -e "${RED}✗ FAIL${NC}: $1" + ((TESTS_FAILED++)) + ((TESTS_TOTAL++)) +} + +print_skip() { + echo -e "${YELLOW}⊘ SKIP${NC}: $1" +} + +print_info() { + echo -e "${BLUE}ℹ INFO${NC}: $1" +} + +# Test functions + +test_api_connectivity() { + print_test "API Connectivity" + + local response + local http_code + + response=$(curl -sS -w "\n%{http_code}" -X POST "${TEST_POSTGREST_URL}/rpc/get_public_config" \ + -H "Content-Type: application/json" \ + -d '{}' 2>&1 || echo -e "\nFAILED") + + http_code=$(echo "$response" | tail -n1) + + if [[ "$http_code" == "200" ]]; then + print_pass "API is reachable (HTTP 200)" + else + print_fail "API is not reachable (HTTP ${http_code})" + fi +} + +test_public_config() { + print_test "Get Public Config" + + local response + response=$(get_public_config "${TEST_POSTGREST_URL}" 2>/dev/null || echo "") + + if [[ -n "$response" ]]; then + # Check if response contains expected fields + if echo "$response" | grep -q "registration_webhook_url"; then + print_pass "Public config retrieved successfully" + print_info "Response: ${response}" + else + print_fail "Public config missing expected fields" + fi + else + print_fail "Failed to retrieve public config" + fi +} + +test_get_instance_by_email() { + print_test "Get Instance Config by Email" + + local response + response=$(get_installer_json_by_email "${TEST_EMAIL}" "${TEST_POSTGREST_URL}" 2>/dev/null || echo "") + + if [[ -n "$response" && "$response" != "[]" ]]; then + # Check if response contains expected fields + if echo "$response" | grep -q "ctid"; then + print_pass "Instance config retrieved by email" + + # Verify no secrets are exposed + if echo "$response" | grep -qE "password|service_role_key|jwt_secret|encryption_key"; then + print_fail "Response contains secrets (SECURITY ISSUE!)" + else + print_pass "No secrets exposed in response" + fi + + # Print sample of response + local ctid + ctid=$(echo "$response" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d[0]['ctid'] if d else 'N/A')" 2>/dev/null || echo "N/A") + print_info "Found CTID: ${ctid}" + else + print_fail "Instance config missing expected fields" + fi + else + print_skip "No instance found for email: ${TEST_EMAIL} (this is OK if instance doesn't exist)" + fi +} + +test_get_instance_by_ctid() { + print_test "Get Instance Config by CTID (requires service role key)" + + if [[ -z "$TEST_SERVICE_ROLE_KEY" ]]; then + print_skip "Service role key not provided (use --service-role-key)" + return + fi + + local response + response=$(get_installer_json_by_ctid "${TEST_CTID}" "${TEST_POSTGREST_URL}" "${TEST_SERVICE_ROLE_KEY}" 2>/dev/null || echo "") + + if [[ -n "$response" && "$response" != "[]" ]]; then + # Check if response contains expected fields + if echo "$response" | grep -q "ctid"; then + print_pass "Instance config retrieved by CTID" + + # Verify no secrets are exposed + if echo "$response" | grep -qE "password|service_role_key|jwt_secret|encryption_key"; then + print_fail "Response contains secrets (SECURITY ISSUE!)" + else + print_pass "No secrets exposed in response" + fi + else + print_fail "Instance config missing expected fields" + fi + else + print_skip "No instance found for CTID: ${TEST_CTID} (this is OK if instance doesn't exist)" + fi +} + +test_store_installer_json() { + print_test "Store Installer JSON (requires service role key)" + + if [[ -z "$TEST_SERVICE_ROLE_KEY" ]]; then + print_skip "Service role key not provided (use --service-role-key)" + return + fi + + # Create test JSON + local test_json + test_json=$(cat </dev/null || echo "") + + if [[ -n "$response" && "$response" != "[]" ]]; then + print_pass "Stored data can be retrieved" + + # Verify secrets are NOT in the response + if echo "$response" | grep -q "TEST_PASSWORD_SHOULD_NOT_BE_EXPOSED"; then + print_fail "CRITICAL: Passwords are exposed in API response!" + elif echo "$response" | grep -q "TEST_SERVICE_ROLE_KEY_SHOULD_NOT_BE_EXPOSED"; then + print_fail "CRITICAL: Service role key is exposed in API response!" + elif echo "$response" | grep -q "TEST_JWT_SECRET_SHOULD_NOT_BE_EXPOSED"; then + print_fail "CRITICAL: JWT secret is exposed in API response!" + elif echo "$response" | grep -q "TEST_ENCRYPTION_KEY_SHOULD_NOT_BE_EXPOSED"; then + print_fail "CRITICAL: Encryption key is exposed in API response!" + else + print_pass "SECURITY: All secrets are properly filtered" + fi + else + print_fail "Stored data could not be retrieved" + fi + else + print_skip "Failed to store installer JSON (instance may not exist in database)" + fi +} + +test_cors_headers() { + print_test "CORS Headers" + + local response + response=$(curl -sS -I -X OPTIONS "${TEST_POSTGREST_URL}/rpc/get_public_config" \ + -H "Origin: https://botkonzept.de" \ + -H "Access-Control-Request-Method: POST" 2>&1 || echo "") + + if echo "$response" | grep -qi "access-control-allow-origin"; then + print_pass "CORS headers are present" + else + print_skip "CORS headers not found (may need configuration)" + fi +} + +test_rate_limiting() { + print_test "Rate Limiting (optional)" + + print_skip "Rate limiting test not implemented (should be configured at nginx/gateway level)" +} + +test_response_format() { + print_test "Response Format Validation" + + local response + response=$(get_public_config "${TEST_POSTGREST_URL}" 2>/dev/null || echo "") + + if [[ -n "$response" ]]; then + # Validate JSON format + if echo "$response" | python3 -m json.tool >/dev/null 2>&1; then + print_pass "Response is valid JSON" + else + print_fail "Response is not valid JSON" + fi + else + print_fail "No response received" + fi +} + +# Main test execution +main() { + print_header "BotKonzept Installer JSON API Tests" + + echo "Test Configuration:" + echo " CTID: ${TEST_CTID}" + echo " Email: ${TEST_EMAIL}" + echo " PostgREST URL: ${TEST_POSTGREST_URL}" + echo " Service Role Key: ${TEST_SERVICE_ROLE_KEY:+***provided***}" + echo "" + + # Run tests + test_api_connectivity + test_public_config + test_response_format + test_cors_headers + test_get_instance_by_email + test_get_instance_by_ctid + test_store_installer_json + test_rate_limiting + + # Print summary + print_header "Test Summary" + + echo "Total Tests: ${TESTS_TOTAL}" + echo -e "${GREEN}Passed: ${TESTS_PASSED}${NC}" + echo -e "${RED}Failed: ${TESTS_FAILED}${NC}" + echo "" + + if [[ $TESTS_FAILED -eq 0 ]]; then + echo -e "${GREEN}✓ All tests passed!${NC}" + exit 0 + else + echo -e "${RED}✗ Some tests failed${NC}" + exit 1 + fi +} + +# Run main +main