chore: OpenCode-Konfiguration mit Ollama qwen3-coder:30b hinzugefügt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 20:12:52 +01:00
parent 6a5669e77d
commit da13e75b9f
11 changed files with 3505 additions and 4 deletions

22
.opencode.json Normal file
View File

@@ -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
}
}
}
}
}

511
API_DOCUMENTATION.md Normal file
View File

@@ -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 <SERVICE_ROLE_KEY>" \
-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 <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": "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 <USER_JWT_TOKEN>"
```
**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 <USER_JWT_TOKEN>
```
**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 <CTID> -- 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`

103
CLAUDE.md Normal file
View File

@@ -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-<unixts>
```
`install.sh` outputs a single JSON line to stdout with all credentials and URLs. Detailed logs go to `logs/<hostname>.log`. Credentials are saved to `credentials/<hostname>.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 511)
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/<hostname>.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-<unix_timestamp>`; 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

View File

@@ -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 <SERVICE_ROLE_KEY>" \
-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`

467
SUPABASE_AUTH_API_TESTS.md Normal file
View File

@@ -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: <SUPABASE_ANON_KEY>" \
-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="<token_for_user_without_instance>"
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="<your_anon_key>"
SERVICE_ROLE_KEY="<your_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',
'<ANON_KEY>' // 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

325
lib_installer_json_api.sh Normal file
View File

@@ -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 <ctid> <customer_email> <postgrest_url> <service_role_key> <json_output>
# 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 <<EOF
{
"customer_email_param": "${customer_email}",
"lxc_id_param": ${ctid},
"installer_json_param": ${json_output}
}
EOF
)
# Make API request
local response
local http_code
response=$(curl -sS -w "\n%{http_code}" -X POST "${postgrest_url}/rpc/store_installer_json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${service_role_key}" \
-H "Prefer: return=representation" \
-d "${payload}" 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
# 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 <customer_email> <postgrest_url>
# 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 <<EOF
{
"customer_email_param": "${customer_email}"
}
EOF
)
# Make API request
local response
local http_code
response=$(curl -sS -w "\n%{http_code}" -X POST "${postgrest_url}/rpc/get_instance_config_by_email" \
-H "Content-Type: application/json" \
-d "${payload}" 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
# 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 <ctid> <postgrest_url> <service_role_key>
# 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 <<EOF
{
"ctid_param": ${ctid}
}
EOF
)
# Make API request
local response
local http_code
response=$(curl -sS -w "\n%{http_code}" -X POST "${postgrest_url}/rpc/get_instance_config_by_ctid" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${service_role_key}" \
-d "${payload}" 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
# 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 <postgrest_url>
# 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 <ctid>
# 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 <postgrest_url>
# 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 <ctid> <customer_email> <postgrest_url>
# 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

426
setup_botkonzept_lxc.sh Executable file
View File

@@ -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}" <<EOF
{
"lxc": {
"lxc_id": ${CTID},
"hostname": "${HOSTNAME}",
"ip": "${CONTAINER_IP}",
"cores": ${CORES},
"memory": ${MEMORY},
"disk": ${DISK}
},
"n8n": {
"url_internal": "http://${CONTAINER_IP}:5678",
"url_external": "http://${CONTAINER_IP}:5678",
"owner_email": "${N8N_OWNER_EMAIL}",
"owner_password": "${N8N_OWNER_PASSWORD}",
"encryption_key": "${N8N_ENCRYPTION_KEY}",
"webhook_base": "http://${CONTAINER_IP}:5678/webhook"
},
"postgresql": {
"host": "localhost",
"port": 5432,
"database": "botkonzept",
"user": "postgres",
"password": "${PG_PASSWORD}"
},
"workflows": {
"registration": "/opt/n8n/registration-workflow.json",
"trial_management": "/opt/n8n/trial-workflow.json"
},
"frontend": {
"test_url": "http://192.168.0.20:8000",
"webhook_url": "http://${CONTAINER_IP}:5678/webhook/botkonzept-registration"
}
}
EOF
log_success "Credentials gespeichert: ${CREDENTIALS_FILE}"
# =====================================================
# Zusammenfassung
# =====================================================
echo ""
echo "=========================================="
echo " BotKonzept LXC Setup abgeschlossen! ✅"
echo "=========================================="
echo ""
echo "LXC Details:"
echo " CTID: ${CTID}"
echo " Hostname: ${HOSTNAME}"
echo " IP: ${CONTAINER_IP}"
echo ""
echo "n8n:"
echo " URL: http://${CONTAINER_IP}:5678"
echo " E-Mail: ${N8N_OWNER_EMAIL}"
echo " Passwort: ${N8N_OWNER_PASSWORD}"
echo ""
echo "PostgreSQL:"
echo " Host: localhost (im Container)"
echo " Database: botkonzept"
echo " User: postgres"
echo " Passwort: ${PG_PASSWORD}"
echo ""
echo "Nächste Schritte:"
echo " 1. n8n öffnen: http://${CONTAINER_IP}:5678"
echo " 2. Mit obigen Credentials einloggen"
echo " 3. Workflows importieren:"
echo " - /opt/n8n/registration-workflow.json"
echo " - /opt/n8n/trial-workflow.json"
echo " 4. Credentials in n8n erstellen (siehe QUICK_START.md)"
echo " 5. Workflows aktivieren"
echo " 6. Frontend Webhook-URL aktualisieren:"
echo " http://${CONTAINER_IP}:5678/webhook/botkonzept-registration"
echo ""
echo "Credentials-Datei: ${CREDENTIALS_FILE}"
echo "=========================================="

View File

@@ -0,0 +1,378 @@
-- =====================================================
-- BotKonzept - Installer JSON API Extension
-- =====================================================
-- Extends the database schema to store and expose installer JSON data
-- safely to frontend clients (without secrets)
-- =====================================================
-- Step 1: Add installer_json column to instances table
-- =====================================================
-- Add column to store the complete installer JSON
ALTER TABLE instances
ADD COLUMN IF NOT EXISTS installer_json JSONB DEFAULT '{}'::jsonb;
-- Create index for faster JSON queries
CREATE INDEX IF NOT EXISTS idx_instances_installer_json ON instances USING gin(installer_json);
-- Add comment
COMMENT ON COLUMN instances.installer_json IS 'Complete installer JSON output from install.sh (includes secrets - use api.instance_config view for safe access)';
-- =====================================================
-- Step 2: Create safe API view (NON-SECRET data only)
-- =====================================================
-- Create API schema if it doesn't exist
CREATE SCHEMA IF NOT EXISTS api;
-- Grant usage on api schema
GRANT USAGE ON SCHEMA api TO anon, authenticated, service_role;
-- Create view that exposes only safe (non-secret) installer data
CREATE OR REPLACE VIEW api.instance_config AS
SELECT
i.id,
i.customer_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
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;
-- 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
-- =====================================================

View File

@@ -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 <USER_JWT_TOKEN>
-- 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 <SERVICE_ROLE_KEY>
-- 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 <SERVICE_ROLE_KEY>
-- Body: {"customer_email_param": "max@beispiel.de", "auth_user_id_param": "550e8400-e29b-41d4-a716-446655440000"}
-- =====================================================
-- End of Supabase Auth API
-- =====================================================

View File

@@ -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,

365
test_installer_json_api.sh Normal file
View File

@@ -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 <<EOF
Usage: bash test_installer_json_api.sh [options]
Options:
--ctid <id> Test CTID (default: 769697636)
--email <email> Test email (default: test@example.com)
--postgrest-url <url> PostgREST URL (default: http://192.168.45.104:3000)
--service-role-key <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 <<EOF
{
"ctid": ${TEST_CTID},
"hostname": "sb-${TEST_CTID}",
"fqdn": "sb-${TEST_CTID}.userman.de",
"ip": "192.168.45.104",
"vlan": 90,
"urls": {
"n8n_internal": "http://192.168.45.104:5678/",
"n8n_external": "https://sb-${TEST_CTID}.userman.de",
"postgrest": "http://192.168.45.104:3000",
"chat_webhook": "https://sb-${TEST_CTID}.userman.de/webhook/rag-chat-webhook/chat",
"chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat",
"upload_form": "https://sb-${TEST_CTID}.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": "TEST_PASSWORD_SHOULD_NOT_BE_EXPOSED"
},
"supabase": {
"url": "http://postgrest:3000",
"url_external": "http://192.168.45.104:3000",
"anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.TEST",
"service_role_key": "TEST_SERVICE_ROLE_KEY_SHOULD_NOT_BE_EXPOSED",
"jwt_secret": "TEST_JWT_SECRET_SHOULD_NOT_BE_EXPOSED"
},
"ollama": {
"url": "http://192.168.45.3:11434",
"model": "ministral-3:3b",
"embedding_model": "nomic-embed-text:latest"
},
"n8n": {
"encryption_key": "TEST_ENCRYPTION_KEY_SHOULD_NOT_BE_EXPOSED",
"owner_email": "admin@userman.de",
"owner_password": "TEST_PASSWORD_SHOULD_NOT_BE_EXPOSED",
"secure_cookie": false
}
}
EOF
)
# Try to store
if store_installer_json_in_db "${TEST_CTID}" "${TEST_EMAIL}" "${TEST_POSTGREST_URL}" "${TEST_SERVICE_ROLE_KEY}" "${test_json}"; then
print_pass "Installer JSON stored successfully"
# Verify it was stored
sleep 1
local response
response=$(get_installer_json_by_email "${TEST_EMAIL}" "${TEST_POSTGREST_URL}" 2>/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