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
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
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
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
+428
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
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
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
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 "=========================================="
+378
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
-- =====================================================
@@ -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
-- =====================================================
+4 -4
View File
@@ -57,7 +57,7 @@ CREATE TABLE IF NOT EXISTS instances (
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
-- Instance details -- Instance details
ctid BIGINT NOT NULL UNIQUE, lxc_id BIGINT NOT NULL UNIQUE,
hostname VARCHAR(255) NOT NULL, hostname VARCHAR(255) NOT NULL,
ip VARCHAR(50) NOT NULL, ip VARCHAR(50) NOT NULL,
fqdn VARCHAR(255) NOT NULL, fqdn VARCHAR(255) NOT NULL,
@@ -86,7 +86,7 @@ CREATE TABLE IF NOT EXISTS instances (
-- Create indexes for instances -- Create indexes for instances
CREATE INDEX idx_instances_customer_id ON instances(customer_id); 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_status ON instances(status);
CREATE INDEX idx_instances_hostname ON instances(hostname); CREATE INDEX idx_instances_hostname ON instances(hostname);
@@ -330,7 +330,7 @@ SELECT
c.created_at, c.created_at,
c.trial_end_date, c.trial_end_date,
EXTRACT(DAY FROM (c.trial_end_date - NOW())) as days_remaining, EXTRACT(DAY FROM (c.trial_end_date - NOW())) as days_remaining,
i.ctid, i.lxc_id,
i.hostname, i.hostname,
i.fqdn i.fqdn
FROM customers c FROM customers c
@@ -351,7 +351,7 @@ SELECT
c.status, c.status,
c.created_at, c.created_at,
c.trial_end_date, c.trial_end_date,
i.ctid, i.lxc_id,
i.hostname, i.hostname,
i.fqdn, i.fqdn,
i.ip, i.ip,
+365
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