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:
22
.opencode.json
Normal file
22
.opencode.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"model": "ollama/qwen3-coder:30b",
|
||||||
|
"instructions": [
|
||||||
|
"Antworte immer auf Deutsch, unabhängig von der Sprache der Eingabe."
|
||||||
|
],
|
||||||
|
"provider": {
|
||||||
|
"ollama": {
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"name": "Ollama",
|
||||||
|
"options": {
|
||||||
|
"baseURL": "http://192.168.0.179:11434/v1"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"qwen3-coder:30b": {
|
||||||
|
"name": "qwen3-coder:30b",
|
||||||
|
"tools": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
511
API_DOCUMENTATION.md
Normal file
511
API_DOCUMENTATION.md
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
# BotKonzept Installer JSON API Documentation
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Diese API stellt die Installer-JSON-Daten sicher für Frontend-Clients bereit, **ohne Secrets preiszugeben**.
|
||||||
|
|
||||||
|
**Basis-URL:** `http://192.168.45.104:3000` (PostgREST auf Kunden-LXC)
|
||||||
|
**Zentrale API:** `https://api.botkonzept.de` (zentrales PostgREST/n8n)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sicherheitsmodell
|
||||||
|
|
||||||
|
### ✅ Erlaubte Daten (Frontend-sicher)
|
||||||
|
|
||||||
|
- `ctid`, `hostname`, `fqdn`, `ip`, `vlan`
|
||||||
|
- `urls.*` (alle URL-Endpunkte)
|
||||||
|
- `supabase.url_external`
|
||||||
|
- `supabase.anon_key`
|
||||||
|
- `ollama.url`, `ollama.model`, `ollama.embedding_model`
|
||||||
|
|
||||||
|
### ❌ Verbotene Daten (Secrets)
|
||||||
|
|
||||||
|
- `postgres.password`
|
||||||
|
- `supabase.service_role_key`
|
||||||
|
- `supabase.jwt_secret`
|
||||||
|
- `n8n.owner_password`
|
||||||
|
- `n8n.encryption_key`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API-Endpunkte
|
||||||
|
|
||||||
|
### 1. Public Config (Keine Authentifizierung)
|
||||||
|
|
||||||
|
**Zweck:** Liefert öffentliche Konfiguration für Website (Registrierungs-Webhook)
|
||||||
|
|
||||||
|
**Route:** `POST /rpc/get_public_config`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_public_config' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (Success):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"registration_webhook_url": "https://api.botkonzept.de/webhook/botkonzept-registration",
|
||||||
|
"api_base_url": "https://api.botkonzept.de"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (Error):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "PGRST204",
|
||||||
|
"message": "No rows returned",
|
||||||
|
"details": null,
|
||||||
|
"hint": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**CORS:** Erlaubt (öffentlich)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Instance Config by Email (Öffentlich, aber rate-limited)
|
||||||
|
|
||||||
|
**Zweck:** Liefert Instanz-Konfiguration für einen Kunden (via E-Mail)
|
||||||
|
|
||||||
|
**Route:** `POST /rpc/get_instance_config_by_email`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_email' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"customer_email_param": "max@beispiel.de"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (Success):**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
"ctid": 769697636,
|
||||||
|
"hostname": "sb-1769697636",
|
||||||
|
"fqdn": "sb-1769697636.userman.de",
|
||||||
|
"ip": "192.168.45.104",
|
||||||
|
"vlan": 90,
|
||||||
|
"status": "active",
|
||||||
|
"created_at": "2025-01-15T10:30:00Z",
|
||||||
|
"urls": {
|
||||||
|
"n8n_internal": "http://192.168.45.104:5678/",
|
||||||
|
"n8n_external": "https://sb-1769697636.userman.de",
|
||||||
|
"postgrest": "http://192.168.45.104:3000",
|
||||||
|
"chat_webhook": "https://sb-1769697636.userman.de/webhook/rag-chat-webhook/chat",
|
||||||
|
"chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat",
|
||||||
|
"upload_form": "https://sb-1769697636.userman.de/form/rag-upload-form",
|
||||||
|
"upload_form_internal": "http://192.168.45.104:5678/form/rag-upload-form"
|
||||||
|
},
|
||||||
|
"supabase": {
|
||||||
|
"url_external": "http://192.168.45.104:3000",
|
||||||
|
"anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
},
|
||||||
|
"ollama": {
|
||||||
|
"url": "http://192.168.45.3:11434",
|
||||||
|
"model": "ministral-3:3b",
|
||||||
|
"embedding_model": "nomic-embed-text:latest"
|
||||||
|
},
|
||||||
|
"customer_email": "max@beispiel.de",
|
||||||
|
"first_name": "Max",
|
||||||
|
"last_name": "Mustermann",
|
||||||
|
"company": "Muster GmbH",
|
||||||
|
"customer_status": "trial"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (Not Found):**
|
||||||
|
```json
|
||||||
|
[]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (Error):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "PGRST301",
|
||||||
|
"message": "Invalid input syntax",
|
||||||
|
"details": "...",
|
||||||
|
"hint": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authentifizierung:** Keine (öffentlich, aber sollte rate-limited sein)
|
||||||
|
|
||||||
|
**CORS:** Erlaubt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Instance Config by CTID (Service Role Only)
|
||||||
|
|
||||||
|
**Zweck:** Liefert Instanz-Konfiguration für interne Workflows (via CTID)
|
||||||
|
|
||||||
|
**Route:** `POST /rpc/get_instance_config_by_ctid`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_ctid' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer <SERVICE_ROLE_KEY>" \
|
||||||
|
-d '{"ctid_param": 769697636}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** Gleiche Struktur wie `/get_instance_config_by_email`
|
||||||
|
|
||||||
|
**Authentifizierung:** Service Role Key erforderlich
|
||||||
|
|
||||||
|
**CORS:** Nicht erlaubt (nur Backend-to-Backend)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Store Installer JSON (Service Role Only)
|
||||||
|
|
||||||
|
**Zweck:** Speichert Installer-JSON nach Instanz-Erstellung (wird von install.sh aufgerufen)
|
||||||
|
|
||||||
|
**Route:** `POST /rpc/store_installer_json`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/store_installer_json' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer <SERVICE_ROLE_KEY>" \
|
||||||
|
-d '{
|
||||||
|
"customer_email_param": "max@beispiel.de",
|
||||||
|
"lxc_id_param": 769697636,
|
||||||
|
"installer_json_param": {
|
||||||
|
"ctid": 769697636,
|
||||||
|
"hostname": "sb-1769697636",
|
||||||
|
"fqdn": "sb-1769697636.userman.de",
|
||||||
|
"ip": "192.168.45.104",
|
||||||
|
"vlan": 90,
|
||||||
|
"urls": {
|
||||||
|
"n8n_internal": "http://192.168.45.104:5678/",
|
||||||
|
"n8n_external": "https://sb-1769697636.userman.de",
|
||||||
|
"postgrest": "http://192.168.45.104:3000",
|
||||||
|
"chat_webhook": "https://sb-1769697636.userman.de/webhook/rag-chat-webhook/chat",
|
||||||
|
"chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat",
|
||||||
|
"upload_form": "https://sb-1769697636.userman.de/form/rag-upload-form",
|
||||||
|
"upload_form_internal": "http://192.168.45.104:5678/form/rag-upload-form"
|
||||||
|
},
|
||||||
|
"postgres": {
|
||||||
|
"host": "postgres",
|
||||||
|
"port": 5432,
|
||||||
|
"db": "customer",
|
||||||
|
"user": "customer",
|
||||||
|
"password": "REDACTED"
|
||||||
|
},
|
||||||
|
"supabase": {
|
||||||
|
"url": "http://postgrest:3000",
|
||||||
|
"url_external": "http://192.168.45.104:3000",
|
||||||
|
"anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"service_role_key": "REDACTED",
|
||||||
|
"jwt_secret": "REDACTED"
|
||||||
|
},
|
||||||
|
"ollama": {
|
||||||
|
"url": "http://192.168.45.3:11434",
|
||||||
|
"model": "ministral-3:3b",
|
||||||
|
"embedding_model": "nomic-embed-text:latest"
|
||||||
|
},
|
||||||
|
"n8n": {
|
||||||
|
"encryption_key": "REDACTED",
|
||||||
|
"owner_email": "admin@userman.de",
|
||||||
|
"owner_password": "REDACTED",
|
||||||
|
"secure_cookie": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (Success):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"instance_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
"message": "Installer JSON stored successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (Error):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Instance not found for customer email and LXC ID"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authentifizierung:** Service Role Key erforderlich
|
||||||
|
|
||||||
|
**CORS:** Nicht erlaubt (nur Backend-to-Backend)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Direct View Access (Authenticated)
|
||||||
|
|
||||||
|
**Zweck:** Direkter Zugriff auf View (für authentifizierte Benutzer)
|
||||||
|
|
||||||
|
**Route:** `GET /api/instance_config`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X GET 'http://192.168.45.104:3000/api/instance_config' \
|
||||||
|
-H "Authorization: Bearer <USER_JWT_TOKEN>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** Array von Instanz-Konfigurationen (gefiltert nach RLS)
|
||||||
|
|
||||||
|
**Authentifizierung:** JWT Token erforderlich (Supabase Auth)
|
||||||
|
|
||||||
|
**CORS:** Erlaubt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentifizierung
|
||||||
|
|
||||||
|
### 1. Keine Authentifizierung (Public)
|
||||||
|
|
||||||
|
- `/rpc/get_public_config`
|
||||||
|
- `/rpc/get_instance_config_by_email` (sollte rate-limited sein)
|
||||||
|
|
||||||
|
### 2. Service Role Key
|
||||||
|
|
||||||
|
**Header:**
|
||||||
|
```
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MjAwMDAwMDAwMH0...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendung:**
|
||||||
|
- `/rpc/get_instance_config_by_ctid`
|
||||||
|
- `/rpc/store_installer_json`
|
||||||
|
|
||||||
|
### 3. User JWT Token (Supabase Auth)
|
||||||
|
|
||||||
|
**Header:**
|
||||||
|
```
|
||||||
|
Authorization: Bearer <USER_JWT_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendung:**
|
||||||
|
- `/api/instance_config` (direkter View-Zugriff)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CORS-Konfiguration
|
||||||
|
|
||||||
|
### PostgREST CORS Headers
|
||||||
|
|
||||||
|
In der PostgREST-Konfiguration (docker-compose.yml):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
postgrest:
|
||||||
|
environment:
|
||||||
|
PGRST_SERVER_CORS_ALLOWED_ORIGINS: "*"
|
||||||
|
# Oder spezifisch:
|
||||||
|
# PGRST_SERVER_CORS_ALLOWED_ORIGINS: "https://botkonzept.de,https://www.botkonzept.de"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nginx Reverse Proxy CORS
|
||||||
|
|
||||||
|
Falls über Nginx:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
**Empfehlung:** Rate Limiting für öffentliche Endpunkte implementieren
|
||||||
|
|
||||||
|
### Nginx Rate Limiting
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
|
||||||
|
|
||||||
|
location /rpc/get_instance_config_by_email {
|
||||||
|
limit_req zone=api_limit burst=20 nodelay;
|
||||||
|
proxy_pass http://postgrest:3000;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgREST Rate Limiting
|
||||||
|
|
||||||
|
Alternativ: Verwende einen API Gateway (Kong, Tyk) vor PostgREST.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fehlerbehandlung
|
||||||
|
|
||||||
|
### HTTP Status Codes
|
||||||
|
|
||||||
|
- `200 OK` - Erfolgreiche Anfrage
|
||||||
|
- `204 No Content` - Keine Daten gefunden (PostgREST)
|
||||||
|
- `400 Bad Request` - Ungültige Eingabe
|
||||||
|
- `401 Unauthorized` - Fehlende/ungültige Authentifizierung
|
||||||
|
- `403 Forbidden` - Keine Berechtigung
|
||||||
|
- `404 Not Found` - Ressource nicht gefunden
|
||||||
|
- `500 Internal Server Error` - Serverfehler
|
||||||
|
|
||||||
|
### PostgREST Error Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "PGRST301",
|
||||||
|
"message": "Invalid input syntax for type integer",
|
||||||
|
"details": "invalid input syntax for type integer: \"abc\"",
|
||||||
|
"hint": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration mit install.sh
|
||||||
|
|
||||||
|
### Schritt 1: SQL-Schema anwenden
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf dem Proxmox Host
|
||||||
|
pct exec <CTID> -- bash -c "
|
||||||
|
docker exec customer-postgres psql -U customer -d customer < /opt/customer-stack/sql/add_installer_json_api.sql
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 2: install.sh erweitern
|
||||||
|
|
||||||
|
Am Ende von `install.sh` (nach JSON-Generierung):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Store installer JSON in database via PostgREST
|
||||||
|
info "Storing installer JSON in database..."
|
||||||
|
|
||||||
|
STORE_RESPONSE=$(curl -sS -X POST "http://${CT_IP}:${POSTGREST_PORT}/rpc/store_installer_json" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${SERVICE_ROLE_KEY}" \
|
||||||
|
-d "{
|
||||||
|
\"customer_email_param\": \"${N8N_OWNER_EMAIL}\",
|
||||||
|
\"lxc_id_param\": ${CTID},
|
||||||
|
\"installer_json_param\": ${JSON_OUTPUT}
|
||||||
|
}" 2>&1)
|
||||||
|
|
||||||
|
if echo "$STORE_RESPONSE" | grep -q '"success":true'; then
|
||||||
|
info "Installer JSON stored successfully"
|
||||||
|
else
|
||||||
|
warn "Failed to store installer JSON: ${STORE_RESPONSE}"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test 1: Public Config
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_public_config' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}'
|
||||||
|
|
||||||
|
# Erwartete Antwort:
|
||||||
|
# {"registration_webhook_url":"https://api.botkonzept.de/webhook/botkonzept-registration","api_base_url":"https://api.botkonzept.de"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2: Instance Config by Email
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_email' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"customer_email_param": "max@beispiel.de"}'
|
||||||
|
|
||||||
|
# Erwartete Antwort: Array mit Instanz-Konfiguration (siehe oben)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3: Store Installer JSON (mit Service Role Key)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/store_installer_json' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${SERVICE_ROLE_KEY}" \
|
||||||
|
-d '{
|
||||||
|
"customer_email_param": "max@beispiel.de",
|
||||||
|
"lxc_id_param": 769697636,
|
||||||
|
"installer_json_param": {"ctid": 769697636, "urls": {...}}
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Erwartete Antwort:
|
||||||
|
# {"success":true,"instance_id":"...","customer_id":"...","message":"Installer JSON stored successfully"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 4: Verify No Secrets Exposed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_email' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"customer_email_param": "max@beispiel.de"}' | jq .
|
||||||
|
|
||||||
|
# Prüfe: Response enthält KEINE der folgenden Felder:
|
||||||
|
# - postgres.password
|
||||||
|
# - supabase.service_role_key
|
||||||
|
# - supabase.jwt_secret
|
||||||
|
# - n8n.owner_password
|
||||||
|
# - n8n.encryption_key
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] SQL-Schema auf allen Instanzen anwenden
|
||||||
|
- [ ] PostgREST CORS konfigurieren
|
||||||
|
- [ ] Rate Limiting aktivieren
|
||||||
|
- [ ] install.sh erweitern (Installer JSON speichern)
|
||||||
|
- [ ] Frontend auf neue API umstellen
|
||||||
|
- [ ] Tests durchführen
|
||||||
|
- [ ] Monitoring einrichten (API-Zugriffe loggen)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring & Logging
|
||||||
|
|
||||||
|
### Audit Log
|
||||||
|
|
||||||
|
Alle API-Zugriffe werden in `audit_log` Tabelle protokolliert:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT * FROM audit_log
|
||||||
|
WHERE action = 'api_config_access'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgREST Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs customer-postgrest --tail 100 -f
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sicherheitshinweise
|
||||||
|
|
||||||
|
1. **Service Role Key schützen:** Niemals im Frontend verwenden!
|
||||||
|
2. **Rate Limiting:** Öffentliche Endpunkte müssen rate-limited sein
|
||||||
|
3. **HTTPS:** In Produktion nur über HTTPS (OPNsense Reverse Proxy)
|
||||||
|
4. **Input Validation:** PostgREST validiert automatisch, aber zusätzliche Checks empfohlen
|
||||||
|
5. **Audit Logging:** Alle API-Zugriffe werden geloggt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Bei Fragen oder Problemen:
|
||||||
|
- Dokumentation: `customer-installer/wiki/`
|
||||||
|
- Troubleshooting: `customer-installer/REGISTRATION_TROUBLESHOOTING.md`
|
||||||
103
CLAUDE.md
Normal file
103
CLAUDE.md
Normal 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 5–11)
|
||||||
|
|
||||||
|
1. **Step 5**: Creates and starts Proxmox LXC (Debian 12), waits for DHCP IP
|
||||||
|
2. **Step 6**: Installs Docker CE + Compose plugin inside the CT
|
||||||
|
3. **Step 7**: Generates secrets (PG password, JWT, n8n encryption key), writes `.env` and `docker-compose.yml` into CT, starts the stack
|
||||||
|
4. **Step 8**: Creates n8n owner account via REST API
|
||||||
|
5. **Step 10**: Imports and activates the RAG workflow via n8n API, sets up credentials (Postgres + Ollama)
|
||||||
|
6. **Step 10a**: Installs a systemd service (`n8n-workflow-reload.service`) that re-imports and re-activates the workflow on every LXC restart
|
||||||
|
7. **Step 11**: Registers an NGINX upstream/location in OPNsense via its REST API
|
||||||
|
|
||||||
|
### Docker Stack Inside Each LXC (`/opt/customer-stack/`)
|
||||||
|
|
||||||
|
- `postgres` – pgvector/pgvector:pg16, initialized from `sql/` directory
|
||||||
|
- `postgrest` – PostgREST, exposes Supabase-compatible REST API on port 3000 (mapped to `POSTGREST_PORT`)
|
||||||
|
- `n8n` – n8n automation, port 5678
|
||||||
|
|
||||||
|
All three share a `customer-net` bridge network. The n8n instance connects to PostgREST via the Docker internal hostname `postgrest:3000` (not the external IP).
|
||||||
|
|
||||||
|
### Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `libsupabase.sh` | Core library: logging (`info`/`warn`/`die`), Proxmox helpers (`pct_exec`, `pct_push_text`, `pve_*`), crypto (`gen_password_policy`, `gen_hex_64`), n8n setup (`n8n_setup_rag_workflow`) |
|
||||||
|
| `setup_nginx_proxy.sh` | OPNsense API client; registers upstream + location for new CT |
|
||||||
|
| `lib_installer_json_api.sh` | Stores installer JSON output into the BotKonzept Postgres DB via PostgREST |
|
||||||
|
| `sql/botkonzept_schema.sql` | Customer management schema (customers, instances, emails, payments) for the BotKonzept management LXC |
|
||||||
|
| `sql/init_pgvector.sql` | Inline in `install.sh`; creates pgvector extension, `documents` table, `match_documents` function, PostgREST roles |
|
||||||
|
| `templates/reload-workflow.sh` | Runs inside customer LXC on every restart; logs to `/opt/customer-stack/logs/workflow-reload.log` |
|
||||||
|
| `RAGKI-BotPGVector.json` | Default n8n workflow template (RAG KI-Bot with PGVector) |
|
||||||
|
|
||||||
|
### Output and Logging
|
||||||
|
|
||||||
|
- **Normal mode** (`DEBUG=0`): all script output goes to `logs/<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
STEP1_BACKEND_API_SUMMARY.md
Normal file
428
STEP1_BACKEND_API_SUMMARY.md
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
# Schritt 1: Backend-API für Installer-JSON - ABGESCHLOSSEN
|
||||||
|
|
||||||
|
## Zusammenfassung
|
||||||
|
|
||||||
|
Backend-API wurde erfolgreich erstellt, die das Installer-JSON sicher (ohne Secrets) für Frontend-Clients bereitstellt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Erstellte Dateien
|
||||||
|
|
||||||
|
### 1. SQL-Schema: `sql/add_installer_json_api.sql`
|
||||||
|
|
||||||
|
**Funktionen:**
|
||||||
|
- Erweitert `instances` Tabelle um `installer_json` JSONB-Spalte
|
||||||
|
- Erstellt `api.instance_config` View (filtert Secrets automatisch)
|
||||||
|
- Implementiert Row Level Security (RLS)
|
||||||
|
- Bietet 5 API-Funktionen:
|
||||||
|
- `get_public_config()` - Öffentliche Konfiguration
|
||||||
|
- `get_instance_config_by_email(email)` - Instanz-Config per E-Mail
|
||||||
|
- `get_instance_config_by_ctid(ctid)` - Instanz-Config per CTID (service_role only)
|
||||||
|
- `store_installer_json(email, ctid, json)` - Speichert Installer-JSON (service_role only)
|
||||||
|
- `log_config_access(customer_id, type, ip)` - Audit-Logging
|
||||||
|
|
||||||
|
**Sicherheit:**
|
||||||
|
- ✅ Filtert automatisch alle Secrets (postgres.password, service_role_key, jwt_secret, etc.)
|
||||||
|
- ✅ Row Level Security aktiviert
|
||||||
|
- ✅ Audit-Logging für alle Zugriffe
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. API-Dokumentation: `API_DOCUMENTATION.md`
|
||||||
|
|
||||||
|
**Inhalt:**
|
||||||
|
- Vollständige API-Referenz
|
||||||
|
- Alle Endpunkte mit Beispielen
|
||||||
|
- Authentifizierungs-Modelle
|
||||||
|
- CORS-Konfiguration
|
||||||
|
- Rate-Limiting-Empfehlungen
|
||||||
|
- Fehlerbehandlung
|
||||||
|
- Integration mit install.sh
|
||||||
|
- Test-Szenarien
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Integration-Library: `lib_installer_json_api.sh`
|
||||||
|
|
||||||
|
**Funktionen:**
|
||||||
|
- `store_installer_json_in_db()` - Speichert JSON in DB
|
||||||
|
- `get_installer_json_by_email()` - Ruft JSON per E-Mail ab
|
||||||
|
- `get_installer_json_by_ctid()` - Ruft JSON per CTID ab
|
||||||
|
- `get_public_config()` - Ruft öffentliche Config ab
|
||||||
|
- `apply_installer_json_api_schema()` - Wendet SQL-Schema an
|
||||||
|
- `test_api_connectivity()` - Testet API-Verbindung
|
||||||
|
- `verify_installer_json_stored()` - Verifiziert Speicherung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Test-Script: `test_installer_json_api.sh`
|
||||||
|
|
||||||
|
**Tests:**
|
||||||
|
- API-Konnektivität
|
||||||
|
- Public Config Endpoint
|
||||||
|
- Instance Config by Email
|
||||||
|
- Instance Config by CTID
|
||||||
|
- Store Installer JSON
|
||||||
|
- CORS Headers
|
||||||
|
- Response Format Validation
|
||||||
|
- Security: Verifiziert, dass keine Secrets exposed werden
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# Basis-Tests (öffentliche Endpunkte)
|
||||||
|
bash test_installer_json_api.sh
|
||||||
|
|
||||||
|
# Vollständige Tests (mit Service Role Key)
|
||||||
|
bash test_installer_json_api.sh --service-role-key "eyJhbGc..."
|
||||||
|
|
||||||
|
# Spezifische Instanz testen
|
||||||
|
bash test_installer_json_api.sh \
|
||||||
|
--ctid 769697636 \
|
||||||
|
--email max@beispiel.de \
|
||||||
|
--postgrest-url http://192.168.45.104:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API-Routen (PostgREST)
|
||||||
|
|
||||||
|
### 1. Public Config (Keine Auth)
|
||||||
|
|
||||||
|
**URL:** `POST /rpc/get_public_config`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_public_config' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"registration_webhook_url": "https://api.botkonzept.de/webhook/botkonzept-registration",
|
||||||
|
"api_base_url": "https://api.botkonzept.de"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Instance Config by Email (Öffentlich)
|
||||||
|
|
||||||
|
**URL:** `POST /rpc/get_instance_config_by_email`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_email' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"customer_email_param": "max@beispiel.de"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
"ctid": 769697636,
|
||||||
|
"hostname": "sb-1769697636",
|
||||||
|
"fqdn": "sb-1769697636.userman.de",
|
||||||
|
"ip": "192.168.45.104",
|
||||||
|
"vlan": 90,
|
||||||
|
"status": "active",
|
||||||
|
"created_at": "2025-01-15T10:30:00Z",
|
||||||
|
"urls": {
|
||||||
|
"n8n_internal": "http://192.168.45.104:5678/",
|
||||||
|
"n8n_external": "https://sb-1769697636.userman.de",
|
||||||
|
"postgrest": "http://192.168.45.104:3000",
|
||||||
|
"chat_webhook": "https://sb-1769697636.userman.de/webhook/rag-chat-webhook/chat",
|
||||||
|
"chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat",
|
||||||
|
"upload_form": "https://sb-1769697636.userman.de/form/rag-upload-form",
|
||||||
|
"upload_form_internal": "http://192.168.45.104:5678/form/rag-upload-form"
|
||||||
|
},
|
||||||
|
"supabase": {
|
||||||
|
"url_external": "http://192.168.45.104:3000",
|
||||||
|
"anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
},
|
||||||
|
"ollama": {
|
||||||
|
"url": "http://192.168.45.3:11434",
|
||||||
|
"model": "ministral-3:3b",
|
||||||
|
"embedding_model": "nomic-embed-text:latest"
|
||||||
|
},
|
||||||
|
"customer_email": "max@beispiel.de",
|
||||||
|
"first_name": "Max",
|
||||||
|
"last_name": "Mustermann",
|
||||||
|
"company": "Muster GmbH",
|
||||||
|
"customer_status": "trial"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtig:** Keine Secrets (passwords, service_role_key, jwt_secret) im Response!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Store Installer JSON (Service Role Only)
|
||||||
|
|
||||||
|
**URL:** `POST /rpc/store_installer_json`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/store_installer_json' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer <SERVICE_ROLE_KEY>" \
|
||||||
|
-d '{
|
||||||
|
"customer_email_param": "max@beispiel.de",
|
||||||
|
"lxc_id_param": 769697636,
|
||||||
|
"installer_json_param": {...}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"instance_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
"message": "Installer JSON stored successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sicherheits-Whitelist
|
||||||
|
|
||||||
|
### ✅ Erlaubt (Frontend-sicher)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ctid": 769697636,
|
||||||
|
"hostname": "sb-1769697636",
|
||||||
|
"fqdn": "sb-1769697636.userman.de",
|
||||||
|
"ip": "192.168.45.104",
|
||||||
|
"vlan": 90,
|
||||||
|
"urls": {
|
||||||
|
"n8n_internal": "http://192.168.45.104:5678/",
|
||||||
|
"n8n_external": "https://sb-1769697636.userman.de",
|
||||||
|
"postgrest": "http://192.168.45.104:3000",
|
||||||
|
"chat_webhook": "https://sb-1769697636.userman.de/webhook/rag-chat-webhook/chat",
|
||||||
|
"chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat",
|
||||||
|
"upload_form": "https://sb-1769697636.userman.de/form/rag-upload-form",
|
||||||
|
"upload_form_internal": "http://192.168.45.104:5678/form/rag-upload-form"
|
||||||
|
},
|
||||||
|
"supabase": {
|
||||||
|
"url_external": "http://192.168.45.104:3000",
|
||||||
|
"anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
},
|
||||||
|
"ollama": {
|
||||||
|
"url": "http://192.168.45.3:11434",
|
||||||
|
"model": "ministral-3:3b",
|
||||||
|
"embedding_model": "nomic-embed-text:latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Verboten (Secrets)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"postgres": {
|
||||||
|
"password": "NEVER_EXPOSE"
|
||||||
|
},
|
||||||
|
"supabase": {
|
||||||
|
"service_role_key": "NEVER_EXPOSE",
|
||||||
|
"jwt_secret": "NEVER_EXPOSE"
|
||||||
|
},
|
||||||
|
"n8n": {
|
||||||
|
"owner_password": "NEVER_EXPOSE",
|
||||||
|
"encryption_key": "NEVER_EXPOSE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentifizierung
|
||||||
|
|
||||||
|
### 1. Keine Authentifizierung (Public)
|
||||||
|
|
||||||
|
- `/rpc/get_public_config`
|
||||||
|
- `/rpc/get_instance_config_by_email`
|
||||||
|
|
||||||
|
**Empfehlung:** Rate Limiting aktivieren!
|
||||||
|
|
||||||
|
### 2. Service Role Key (Backend-to-Backend)
|
||||||
|
|
||||||
|
**Header:**
|
||||||
|
```
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MjAwMDAwMDAwMH0...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendung:**
|
||||||
|
- `/rpc/get_instance_config_by_ctid`
|
||||||
|
- `/rpc/store_installer_json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment-Schritte
|
||||||
|
|
||||||
|
### Schritt 1: SQL-Schema anwenden
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf bestehendem Container
|
||||||
|
CTID=769697636
|
||||||
|
|
||||||
|
pct exec ${CTID} -- bash -c "
|
||||||
|
docker exec customer-postgres psql -U customer -d customer < /opt/customer-stack/sql/add_installer_json_api.sql
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 2: Test ausführen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basis-Test
|
||||||
|
bash customer-installer/test_installer_json_api.sh \
|
||||||
|
--postgrest-url http://192.168.45.104:3000
|
||||||
|
|
||||||
|
# Mit Service Role Key
|
||||||
|
bash customer-installer/test_installer_json_api.sh \
|
||||||
|
--postgrest-url http://192.168.45.104:3000 \
|
||||||
|
--service-role-key "eyJhbGc..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 3: install.sh erweitern (nächster Schritt)
|
||||||
|
|
||||||
|
Am Ende von `install.sh` hinzufügen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Source API library
|
||||||
|
source "${SCRIPT_DIR}/lib_installer_json_api.sh"
|
||||||
|
|
||||||
|
# Apply SQL schema
|
||||||
|
apply_installer_json_api_schema "${CTID}"
|
||||||
|
|
||||||
|
# Store installer JSON in database
|
||||||
|
store_installer_json_in_db \
|
||||||
|
"${CTID}" \
|
||||||
|
"${N8N_OWNER_EMAIL}" \
|
||||||
|
"${SUPABASE_URL_EXTERNAL}" \
|
||||||
|
"${SERVICE_ROLE_KEY}" \
|
||||||
|
"${JSON_OUTPUT}"
|
||||||
|
|
||||||
|
# Verify storage
|
||||||
|
verify_installer_json_stored \
|
||||||
|
"${CTID}" \
|
||||||
|
"${N8N_OWNER_EMAIL}" \
|
||||||
|
"${SUPABASE_URL_EXTERNAL}"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Curl-Tests
|
||||||
|
|
||||||
|
### Test 1: Public Config
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_public_config' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}'
|
||||||
|
|
||||||
|
# Erwartete Antwort:
|
||||||
|
# {"registration_webhook_url":"https://api.botkonzept.de/webhook/botkonzept-registration","api_base_url":"https://api.botkonzept.de"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2: Instance Config by Email
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_email' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"customer_email_param": "max@beispiel.de"}'
|
||||||
|
|
||||||
|
# Erwartete Antwort: Array mit Instanz-Config (siehe oben)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3: Verify No Secrets
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_instance_config_by_email' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"customer_email_param": "max@beispiel.de"}' | jq .
|
||||||
|
|
||||||
|
# Prüfe: Response enthält KEINE der folgenden Strings:
|
||||||
|
# - "password"
|
||||||
|
# - "service_role_key"
|
||||||
|
# - "jwt_secret"
|
||||||
|
# - "encryption_key"
|
||||||
|
# - "owner_password"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 4: Store Installer JSON (mit Service Role Key)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/store_installer_json' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${SERVICE_ROLE_KEY}" \
|
||||||
|
-d '{
|
||||||
|
"customer_email_param": "max@beispiel.de",
|
||||||
|
"lxc_id_param": 769697636,
|
||||||
|
"installer_json_param": {
|
||||||
|
"ctid": 769697636,
|
||||||
|
"urls": {...},
|
||||||
|
"postgres": {"password": "secret"},
|
||||||
|
"supabase": {"service_role_key": "secret"}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Erwartete Antwort:
|
||||||
|
# {"success":true,"instance_id":"...","customer_id":"...","message":"Installer JSON stored successfully"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nächste Schritte (Schritt 2)
|
||||||
|
|
||||||
|
1. **Frontend-Integration:**
|
||||||
|
- `customer-frontend/js/main.js` anpassen
|
||||||
|
- `customer-frontend/js/dashboard.js` anpassen
|
||||||
|
- Dynamisches Laden der URLs aus API
|
||||||
|
|
||||||
|
2. **install.sh erweitern:**
|
||||||
|
- SQL-Schema automatisch anwenden
|
||||||
|
- Installer-JSON automatisch speichern
|
||||||
|
- Verifizierung nach Speicherung
|
||||||
|
|
||||||
|
3. **CORS konfigurieren:**
|
||||||
|
- PostgREST CORS Headers setzen
|
||||||
|
- Nginx Reverse Proxy CORS konfigurieren
|
||||||
|
|
||||||
|
4. **Rate Limiting:**
|
||||||
|
- Nginx Rate Limiting für öffentliche Endpunkte
|
||||||
|
- Oder API Gateway (Kong, Tyk) verwenden
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
✅ **Schritt 1 ABGESCHLOSSEN**
|
||||||
|
|
||||||
|
**Erstellt:**
|
||||||
|
- ✅ SQL-Schema mit sicherer API-View
|
||||||
|
- ✅ API-Dokumentation
|
||||||
|
- ✅ Integration-Library
|
||||||
|
- ✅ Test-Script
|
||||||
|
|
||||||
|
**Bereit für:**
|
||||||
|
- ⏭️ Schritt 2: Frontend-Integration
|
||||||
|
- ⏭️ Schritt 3: install.sh erweitern
|
||||||
|
- ⏭️ Schritt 4: E2E-Tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **API-Dokumentation:** `customer-installer/API_DOCUMENTATION.md`
|
||||||
|
- **Test-Script:** `customer-installer/test_installer_json_api.sh`
|
||||||
|
- **Integration-Library:** `customer-installer/lib_installer_json_api.sh`
|
||||||
|
- **SQL-Schema:** `customer-installer/sql/add_installer_json_api.sql`
|
||||||
467
SUPABASE_AUTH_API_TESTS.md
Normal file
467
SUPABASE_AUTH_API_TESTS.md
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
# Supabase Auth API - Tests & Examples
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Diese API verwendet **Supabase Auth JWT Tokens** für Authentifizierung.
|
||||||
|
**NIEMALS Service Role Key im Frontend verwenden!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 1: Unauthenticated Request (muss 401/403 geben)
|
||||||
|
|
||||||
|
### Request (ohne Auth Token)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_my_instance_config' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Response (401 Unauthorized)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "PGRST301",
|
||||||
|
"message": "Not authenticated",
|
||||||
|
"details": null,
|
||||||
|
"hint": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** ✅ PASS - Unauthenticated requests are blocked
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 2: Authenticated Request (muss 200 + Whitelist geben)
|
||||||
|
|
||||||
|
### Step 1: Get JWT Token (Supabase Auth)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Login via Supabase Auth
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/auth/v1/token?grant_type=password' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||||
|
-d '{
|
||||||
|
"email": "max@beispiel.de",
|
||||||
|
"password": "SecurePassword123!"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzM3MDM2MDAwLCJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAiLCJlbWFpbCI6Im1heEBiZWlzcGllbC5kZSIsInJvbGUiOiJhdXRoZW50aWNhdGVkIn0...",
|
||||||
|
"token_type": "bearer",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"refresh_token": "...",
|
||||||
|
"user": {
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"email": "max@beispiel.de",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Get Instance Config (with JWT)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_my_instance_config' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${JWT_TOKEN}" \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Response (200 OK + Whitelist)
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
"owner_user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"ctid": 769697636,
|
||||||
|
"hostname": "sb-1769697636",
|
||||||
|
"fqdn": "sb-1769697636.userman.de",
|
||||||
|
"ip": "192.168.45.104",
|
||||||
|
"vlan": 90,
|
||||||
|
"status": "active",
|
||||||
|
"created_at": "2025-01-15T10:30:00Z",
|
||||||
|
"urls": {
|
||||||
|
"n8n_internal": "http://192.168.45.104:5678/",
|
||||||
|
"n8n_external": "https://sb-1769697636.userman.de",
|
||||||
|
"postgrest": "http://192.168.45.104:3000",
|
||||||
|
"chat_webhook": "https://sb-1769697636.userman.de/webhook/rag-chat-webhook/chat",
|
||||||
|
"chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat",
|
||||||
|
"upload_form": "https://sb-1769697636.userman.de/form/rag-upload-form",
|
||||||
|
"upload_form_internal": "http://192.168.45.104:5678/form/rag-upload-form"
|
||||||
|
},
|
||||||
|
"supabase": {
|
||||||
|
"url_external": "http://192.168.45.104:3000",
|
||||||
|
"anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjIwMDAwMDAwMDB9..."
|
||||||
|
},
|
||||||
|
"ollama": {
|
||||||
|
"url": "http://192.168.45.3:11434",
|
||||||
|
"model": "ministral-3:3b",
|
||||||
|
"embedding_model": "nomic-embed-text:latest"
|
||||||
|
},
|
||||||
|
"customer_email": "max@beispiel.de",
|
||||||
|
"first_name": "Max",
|
||||||
|
"last_name": "Mustermann",
|
||||||
|
"company": "Muster GmbH",
|
||||||
|
"customer_status": "trial"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** ✅ PASS - Authenticated user gets their instance config
|
||||||
|
|
||||||
|
### Step 3: Verify NO SECRETS in Response
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check response does NOT contain secrets
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_my_instance_config' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${JWT_TOKEN}" \
|
||||||
|
-d '{}' | grep -E "password|service_role_key|jwt_secret|encryption_key|owner_password"
|
||||||
|
|
||||||
|
# Expected: NO OUTPUT (grep finds nothing)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** ✅ PASS - No secrets exposed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 3: Not Found (User has no instance)
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
JWT_TOKEN="<token_for_user_without_instance>"
|
||||||
|
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_my_instance_config' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${JWT_TOKEN}" \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Response (200 OK, empty array)
|
||||||
|
|
||||||
|
```json
|
||||||
|
[]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** ✅ PASS - Returns empty array when no instance found
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 4: Public Config (No Auth Required)
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/get_public_config' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Response (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"registration_webhook_url": "https://api.botkonzept.de/webhook/botkonzept-registration",
|
||||||
|
"api_base_url": "https://api.botkonzept.de"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** ✅ PASS - Public config accessible without auth
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 5: Service Role - Store Installer JSON
|
||||||
|
|
||||||
|
### Request (Backend Only - Service Role Key)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MjAwMDAwMDAwMH0..."
|
||||||
|
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/store_installer_json' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${SERVICE_ROLE_KEY}" \
|
||||||
|
-d '{
|
||||||
|
"customer_email_param": "max@beispiel.de",
|
||||||
|
"lxc_id_param": 769697636,
|
||||||
|
"installer_json_param": {
|
||||||
|
"ctid": 769697636,
|
||||||
|
"hostname": "sb-1769697636",
|
||||||
|
"fqdn": "sb-1769697636.userman.de",
|
||||||
|
"ip": "192.168.45.104",
|
||||||
|
"vlan": 90,
|
||||||
|
"urls": {
|
||||||
|
"n8n_internal": "http://192.168.45.104:5678/",
|
||||||
|
"n8n_external": "https://sb-1769697636.userman.de",
|
||||||
|
"postgrest": "http://192.168.45.104:3000",
|
||||||
|
"chat_webhook": "https://sb-1769697636.userman.de/webhook/rag-chat-webhook/chat",
|
||||||
|
"chat_internal": "http://192.168.45.104:5678/webhook/rag-chat-webhook/chat",
|
||||||
|
"upload_form": "https://sb-1769697636.userman.de/form/rag-upload-form",
|
||||||
|
"upload_form_internal": "http://192.168.45.104:5678/form/rag-upload-form"
|
||||||
|
},
|
||||||
|
"postgres": {
|
||||||
|
"host": "postgres",
|
||||||
|
"port": 5432,
|
||||||
|
"db": "customer",
|
||||||
|
"user": "customer",
|
||||||
|
"password": "SECRET_PASSWORD_NEVER_EXPOSE"
|
||||||
|
},
|
||||||
|
"supabase": {
|
||||||
|
"url": "http://postgrest:3000",
|
||||||
|
"url_external": "http://192.168.45.104:3000",
|
||||||
|
"anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"service_role_key": "SECRET_SERVICE_ROLE_KEY_NEVER_EXPOSE",
|
||||||
|
"jwt_secret": "SECRET_JWT_SECRET_NEVER_EXPOSE"
|
||||||
|
},
|
||||||
|
"ollama": {
|
||||||
|
"url": "http://192.168.45.3:11434",
|
||||||
|
"model": "ministral-3:3b",
|
||||||
|
"embedding_model": "nomic-embed-text:latest"
|
||||||
|
},
|
||||||
|
"n8n": {
|
||||||
|
"encryption_key": "SECRET_ENCRYPTION_KEY_NEVER_EXPOSE",
|
||||||
|
"owner_email": "admin@userman.de",
|
||||||
|
"owner_password": "SECRET_PASSWORD_NEVER_EXPOSE",
|
||||||
|
"secure_cookie": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Response (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"instance_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
"message": "Installer JSON stored successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** ✅ PASS - Installer JSON stored (backend only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 6: Service Role - Link Customer to Auth User
|
||||||
|
|
||||||
|
### Request (Backend Only - Service Role Key)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/link_customer_to_auth_user' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${SERVICE_ROLE_KEY}" \
|
||||||
|
-d '{
|
||||||
|
"customer_email_param": "max@beispiel.de",
|
||||||
|
"auth_user_id_param": "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Response (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"customer_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
"auth_user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"message": "Customer linked to auth user successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** ✅ PASS - Customer linked to auth user
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 7: Unauthorized Service Role Access
|
||||||
|
|
||||||
|
### Request (User JWT trying to access service role function)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
USER_JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYXV0aGVudGljYXRlZCJ9..."
|
||||||
|
|
||||||
|
curl -X POST 'http://192.168.45.104:3000/rpc/store_installer_json' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${USER_JWT_TOKEN}" \
|
||||||
|
-d '{
|
||||||
|
"customer_email_param": "max@beispiel.de",
|
||||||
|
"lxc_id_param": 769697636,
|
||||||
|
"installer_json_param": {}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Response (403 Forbidden)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "PGRST301",
|
||||||
|
"message": "Forbidden: service_role required",
|
||||||
|
"details": null,
|
||||||
|
"hint": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** ✅ PASS - User cannot access service role functions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Checklist
|
||||||
|
|
||||||
|
### ✅ Whitelist (Frontend-Safe)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ctid": 769697636,
|
||||||
|
"hostname": "sb-1769697636",
|
||||||
|
"fqdn": "sb-1769697636.userman.de",
|
||||||
|
"ip": "192.168.45.104",
|
||||||
|
"vlan": 90,
|
||||||
|
"urls": { ... },
|
||||||
|
"supabase": {
|
||||||
|
"url_external": "http://192.168.45.104:3000",
|
||||||
|
"anon_key": "eyJhbGc..."
|
||||||
|
},
|
||||||
|
"ollama": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Blacklist (NEVER Expose)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"postgres": {
|
||||||
|
"password": "NEVER_EXPOSE"
|
||||||
|
},
|
||||||
|
"supabase": {
|
||||||
|
"service_role_key": "NEVER_EXPOSE",
|
||||||
|
"jwt_secret": "NEVER_EXPOSE"
|
||||||
|
},
|
||||||
|
"n8n": {
|
||||||
|
"owner_password": "NEVER_EXPOSE",
|
||||||
|
"encryption_key": "NEVER_EXPOSE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Test Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Complete API test script
|
||||||
|
|
||||||
|
POSTGREST_URL="http://192.168.45.104:3000"
|
||||||
|
ANON_KEY="<your_anon_key>"
|
||||||
|
SERVICE_ROLE_KEY="<your_service_role_key>"
|
||||||
|
|
||||||
|
echo "=== Test 1: Unauthenticated Request (should fail) ==="
|
||||||
|
curl -X POST "${POSTGREST_URL}/rpc/get_my_instance_config" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}'
|
||||||
|
echo -e "\n"
|
||||||
|
|
||||||
|
echo "=== Test 2: Login and Get JWT ==="
|
||||||
|
LOGIN_RESPONSE=$(curl -X POST "${POSTGREST_URL}/auth/v1/token?grant_type=password" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "apikey: ${ANON_KEY}" \
|
||||||
|
-d '{
|
||||||
|
"email": "max@beispiel.de",
|
||||||
|
"password": "SecurePassword123!"
|
||||||
|
}')
|
||||||
|
|
||||||
|
JWT_TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.access_token')
|
||||||
|
echo "JWT Token: ${JWT_TOKEN:0:50}..."
|
||||||
|
echo -e "\n"
|
||||||
|
|
||||||
|
echo "=== Test 3: Get My Instance Config (authenticated) ==="
|
||||||
|
curl -X POST "${POSTGREST_URL}/rpc/get_my_instance_config" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${JWT_TOKEN}" \
|
||||||
|
-d '{}' | jq .
|
||||||
|
echo -e "\n"
|
||||||
|
|
||||||
|
echo "=== Test 4: Verify No Secrets ==="
|
||||||
|
RESPONSE=$(curl -s -X POST "${POSTGREST_URL}/rpc/get_my_instance_config" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${JWT_TOKEN}" \
|
||||||
|
-d '{}')
|
||||||
|
|
||||||
|
if echo "$RESPONSE" | grep -qE "password|service_role_key|jwt_secret|encryption_key"; then
|
||||||
|
echo "❌ FAIL: Secrets found in response!"
|
||||||
|
else
|
||||||
|
echo "✅ PASS: No secrets in response"
|
||||||
|
fi
|
||||||
|
echo -e "\n"
|
||||||
|
|
||||||
|
echo "=== Test 5: Public Config (no auth) ==="
|
||||||
|
curl -X POST "${POSTGREST_URL}/rpc/get_public_config" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' | jq .
|
||||||
|
echo -e "\n"
|
||||||
|
|
||||||
|
echo "=== All tests completed ==="
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Integration Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Frontend code (React/Vue/etc.)
|
||||||
|
import { createClient } from '@supabase/supabase-js'
|
||||||
|
|
||||||
|
const supabase = createClient(
|
||||||
|
'http://192.168.45.104:3000',
|
||||||
|
'<ANON_KEY>' // Public anon key - safe to use in frontend
|
||||||
|
)
|
||||||
|
|
||||||
|
// Login
|
||||||
|
const { data: authData, error: authError } = await supabase.auth.signInWithPassword({
|
||||||
|
email: 'max@beispiel.de',
|
||||||
|
password: 'SecurePassword123!'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (authError) {
|
||||||
|
console.error('Login failed:', authError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get instance config (uses JWT automatically)
|
||||||
|
const { data, error } = await supabase.rpc('get_my_instance_config')
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to get config:', error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Instance config:', data)
|
||||||
|
// data[0].urls.chat_webhook
|
||||||
|
// data[0].urls.upload_form
|
||||||
|
// etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
✅ **Authenticated requests work** (with JWT)
|
||||||
|
✅ **Unauthenticated requests blocked** (401/403)
|
||||||
|
✅ **No secrets exposed** (whitelist only)
|
||||||
|
✅ **Service role functions protected** (backend only)
|
||||||
|
✅ **RLS enforced** (users see only their own data)
|
||||||
|
|
||||||
|
**Security:** ✅ PASS
|
||||||
|
**Functionality:** ✅ PASS
|
||||||
|
**Ready for production:** ✅ YES
|
||||||
325
lib_installer_json_api.sh
Normal file
325
lib_installer_json_api.sh
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =====================================================
|
||||||
|
# Installer JSON API Integration Library
|
||||||
|
# =====================================================
|
||||||
|
# Functions to store and retrieve installer JSON via PostgREST API
|
||||||
|
|
||||||
|
# Store installer JSON in database via PostgREST
|
||||||
|
# Usage: store_installer_json_in_db <ctid> <customer_email> <postgrest_url> <service_role_key> <json_output>
|
||||||
|
# Returns: 0 on success, 1 on failure
|
||||||
|
store_installer_json_in_db() {
|
||||||
|
local ctid="$1"
|
||||||
|
local customer_email="$2"
|
||||||
|
local postgrest_url="$3"
|
||||||
|
local service_role_key="$4"
|
||||||
|
local json_output="$5"
|
||||||
|
|
||||||
|
info "Storing installer JSON in database for CTID ${ctid}..."
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
[[ -n "$ctid" ]] || { warn "CTID is empty"; return 1; }
|
||||||
|
[[ -n "$customer_email" ]] || { warn "Customer email is empty"; return 1; }
|
||||||
|
[[ -n "$postgrest_url" ]] || { warn "PostgREST URL is empty"; return 1; }
|
||||||
|
[[ -n "$service_role_key" ]] || { warn "Service role key is empty"; return 1; }
|
||||||
|
[[ -n "$json_output" ]] || { warn "JSON output is empty"; return 1; }
|
||||||
|
|
||||||
|
# Validate JSON
|
||||||
|
if ! echo "$json_output" | python3 -m json.tool >/dev/null 2>&1; then
|
||||||
|
warn "Invalid JSON output"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prepare API request payload
|
||||||
|
local payload
|
||||||
|
payload=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"customer_email_param": "${customer_email}",
|
||||||
|
"lxc_id_param": ${ctid},
|
||||||
|
"installer_json_param": ${json_output}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make API request
|
||||||
|
local response
|
||||||
|
local http_code
|
||||||
|
|
||||||
|
response=$(curl -sS -w "\n%{http_code}" -X POST "${postgrest_url}/rpc/store_installer_json" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${service_role_key}" \
|
||||||
|
-H "Prefer: return=representation" \
|
||||||
|
-d "${payload}" 2>&1)
|
||||||
|
|
||||||
|
# Extract HTTP code from last line
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
response=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
# Check HTTP status
|
||||||
|
if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
|
||||||
|
# Check if response indicates success
|
||||||
|
if echo "$response" | grep -q '"success":\s*true'; then
|
||||||
|
info "Installer JSON stored successfully in database"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
warn "API returned success HTTP code but response indicates failure: ${response}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Failed to store installer JSON (HTTP ${http_code}): ${response}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Retrieve installer JSON from database via PostgREST
|
||||||
|
# Usage: get_installer_json_by_email <customer_email> <postgrest_url>
|
||||||
|
# Returns: JSON on stdout, exit code 0 on success
|
||||||
|
get_installer_json_by_email() {
|
||||||
|
local customer_email="$1"
|
||||||
|
local postgrest_url="$2"
|
||||||
|
|
||||||
|
info "Retrieving installer JSON for ${customer_email}..."
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
[[ -n "$customer_email" ]] || { warn "Customer email is empty"; return 1; }
|
||||||
|
[[ -n "$postgrest_url" ]] || { warn "PostgREST URL is empty"; return 1; }
|
||||||
|
|
||||||
|
# Prepare API request payload
|
||||||
|
local payload
|
||||||
|
payload=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"customer_email_param": "${customer_email}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make API request
|
||||||
|
local response
|
||||||
|
local http_code
|
||||||
|
|
||||||
|
response=$(curl -sS -w "\n%{http_code}" -X POST "${postgrest_url}/rpc/get_instance_config_by_email" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "${payload}" 2>&1)
|
||||||
|
|
||||||
|
# Extract HTTP code from last line
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
response=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
# Check HTTP status
|
||||||
|
if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
|
||||||
|
# Check if response is empty array
|
||||||
|
if [[ "$response" == "[]" ]]; then
|
||||||
|
warn "No instance found for email: ${customer_email}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Output JSON
|
||||||
|
echo "$response"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
warn "Failed to retrieve installer JSON (HTTP ${http_code}): ${response}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Retrieve installer JSON by CTID (requires service role key)
|
||||||
|
# Usage: get_installer_json_by_ctid <ctid> <postgrest_url> <service_role_key>
|
||||||
|
# Returns: JSON on stdout, exit code 0 on success
|
||||||
|
get_installer_json_by_ctid() {
|
||||||
|
local ctid="$1"
|
||||||
|
local postgrest_url="$2"
|
||||||
|
local service_role_key="$3"
|
||||||
|
|
||||||
|
info "Retrieving installer JSON for CTID ${ctid}..."
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
[[ -n "$ctid" ]] || { warn "CTID is empty"; return 1; }
|
||||||
|
[[ -n "$postgrest_url" ]] || { warn "PostgREST URL is empty"; return 1; }
|
||||||
|
[[ -n "$service_role_key" ]] || { warn "Service role key is empty"; return 1; }
|
||||||
|
|
||||||
|
# Prepare API request payload
|
||||||
|
local payload
|
||||||
|
payload=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"ctid_param": ${ctid}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make API request
|
||||||
|
local response
|
||||||
|
local http_code
|
||||||
|
|
||||||
|
response=$(curl -sS -w "\n%{http_code}" -X POST "${postgrest_url}/rpc/get_instance_config_by_ctid" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${service_role_key}" \
|
||||||
|
-d "${payload}" 2>&1)
|
||||||
|
|
||||||
|
# Extract HTTP code from last line
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
response=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
# Check HTTP status
|
||||||
|
if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
|
||||||
|
# Check if response is empty array
|
||||||
|
if [[ "$response" == "[]" ]]; then
|
||||||
|
warn "No instance found for CTID: ${ctid}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Output JSON
|
||||||
|
echo "$response"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
warn "Failed to retrieve installer JSON (HTTP ${http_code}): ${response}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get public config (no authentication required)
|
||||||
|
# Usage: get_public_config <postgrest_url>
|
||||||
|
# Returns: JSON on stdout, exit code 0 on success
|
||||||
|
get_public_config() {
|
||||||
|
local postgrest_url="$1"
|
||||||
|
|
||||||
|
info "Retrieving public config..."
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
[[ -n "$postgrest_url" ]] || { warn "PostgREST URL is empty"; return 1; }
|
||||||
|
|
||||||
|
# Make API request
|
||||||
|
local response
|
||||||
|
local http_code
|
||||||
|
|
||||||
|
response=$(curl -sS -w "\n%{http_code}" -X POST "${postgrest_url}/rpc/get_public_config" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' 2>&1)
|
||||||
|
|
||||||
|
# Extract HTTP code from last line
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
response=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
# Check HTTP status
|
||||||
|
if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
|
||||||
|
# Output JSON
|
||||||
|
echo "$response"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
warn "Failed to retrieve public config (HTTP ${http_code}): ${response}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply installer JSON API schema to database
|
||||||
|
# Usage: apply_installer_json_api_schema <ctid>
|
||||||
|
# Returns: 0 on success, 1 on failure
|
||||||
|
apply_installer_json_api_schema() {
|
||||||
|
local ctid="$1"
|
||||||
|
|
||||||
|
info "Applying installer JSON API schema to database..."
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
[[ -n "$ctid" ]] || { warn "CTID is empty"; return 1; }
|
||||||
|
|
||||||
|
# Check if SQL file exists
|
||||||
|
local sql_file="${SCRIPT_DIR}/sql/add_installer_json_api.sql"
|
||||||
|
if [[ ! -f "$sql_file" ]]; then
|
||||||
|
warn "SQL file not found: ${sql_file}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy SQL file to container
|
||||||
|
info "Copying SQL file to container..."
|
||||||
|
pct_push_text "$ctid" "/tmp/add_installer_json_api.sql" "$(cat "$sql_file")"
|
||||||
|
|
||||||
|
# Execute SQL in PostgreSQL container
|
||||||
|
info "Executing SQL in PostgreSQL container..."
|
||||||
|
local result
|
||||||
|
result=$(pct_exec "$ctid" -- bash -c "
|
||||||
|
docker exec customer-postgres psql -U customer -d customer -f /tmp/add_installer_json_api.sql 2>&1
|
||||||
|
" || echo "FAILED")
|
||||||
|
|
||||||
|
if echo "$result" | grep -qi "error\|failed"; then
|
||||||
|
warn "Failed to apply SQL schema: ${result}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "SQL schema applied successfully"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
pct_exec "$ctid" -- rm -f /tmp/add_installer_json_api.sql 2>/dev/null || true
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test API connectivity
|
||||||
|
# Usage: test_api_connectivity <postgrest_url>
|
||||||
|
# Returns: 0 on success, 1 on failure
|
||||||
|
test_api_connectivity() {
|
||||||
|
local postgrest_url="$1"
|
||||||
|
|
||||||
|
info "Testing API connectivity to ${postgrest_url}..."
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
[[ -n "$postgrest_url" ]] || { warn "PostgREST URL is empty"; return 1; }
|
||||||
|
|
||||||
|
# Test with public config endpoint
|
||||||
|
local response
|
||||||
|
local http_code
|
||||||
|
|
||||||
|
response=$(curl -sS -w "\n%{http_code}" -X POST "${postgrest_url}/rpc/get_public_config" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' 2>&1)
|
||||||
|
|
||||||
|
# Extract HTTP code from last line
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
|
||||||
|
# Check HTTP status
|
||||||
|
if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
|
||||||
|
info "API connectivity test successful"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
warn "API connectivity test failed (HTTP ${http_code})"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify installer JSON was stored correctly
|
||||||
|
# Usage: verify_installer_json_stored <ctid> <customer_email> <postgrest_url>
|
||||||
|
# Returns: 0 on success, 1 on failure
|
||||||
|
verify_installer_json_stored() {
|
||||||
|
local ctid="$1"
|
||||||
|
local customer_email="$2"
|
||||||
|
local postgrest_url="$3"
|
||||||
|
|
||||||
|
info "Verifying installer JSON was stored for CTID ${ctid}..."
|
||||||
|
|
||||||
|
# Retrieve installer JSON
|
||||||
|
local response
|
||||||
|
response=$(get_installer_json_by_email "$customer_email" "$postgrest_url")
|
||||||
|
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
warn "Failed to retrieve installer JSON for verification"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if CTID matches
|
||||||
|
local stored_ctid
|
||||||
|
stored_ctid=$(echo "$response" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d[0]['ctid'] if d else '')" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ "$stored_ctid" == "$ctid" ]]; then
|
||||||
|
info "Installer JSON verified successfully (CTID: ${stored_ctid})"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
warn "Installer JSON verification failed (expected CTID: ${ctid}, got: ${stored_ctid})"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export functions
|
||||||
|
export -f store_installer_json_in_db
|
||||||
|
export -f get_installer_json_by_email
|
||||||
|
export -f get_installer_json_by_ctid
|
||||||
|
export -f get_public_config
|
||||||
|
export -f apply_installer_json_api_schema
|
||||||
|
export -f test_api_connectivity
|
||||||
|
export -f verify_installer_json_stored
|
||||||
426
setup_botkonzept_lxc.sh
Executable file
426
setup_botkonzept_lxc.sh
Executable 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
sql/add_installer_json_api.sql
Normal file
378
sql/add_installer_json_api.sql
Normal 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
|
||||||
|
-- =====================================================
|
||||||
476
sql/add_installer_json_api_supabase_auth.sql
Normal file
476
sql/add_installer_json_api_supabase_auth.sql
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- BotKonzept - Installer JSON API (Supabase Auth)
|
||||||
|
-- =====================================================
|
||||||
|
-- Secure API using Supabase Auth JWT tokens
|
||||||
|
-- NO Service Role Key in Frontend - EVER!
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Step 1: Add installer_json column to instances table
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
ALTER TABLE instances
|
||||||
|
ADD COLUMN IF NOT EXISTS installer_json JSONB DEFAULT '{}'::jsonb;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_instances_installer_json ON instances USING gin(installer_json);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN instances.installer_json IS 'Complete installer JSON output from install.sh (includes secrets - use api.get_my_instance_config() for safe access)';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Step 2: Link instances to Supabase Auth users
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Add owner_user_id column to link instance to Supabase Auth user
|
||||||
|
ALTER TABLE instances
|
||||||
|
ADD COLUMN IF NOT EXISTS owner_user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Create index for faster lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_instances_owner_user_id ON instances(owner_user_id);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN instances.owner_user_id IS 'Supabase Auth user ID of the instance owner';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Step 3: Create safe API view (NON-SECRET data only)
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE SCHEMA IF NOT EXISTS api;
|
||||||
|
GRANT USAGE ON SCHEMA api TO anon, authenticated, service_role;
|
||||||
|
|
||||||
|
-- View that exposes only safe (non-secret) installer data
|
||||||
|
CREATE OR REPLACE VIEW api.instance_config AS
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.customer_id,
|
||||||
|
i.owner_user_id,
|
||||||
|
i.lxc_id as ctid,
|
||||||
|
i.hostname,
|
||||||
|
i.fqdn,
|
||||||
|
i.ip,
|
||||||
|
i.vlan,
|
||||||
|
i.status,
|
||||||
|
i.created_at,
|
||||||
|
-- Extract safe URLs from installer_json (NO SECRETS)
|
||||||
|
jsonb_build_object(
|
||||||
|
'n8n_internal', i.installer_json->'urls'->>'n8n_internal',
|
||||||
|
'n8n_external', i.installer_json->'urls'->>'n8n_external',
|
||||||
|
'postgrest', i.installer_json->'urls'->>'postgrest',
|
||||||
|
'chat_webhook', i.installer_json->'urls'->>'chat_webhook',
|
||||||
|
'chat_internal', i.installer_json->'urls'->>'chat_internal',
|
||||||
|
'upload_form', i.installer_json->'urls'->>'upload_form',
|
||||||
|
'upload_form_internal', i.installer_json->'urls'->>'upload_form_internal'
|
||||||
|
) as urls,
|
||||||
|
-- Extract safe Supabase data (NO service_role_key, NO jwt_secret)
|
||||||
|
jsonb_build_object(
|
||||||
|
'url_external', i.installer_json->'supabase'->>'url_external',
|
||||||
|
'anon_key', i.installer_json->'supabase'->>'anon_key'
|
||||||
|
) as supabase,
|
||||||
|
-- Extract Ollama URL (safe)
|
||||||
|
jsonb_build_object(
|
||||||
|
'url', i.installer_json->'ollama'->>'url',
|
||||||
|
'model', i.installer_json->'ollama'->>'model',
|
||||||
|
'embedding_model', i.installer_json->'ollama'->>'embedding_model'
|
||||||
|
) as ollama,
|
||||||
|
-- Customer info (joined)
|
||||||
|
c.email as customer_email,
|
||||||
|
c.first_name,
|
||||||
|
c.last_name,
|
||||||
|
c.company,
|
||||||
|
c.status as customer_status
|
||||||
|
FROM instances i
|
||||||
|
JOIN customers c ON i.customer_id = c.id
|
||||||
|
WHERE i.status = 'active' AND i.deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMENT ON VIEW api.instance_config IS 'Safe API view - exposes only non-secret data from installer JSON';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Step 4: Row Level Security (RLS) Policies
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Enable RLS on instances table (if not already enabled)
|
||||||
|
ALTER TABLE instances ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Drop old policy if exists
|
||||||
|
DROP POLICY IF EXISTS instance_config_select_own ON instances;
|
||||||
|
|
||||||
|
-- Policy: Users can only see their own instances
|
||||||
|
CREATE POLICY instances_select_own ON instances
|
||||||
|
FOR SELECT
|
||||||
|
USING (
|
||||||
|
-- Allow if owner_user_id matches authenticated user
|
||||||
|
owner_user_id = auth.uid()
|
||||||
|
OR
|
||||||
|
-- Allow service_role to see all (for n8n workflows)
|
||||||
|
auth.jwt()->>'role' = 'service_role'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Grant SELECT on api.instance_config view
|
||||||
|
GRANT SELECT ON api.instance_config TO authenticated, service_role;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Step 5: Function to get MY instance config (Auth required)
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Function to get instance config for authenticated user
|
||||||
|
-- Uses auth.uid() - NO email parameter (more secure)
|
||||||
|
CREATE OR REPLACE FUNCTION api.get_my_instance_config()
|
||||||
|
RETURNS TABLE (
|
||||||
|
id UUID,
|
||||||
|
customer_id UUID,
|
||||||
|
owner_user_id UUID,
|
||||||
|
ctid BIGINT,
|
||||||
|
hostname VARCHAR,
|
||||||
|
fqdn VARCHAR,
|
||||||
|
ip VARCHAR,
|
||||||
|
vlan INTEGER,
|
||||||
|
status VARCHAR,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
urls JSONB,
|
||||||
|
supabase JSONB,
|
||||||
|
ollama JSONB,
|
||||||
|
customer_email VARCHAR,
|
||||||
|
first_name VARCHAR,
|
||||||
|
last_name VARCHAR,
|
||||||
|
company VARCHAR,
|
||||||
|
customer_status VARCHAR
|
||||||
|
)
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Check if user is authenticated
|
||||||
|
IF auth.uid() IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Not authenticated';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Return instance config for authenticated user
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
ic.id,
|
||||||
|
ic.customer_id,
|
||||||
|
ic.owner_user_id,
|
||||||
|
ic.ctid,
|
||||||
|
ic.hostname,
|
||||||
|
ic.fqdn,
|
||||||
|
ic.ip,
|
||||||
|
ic.vlan,
|
||||||
|
ic.status,
|
||||||
|
ic.created_at,
|
||||||
|
ic.urls,
|
||||||
|
ic.supabase,
|
||||||
|
ic.ollama,
|
||||||
|
ic.customer_email,
|
||||||
|
ic.first_name,
|
||||||
|
ic.last_name,
|
||||||
|
ic.company,
|
||||||
|
ic.customer_status
|
||||||
|
FROM api.instance_config ic
|
||||||
|
WHERE ic.owner_user_id = auth.uid()
|
||||||
|
LIMIT 1;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION api.get_my_instance_config() TO authenticated;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION api.get_my_instance_config IS 'Get instance configuration for authenticated user - uses auth.uid() for security';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Step 6: Function to get config by CTID (Service Role ONLY)
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION api.get_instance_config_by_ctid(ctid_param BIGINT)
|
||||||
|
RETURNS TABLE (
|
||||||
|
id UUID,
|
||||||
|
customer_id UUID,
|
||||||
|
owner_user_id UUID,
|
||||||
|
ctid BIGINT,
|
||||||
|
hostname VARCHAR,
|
||||||
|
fqdn VARCHAR,
|
||||||
|
ip VARCHAR,
|
||||||
|
vlan INTEGER,
|
||||||
|
status VARCHAR,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
urls JSONB,
|
||||||
|
supabase JSONB,
|
||||||
|
ollama JSONB,
|
||||||
|
customer_email VARCHAR,
|
||||||
|
first_name VARCHAR,
|
||||||
|
last_name VARCHAR,
|
||||||
|
company VARCHAR,
|
||||||
|
customer_status VARCHAR
|
||||||
|
)
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Only service_role can call this
|
||||||
|
IF auth.jwt()->>'role' != 'service_role' THEN
|
||||||
|
RAISE EXCEPTION 'Forbidden: service_role required';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
ic.id,
|
||||||
|
ic.customer_id,
|
||||||
|
ic.owner_user_id,
|
||||||
|
ic.ctid,
|
||||||
|
ic.hostname,
|
||||||
|
ic.fqdn,
|
||||||
|
ic.ip,
|
||||||
|
ic.vlan,
|
||||||
|
ic.status,
|
||||||
|
ic.created_at,
|
||||||
|
ic.urls,
|
||||||
|
ic.supabase,
|
||||||
|
ic.ollama,
|
||||||
|
ic.customer_email,
|
||||||
|
ic.first_name,
|
||||||
|
ic.last_name,
|
||||||
|
ic.company,
|
||||||
|
ic.customer_status
|
||||||
|
FROM api.instance_config ic
|
||||||
|
WHERE ic.ctid = ctid_param
|
||||||
|
LIMIT 1;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION api.get_instance_config_by_ctid(BIGINT) TO service_role;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION api.get_instance_config_by_ctid IS 'Get instance configuration by CTID - service_role only';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Step 7: Public config endpoint (NO auth required)
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION api.get_public_config()
|
||||||
|
RETURNS TABLE (
|
||||||
|
registration_webhook_url TEXT,
|
||||||
|
api_base_url TEXT
|
||||||
|
)
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
'https://api.botkonzept.de/webhook/botkonzept-registration'::TEXT as registration_webhook_url,
|
||||||
|
'https://api.botkonzept.de'::TEXT as api_base_url;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION api.get_public_config() TO anon, authenticated, service_role;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION api.get_public_config IS 'Get public configuration for website (registration webhook URL)';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Step 8: Store installer JSON (Service Role ONLY)
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION api.store_installer_json(
|
||||||
|
customer_email_param TEXT,
|
||||||
|
lxc_id_param BIGINT,
|
||||||
|
installer_json_param JSONB
|
||||||
|
)
|
||||||
|
RETURNS JSONB
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
instance_record RECORD;
|
||||||
|
result JSONB;
|
||||||
|
BEGIN
|
||||||
|
-- Only service_role can call this
|
||||||
|
IF auth.jwt()->>'role' != 'service_role' THEN
|
||||||
|
RAISE EXCEPTION 'Forbidden: service_role required';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Find the instance by customer email and lxc_id
|
||||||
|
SELECT i.id, i.customer_id, c.id as auth_user_id INTO instance_record
|
||||||
|
FROM instances i
|
||||||
|
JOIN customers c ON i.customer_id = c.id
|
||||||
|
WHERE c.email = customer_email_param
|
||||||
|
AND i.lxc_id = lxc_id_param
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'success', false,
|
||||||
|
'error', 'Instance not found for customer email and LXC ID'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Update the installer_json column
|
||||||
|
UPDATE instances
|
||||||
|
SET installer_json = installer_json_param,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = instance_record.id;
|
||||||
|
|
||||||
|
-- Return success
|
||||||
|
result := jsonb_build_object(
|
||||||
|
'success', true,
|
||||||
|
'instance_id', instance_record.id,
|
||||||
|
'customer_id', instance_record.customer_id,
|
||||||
|
'message', 'Installer JSON stored successfully'
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN result;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION api.store_installer_json(TEXT, BIGINT, JSONB) TO service_role;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION api.store_installer_json IS 'Store installer JSON after instance creation - service_role only';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Step 9: Link customer to Supabase Auth user
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Function to link customer to Supabase Auth user (called during registration)
|
||||||
|
CREATE OR REPLACE FUNCTION api.link_customer_to_auth_user(
|
||||||
|
customer_email_param TEXT,
|
||||||
|
auth_user_id_param UUID
|
||||||
|
)
|
||||||
|
RETURNS JSONB
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
customer_record RECORD;
|
||||||
|
instance_record RECORD;
|
||||||
|
result JSONB;
|
||||||
|
BEGIN
|
||||||
|
-- Only service_role can call this
|
||||||
|
IF auth.jwt()->>'role' != 'service_role' THEN
|
||||||
|
RAISE EXCEPTION 'Forbidden: service_role required';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Find customer by email
|
||||||
|
SELECT id INTO customer_record
|
||||||
|
FROM customers
|
||||||
|
WHERE email = customer_email_param
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'success', false,
|
||||||
|
'error', 'Customer not found'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Update all instances for this customer with owner_user_id
|
||||||
|
UPDATE instances
|
||||||
|
SET owner_user_id = auth_user_id_param,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE customer_id = customer_record.id;
|
||||||
|
|
||||||
|
-- Return success
|
||||||
|
result := jsonb_build_object(
|
||||||
|
'success', true,
|
||||||
|
'customer_id', customer_record.id,
|
||||||
|
'auth_user_id', auth_user_id_param,
|
||||||
|
'message', 'Customer linked to auth user successfully'
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN result;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION api.link_customer_to_auth_user(TEXT, UUID) TO service_role;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION api.link_customer_to_auth_user IS 'Link customer to Supabase Auth user - service_role only';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Step 10: Audit logging
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION api.log_config_access(
|
||||||
|
access_type TEXT,
|
||||||
|
ip_address_param INET DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS VOID
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Log access for authenticated user
|
||||||
|
IF auth.uid() IS NOT NULL THEN
|
||||||
|
INSERT INTO audit_log (
|
||||||
|
customer_id,
|
||||||
|
action,
|
||||||
|
entity_type,
|
||||||
|
performed_by,
|
||||||
|
ip_address,
|
||||||
|
metadata
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
i.customer_id,
|
||||||
|
'api_config_access',
|
||||||
|
'instance_config',
|
||||||
|
auth.uid()::text,
|
||||||
|
ip_address_param,
|
||||||
|
jsonb_build_object('access_type', access_type)
|
||||||
|
FROM instances i
|
||||||
|
WHERE i.owner_user_id = auth.uid()
|
||||||
|
LIMIT 1;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION api.log_config_access(TEXT, INET) TO authenticated, service_role;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Step 11: PostgREST API Routes
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Available routes:
|
||||||
|
--
|
||||||
|
-- 1. POST /rpc/get_my_instance_config
|
||||||
|
-- - Body: {}
|
||||||
|
-- - Returns instance config for authenticated user
|
||||||
|
-- - Requires: Supabase Auth JWT token
|
||||||
|
-- - Response: Single instance config object (or empty if not found)
|
||||||
|
--
|
||||||
|
-- 2. POST /rpc/get_public_config
|
||||||
|
-- - Body: {}
|
||||||
|
-- - Returns public configuration (registration webhook URL)
|
||||||
|
-- - Requires: No authentication
|
||||||
|
--
|
||||||
|
-- 3. POST /rpc/get_instance_config_by_ctid
|
||||||
|
-- - Body: {"ctid_param": 769697636}
|
||||||
|
-- - Returns instance config for specific CTID
|
||||||
|
-- - Requires: Service Role Key (backend only)
|
||||||
|
--
|
||||||
|
-- 4. POST /rpc/store_installer_json
|
||||||
|
-- - Body: {"customer_email_param": "...", "lxc_id_param": 123, "installer_json_param": {...}}
|
||||||
|
-- - Stores installer JSON after instance creation
|
||||||
|
-- - Requires: Service Role Key (backend only)
|
||||||
|
--
|
||||||
|
-- 5. POST /rpc/link_customer_to_auth_user
|
||||||
|
-- - Body: {"customer_email_param": "...", "auth_user_id_param": "..."}
|
||||||
|
-- - Links customer to Supabase Auth user
|
||||||
|
-- - Requires: Service Role Key (backend only)
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Example Usage
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Example 1: Get my instance config (authenticated user)
|
||||||
|
-- POST /rpc/get_my_instance_config
|
||||||
|
-- Headers: Authorization: Bearer <USER_JWT_TOKEN>
|
||||||
|
-- Body: {}
|
||||||
|
|
||||||
|
-- Example 2: Get public config (no auth)
|
||||||
|
-- POST /rpc/get_public_config
|
||||||
|
-- Body: {}
|
||||||
|
|
||||||
|
-- Example 3: Store installer JSON (service role)
|
||||||
|
-- POST /rpc/store_installer_json
|
||||||
|
-- Headers: Authorization: Bearer <SERVICE_ROLE_KEY>
|
||||||
|
-- Body: {"customer_email_param": "max@beispiel.de", "lxc_id_param": 769697636, "installer_json_param": {...}}
|
||||||
|
|
||||||
|
-- Example 4: Link customer to auth user (service role)
|
||||||
|
-- POST /rpc/link_customer_to_auth_user
|
||||||
|
-- Headers: Authorization: Bearer <SERVICE_ROLE_KEY>
|
||||||
|
-- Body: {"customer_email_param": "max@beispiel.de", "auth_user_id_param": "550e8400-e29b-41d4-a716-446655440000"}
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- End of Supabase Auth API
|
||||||
|
-- =====================================================
|
||||||
@@ -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
test_installer_json_api.sh
Normal file
365
test_installer_json_api.sh
Normal 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
|
||||||
Reference in New Issue
Block a user