API-Referenz

Verbindungen, Remote-Modus und Headless-Betrieb

Eine Verbindung ist eine gespeicherte andere Instanz (Name, Adresse, API-Token mit Schreibrecht dort). Verbindungen gehören zur lokalen Instanz, sind nur per Admin-Session erreichbar 👤 und nicht im Bundle; der Token wird nie ausgeliefert (nur token_prefix).

{ "id": 1, "name": "Live-Server", "url": "https://www.firma.de", "token_prefix": "ic_Ab3dEf4",
  "last_sync_at": 1791313300, "last_sync": "push", "created_at": 1791313195 }
Endpunkt Beschreibung
GET /connections Alle Verbindungen
POST /connections { "name", "url", "token" } → 201. Die Adresse wird normalisiert (ohne Schema: https://)
PUT /connections/{id} Name, Adresse, Token ändern (leerer Token = unverändert). Eine andere Adresse setzt den Abgleich-Stand zurück
DELETE /connections/{id} Löschen → 204
GET /connections/{id}/status Fragt den Server ab: reachable, error, remote (Antwort von /bundle/info des Servers), local_schema, synced, local_changed / remote_changed (Inhalt hier bzw. dort seit dem letzten Abgleich geändert, Live-Daten ausgenommen), local_version, local_commit, version_mismatch (anderes Schema oder andere Version – bei Entwicklungs-Builds anderer Commit)
POST /connections/select { "id": 1 } schaltet den Remote-Modus ein, { "id": 0 } aus → 204

Remote-Modus: select setzt das Cookie ic_remote. Solange es gesetzt ist, reicht die Instanz alle Anfragen einer Admin-Session an /api/v1/…, /media/… und /_assets/… mit dem gespeicherten Token an den Server der Verbindung weiter – das Admin arbeitet dann live mit ihm. Ausgenommen (bleiben lokal): /auth/…, /users, /tokens, /connections…, /bundle/push, /bundle/pull. Schreibende Anfragen brauchen wie sonst X-IC-CSRF; ohne Session wird nichts weitergereicht. GET /auth/status enthält dann remote (die Verbindung). Weitergereichte Antworten tragen Cache-Control: no-store, weil dieselbe Adresse je nach Auswahl von einer anderen Instanz kommt.

Headless-Betrieb: ohne Admin-App, Anmeldung und Sitzungen – fest per IC_HEADLESS=1 bzw. -headless oder umschaltbar per PUT /system. /cms-admin, /auth/… und alle 👤-Endpunkte liefern 404, Session-Cookies werden ignoriert. Website, Token-API, HTTP-Eingänge und öffentliche Pens laufen normal; /healthz und /bundle/info melden "headless": true. Einen Token legt illusioncms -data <ordner> -create-token "Name" an (Schreibrecht, Ausgabe auf stdout).

Master-Token (IC_MASTER_TOKEN, mindestens 32 Zeichen): gilt wie ein API-Token mit Schreibrecht (Header oder bei GET ?token=), ohne Benutzerkonto und ohne Eintrag in der Datenbank – für Server, die nur per Verbindung gepflegt werden, und als Notausgang. Konto- und Token-Verwaltung (👤) bleibt ihm verschlossen. Autor im Verlauf: Master-Token.

GET /system ✏️ · PUT /system ✏️

Betrieb dieser Instanz – auch per Token, also aus einer anderen Instanz über eine Verbindung.

{ "admin_enabled": true, "forced": false, "write_tokens": 2, "master_token": false, "version": "v0.1.9" }

PUT mit { "admin_enabled": false } schaltet das Admin ab (gespeichert, gilt auch nach einem Neustart), true wieder ein. 409, wenn das Admin per Startoption abgeschaltet ist (forced) oder beim Abschalten weder ein Token mit Schreibrecht noch ein Master-Token existiert (sonst käme niemand mehr hinein). Auf dem Server selbst schaltet illusioncms -enable-admin es wieder ein.

Sicherheits-Header

GET /security ✏️ · PUT /security ✏️ – Einstellungen dieser Instanz (nicht im Bundle) und die daraus erzeugten Header. PUT ist ein Merge-Update.

{
  "settings": {
    "referrer_policy": "strict-origin-when-cross-origin",
    "framing": "sameorigin",
    "frame_domains": [],
    "hsts": false, "hsts_max_age": 31536000, "hsts_subdomains": false, "hsts_preload": false,
    "permissions_policy": false,
    "csp": "", "csp_report_only": false,
    "hide_version": false
  },
  "headers": {
    "all": [{ "name": "X-Content-Type-Options", "value": "nosniff" }, { "name": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }],
    "site": [{ "name": "X-Frame-Options", "value": "SAMEORIGIN" }, { "name": "Content-Security-Policy", "value": "frame-ancestors 'self'" }]
  }
}
Feld Werte
referrer_policy ein Wert der Referrer-Policy-Spezifikation oder "" (kein Header)
framing sameorigin, deny, any (kein Schutz) oder domains mit frame_domains (https://partner.de, *.partner.de). Gesendet als X-Frame-Options und/oder CSP frame-ancestors; steht frame-ancestors schon in csp, gilt diese Angabe
hsts* max-age 0 bis 63072000 s; preload braucht hsts_subdomains und mindestens 1 Jahr. Nur über HTTPS (direkt oder X-Forwarded-Proto: https von einem lokalen/privaten Proxy)
permissions_policy sperrt Kamera, Mikrofon, Standort, Zahlungen, USB, Serial, Bluetooth, HID, MIDI, Topics
csp, csp_report_only eigene Content-Security-Policy (max. 4000 Zeichen, Zeilenumbrüche werden zu Leerzeichen); report_only sendet sie als …-Report-Only
hide_version /healthz liefert ohne Anmeldung nur status und db

all gilt für alle Antworten, site nur für Website-Seiten (nicht /cms-admin, /api, /media, /_assets, /healthz). Ungültige Werte: 400.

IllusionDB-Verbindungen

Verbindungen zu IllusionDB-Servern für den Workflow-Node illusiondb und idb.* im Function-Node. Gespeichert in der Instanz (nicht im Bundle); Workflows verweisen über den Namen.

GET /illusiondb ✏️ – [{ "name": "shop", "url": "https://db.firma.de", "has_token": true }] (der Token wird nie ausgegeben).

PUT /illusiondb ✏️ – ersetzt die Liste: [{ "name": "shop", "url": "https://db.firma.de", "token": "…" }]. Name: Buchstaben, Ziffern, Leerzeichen, . _ - (max. 64, eindeutig ohne Groß-/Kleinschreibung); Adresse http(s)://… (ein angehängtes /api/v1 wird entfernt). Fehlt token (oder ist null), bleibt der bisherige der Verbindung – beim Umbenennen über old_name zugeordnet; "" entfernt ihn. Antwort wie GET.

POST /illusiondb/{name}/test ✏️ – { "ok": true, "version": "1.4.0", "tables": 12 }, sonst 502 mit der Meldung von IllusionDB (z. B. authentication required).

GET /illusiondb/{name}/tables ✏️ – lesbare Tabellen aller Datenquellen (ohne n:m-Hilfstabellen, eine Minute zwischengespeichert, ?fresh=1 lädt neu): [{ "id", "name", "schema", "source", "source_id", "ref", "public_read" }]. ref ist der Name, wie ihn der Node verwendet (bei Mehrdeutigkeit Datenquelle/name).

GET /illusiondb/{name}/tables/{table} ✏️ – {table} wie im Node (Name, Datenquelle/name oder ID): { "table": {…}, "columns": [{ "name", "ui_type", "field_kind", "primary_key", "nullable" }], "views": [{ "id", "name", "type", "default" }] }. Unbekannte Tabelle: 404.

E-Mail-Versand

GET /system/mail ✏️ – SMTP-Zugang für den Workflow-Node mail: { "host": "smtp.firma.de", "port": 587, "user": "web@firma.de", "from": "Website <web@firma.de>", "has_password": true } (das Passwort wird nie ausgegeben).

PUT /system/mail ✏️ – gleiche Felder plus password; fehlt password, bleibt das bisherige. Port 465 = TLS, sonst STARTTLS (mit Benutzer Pflicht). Gespeichert in der Instanz, nicht im Bundle.

POST /system/mail/test ✏️ mit { "to": "ich@firma.de" } schickt eine Testmail – 204, sonst 502 mit der Meldung des Servers.

Medienspeicher

Ablage der Mediendateien dieser Instanz: lokal (data/media, Standard) oder ein S3-kompatibler Speicher (Garage, AWS, Hetzner, Cloudflare R2 …). Gespeichert in der Einstellung storage (nicht im Bundle). Die Adressen bleiben /media/… – das CMS liefert auch S3-Dateien selbst aus (Range, 304), der Bucket bleibt privat. Alle Endpunkte gehen auch per Token, also über eine Verbindung im Remote-Modus.

GET /storage ✏️

{
  "type": "s3",
  "s3": { "endpoint": "https://fsn1.your-objectstorage.com", "region": "fsn1", "bucket": "firma-media", "prefix": "cms/",
          "access_key": "AKIA…", "path_style": false, "secret_set": true },
  "cache_mb": 0,
  "cache": { "enabled": true, "max_bytes": 536870912, "used_bytes": 10485760, "entries": 120, "default_bytes": 536870912 },
  "media": { "files": 812, "bytes": 734003200 },
  "migration": { "status": "idle" },
  "encryption": "file"
}

Das Secret wird nie ausgegeben (secret_set). Bei type: "local" steht eine eingetragene S3-Konfiguration als Entwurf in s3. media zählt die Objekte der aktiven Ablage (eine Minute zwischengespeichert, partial: true nach 20 s Zeitlimit). encryption: Schlüssel aus IC_ENCRYPTION_KEY (env) oder data/secret.key (file). Ließ sich S3 beim Start nicht aktivieren (z. B. Schlüssel geändert), stehen error/error_code dabei und die lokale Ablage ist aktiv.

PUT /storage ✏️ – { "s3": { "endpoint", "region", "bucket", "prefix", "access_key", "secret_key", "path_style" }, "cache_mb": 0 }. Fehlt secret_key (oder ist leer), bleibt das gespeicherte – solange access_key gleich bleibt. Ist S3 aktiv, wird die neue Konfiguration erst getestet und gilt dann sofort; bucket/prefix lassen sich dann nur per Migration ändern (409 storage.migrationRequired). Bei lokaler Ablage ist es nur ein Entwurf; leere Felder löschen ihn. cache_mb (0 = Startoption -media-cache-mb) ist die Größe des Plattencaches vor S3. Antwort wie GET.

POST /storage/check ✏️ – { "s3": {…} } (ohne s3: die gespeicherte Konfiguration) testet mit einem Probe-Objekt (schreiben, lesen, löschen): { "ok": true }, sonst 400 (Konfiguration, z. B. storage.endpointInvalid) bzw. 502 mit storage.unreachable, storage.authFailed, storage.noBucket, storage.accessDenied oder storage.checkFailed und der Meldung des Speichers in params.error.

POST /storage/migrate ✏️ – geführte Migration als Hintergrund-Job: { "target": "s3", "s3": {…}, "delete_old": false } bzw. { "target": "local" }. Antwort 202 mit dem Status. Ablauf (phase): check (Ziel testen) → scan → copy (übersprungen wird, was am Ziel mit gleicher Größe und nicht älter schon liegt) → verify (jedes kopierte Objekt am Ziel vorhanden, gleiche Größe) → switch (Einstellung speichern, umschalten, Seiten-Cache leeren) → optional cleanup (alte Objekte löschen). Schreibzugriffe während der Migration (Uploads, Löschen, Bundle-Import) gehen an die alte Ablage und werden mitgeschrieben; beim Umschalten werden sie kurz angehalten, die geänderten Dateien nachgezogen, danach landen sie am Ziel. Fehler und Abbruch lassen die alte Ablage aktiv. 409 bei laufender Migration (storage.migrationRunning) oder gleichem Ziel (storage.sameTarget).

GET /storage/migrate ✏️ – Status:

{ "status": "running", "phase": "copy", "from": "local", "to": "s3", "delete_old": false,
  "total_files": 812, "total_bytes": 734003200, "done_files": 300, "done_bytes": 201326592,
  "skipped_files": 0, "verified_files": 0, "changed_files": 0, "deleted_files": 0, "started_at": 1791571753 }

status: idle, running, done, failed (mit error, error_code, error_params), canceled. changed_files = beim Umschalten nachgezogene Änderungen, cleanup_errors = alte Objekte, die sich nicht löschen ließen (nur Warnung).

DELETE /storage/migrate ✏️ – bricht ab (bis vor switch; danach 409 storage.migrationTooLate, ohne Job 409 storage.migrationNotRunning).