OBS/Kostenpflichtige Module/RESTServer/Scripting: Unterschied zwischen den Versionen

Aus OBS Wiki
Zur Navigation springen Zur Suche springen
Böhrer (Diskussion | Beiträge)
Die Seite wurde neu angelegt: „Photo Credіt rating Graphic Ƅy Flickr.com, courtesy of Gordana Adamοvic-Mladenovic Mom and dad ϲonsiderably hаve an effect on their children�s habits.<b…“
 
Rademacker (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
 
(12 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
Photo Credіt rating Graphic Ƅy Flickr.com, courtesy of Gordana Adamοvic-Mladenovic Mom and dad ϲonsiderably hаve an effect on their children�s habits.<br>Children are like sponges--thеy product almоst everүthіng a guardian does and include what tҺey see into tҺeir individuɑl life. It is essential that dad and mom eѕtаblishеd the right illustrаtions fⲟr their kids. Detrimental illustrations can be harmful to a chilԁ�s advancement and can direct to undesirable habits.<br><br>No time to exercise routine? Master much more about LIVESTRONG.COM's nourishment and fitness apрⅼication! If yⲟu liked this article and you also would like to гecеive more info reǥarding [http://194.44.219.205/mediawiki/index.php/How_Consuming_Healthful_Harmful_Food_items_Impacts_Your_Body custom essay] i implore үou to viѕit the internet site. Socіal Expertise Rely Antisocial kids master their conduct from their parents� examρⅼes, according to research completed by the Coⅼlᥱge of Chicago revealed in the Јournal of Abnormal Kid Psychology.<br><br>Social abilіtieѕ can be interpreted as aⅼl the things from the standard well mannered �please� ɑnd �thank you� to speaking in entrance of crowds. Kids proɗuct their moms and dаdѕ and find out from them.<br><br>A Pressured-Out Legacy A parent�s reaction to prеssᥙre influencеs the way a child reacts to anxiety, states the web page More4Kidѕ. If a parent reacts negatively, a lіttle one will master to react negɑtively as well. In addition, adverse reactions to anxiety, this kind of as yelling and lashing out, can scare a kid.<br>Childrеn can master to shut them seⅼves down and may well even belieѵe that they are the induce of the tension. If pressure is taken care of positivеlʏ, it assists young children see that their paгents' aԀore for them never ever improvements, even when they are strеssed oսt.<br><br>Keᥱp Self-сontrol Positive The way a mother or father disciplines tremendously affects their chіldren's behavioг, as dеscribed on FamіⅼyDoctor.org. When a ⲣarent elects to use physical punishmеnt, these kinds of as spanking, it does not [http://www.magaltc.com educate] the little one how to alter his conduсt. Little oneѕ can also respond aggressively to bodily punishment.<br>When dad and mom chooses alternate soгts of ρunishment, these types of as time-outs, tһey arе serving to modіfy the child�s terrible actions in a quiet manner.<br><br>Fighting Frenzy If argսing among the motheгs and fathers is cоmpleted quite and with maturity, a kid can truly reward frοm seeing how confⅼictѕ are settled. Verbaⅼ and actսal physical fights are extremely difficult on children, warns the Youngster-Self-control-with-Adore web site. Young cһіldren might blame by themselves for their parents� argumentѕ and maу well be traumatizing for years tօ appear.<br>Children could develop minimal ѕelf-eѕteems and may well even behave violently towards other children. Dysfunctional families breed dysfunctional young children. Young children generally repeat this habits in their potential associаtions.<br><br>Child Abuse Deѕtroyѕ Boy or giгl аbuѕe triggers a range of antisocіal and destructive behaviors, in accordance to the web site HealthyPlace.com. This is simрly becauѕe abusеd youngsters try out to cope and to recognize why they arе ǥetting abused. Dad аnd mom who abuse tҺeir yoսng children could trigger their little ones to be intense аnd violent, expertise finding out complications and eᴠen turn into іnclսded in medicine or alcoholic beverages.<br><br>[https://En.wikipedia.org/wiki/Parents%27_Day Parents] who abuse present the reverse of what a kid demands to expand uр healthful. Rather, thеy damage the within and outsіde worⅼd of а boy or girl.
{{Kostenpflichtige Module}}
 
=Anleitung für Endpunkt-Skripte=
 
Jede Anfrage an einen Endpunkt wird nach erfolgreicher Authentifizierung und Autorisierung an das hinterlegte Skript weitergegeben. Das Skript ist in Object Pascal geschrieben und greift auf die OBS-Bibliothek (DB-Zugriff, Hilfsfunktionen) zu.
 
==Sicherheitshinweise==
 
{{Hinweis|Endpunkt-Skripte verarbeiten Daten aus dem Internet. Übergabeparameter dürfen niemals ungeprüft in SQL-Anweisungen eingebaut werden. Werte immer über ''DB_SQLVal'' bzw. Parameter-Bindings absichern, Eingabewerte gegen Whitelists prüfen.}}
 
* Eingaben gegen erwartete Werte prüfen (z.B. Pflichtfelder, erlaubte Typen, Wertebereiche).
* Nur das zurückgeben, was der Konsument wirklich braucht - keine internen IDs, keine Sys-Felder, keine Passwörter.
* Bei Fehlern keine internen Details an den Konsumenten zurückgeben; stattdessen schreibt der Server ohnehin Detail-Einträge in '''RESTSRV_PROTO'''.
 
==Methoden-Signatur==
 
Pro HTTP-Methode wird im Skript eine gleichnamige Funktion implementiert. Der Server ruft genau die Funktion auf, die zur Methode des eingehenden Requests passt.
 
function Get  (oParams: TStrings; oBody: TJSONObject): string;
function Post  (oParams: TStrings; oBody: TJSONObject): string;
function Put  (oParams: TStrings; oBody: TJSONObject): string;
function Delete(oParams: TStrings; oBody: TJSONObject): string;
function Patch (oParams: TStrings; oBody: TJSONObject): string;
 
Nicht implementierte Methoden liefern automatisch ''405''-ähnliche Fehler über die generische Skript-Antwort.
 
==Parameter==
 
===oParams (TStrings)===
 
Enthält alle Query-Parameter, POST-Parameter (form-urlencoded) sowie die durchgereichten HTTP-Header. Werte sind immer Strings.
 
Filterung durch den Server:
 
* Geblockte Header werden nicht durchgereicht: ''authorization'', ''cookie'', ''proxy-authorization'', ''x-forwarded-for'', ''x-real-ip'', ''apikey'', ''api_key''.
* Parameter mit Präfix '''_OBS_''' können von aussen nicht gesetzt werden; sie sind für interne Werte reserviert.
* Werte werden auf max. 1024 Zeichen begrenzt.
* Null-Bytes und Steuerzeichen (ausser Tab, CR, LF) werden entfernt.
 
Zugriff im Skript:
 
if (not Empty(oParams.Values['kundennr'])) then begin
    cKundenNr := oParams.Values['kundennr'];
end;
 
===oBody (TJSONObject)===
 
Enthält den Request-Body, sofern dieser ein gültiges JSON-Objekt ist. Bei leerem oder nicht-JSON-Body wird ''nil'' übergeben.
 
{{Hinweis|'''Generics funktionieren im Skript nicht.''' Aus Delphi bekannte
Schreibweisen wie ''oBody.TryGetValue&lt;string&gt;('feld', cVar)'' lassen sich nicht
übersetzen. Gelesen wird über ''GetValue''.}}
 
''GetValue'' liefert ''nil'', wenn das Feld im Body '''nicht enthalten''' ist -
daran lässt sich „nicht gesendet" von „leer gesendet" unterscheiden. Der Wert ist
immer ein '''String''' und wird bei Bedarf selbst gewandelt.
 
cUser := '';
cPass := '';
if (Assigned(oBody)) then begin
    oVal := oBody.GetValue('username');
    if (Assigned(oVal)) then begin
        cUser := oVal.Value;
    end;
    oVal := oBody.GetValue('password');
    if (Assigned(oVal)) then begin
        cPass := oVal.Value;
    end;
end;
 
Bei mehr als zwei Feldern lohnt eine kleine Hilfsfunktion, die das Muster einmal
kapselt:
 
<syntaxhighlight lang="pascal" line>
function _BodyStr(oBody: TJSONObject; const cFeld: string): string;
var oVal: TJSONValue;
begin
    result := '';
    if (oBody = nil) then begin
        exit;
    end;
    oVal := oBody.GetValue(cFeld);
    if (Assigned(oVal)) then begin
        result := oVal.Value;
    end;
end;
 
// Aufruf:
cBetreff := _BodyStr(oBody, 'betreff');
</syntaxhighlight>
 
Andere Typen entstehen aus dem String:
 
{| class="wikitable"
! Zieltyp !! Umwandlung
|-
| Ganzzahl || <code>iVal(_BodyStr(oBody, 'menge'))</code>
|-
| Kommazahl || <code>fVal(StrTran(_BodyStr(oBody, 'preis'), '.', ','))</code> - '''JSON liefert den Dezimalpunkt''', ''fVal'' erwartet die lokale Notation
|-
| Boolean || <code>Lower(_BodyStr(oBody, 'aktiv')) = 'true'</code>
|-
| Datum/Zeit || ISO-8601-String selbst zerlegen (''CToDT'' erwartet das deutsche Format)
|}
 
Maximale Body-Grösse: 10 MB.
 
===Reservierte _OBS_-Parameter===
 
Bei aktiver JWT-Authentifizierung stehen die Token-Claims als Parameter zur Verfügung:
 
{| class="wikitable"
! Parameter            !! Inhalt
|-
| _OBS_JWT_ID          || JWT-Id (''jti''-Claim) - typisch die User-Id. '''Muss nicht eindeutig sein''': die Sitzung führt der Server über eigene Kennungen (siehe unten)
|-
| _OBS_JWT_SUBJECT    || Subject (''sub''-Claim) - typisch Benutzername
|-
| _OBS_JWT_AUDIENCE    || Audience (''aud''-Claim) - typisch Mandant / Rolle
|-
| _OBS_JWT_CLAIM_&lt;name&gt; || Beliebiger Custom-Claim des Tokens (z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles); im Folge-Skript lesbar
|-
| _OBS_TRACE_ID        || Korrelations-ID des Requests (auch als Response-Header X-Trace-Id)
|}
 
Unabhängig von JWT stehen ausserdem immer zur Verfügung:
 
{| class="wikitable"
! Parameter            !! Inhalt
|-
| _OBS_SERVER_TIME    || Aktuelle Serverzeit als ISO-8601 '''mit UTC-Offset''' (z.B. ''2026-08-17T09:12:33+02:00''). Nützlich, um Datumswerte in derselben Zeitzone auszuliefern, in der der Server arbeitet, und damit ein Client seinen Uhren-Versatz bestimmen kann
|-
| _OBS_JWT_EXP_SEC    || Laufzeit des Access-Tokens in Sekunden (''JWT-Exp'' × 60). Damit kann ein Konfigurations-Endpunkt denselben Wert ausliefern, den der Token wirklich hat
|}
 
Diese Werte werden vom Server gesetzt und können vom Skript für Berechtigungs- und Mandantenprüfungen verwendet werden.
 
Zusätzlich trägt jedes Token zwei '''Sitzungskennungen des Servers''', lesbar als
''_OBS_JWT_CLAIM_sid'' und ''_OBS_JWT_CLAIM_rid'':
 
{| class="wikitable"
! Claim !! Inhalt
|-
| sid || Sitzung. Bleibt über alle Erneuerungen (Refresh) hinweg gleich und ist der Schlüssel für das Abmelden
|-
| rid || Zeilenkennung des einzelnen Token-Paares in ''RESTSRV_TOKEN''. Bei jeder Erneuerung neu
|}
 
Beide sind OBS-UIDs (10 Zeichen) und werden vom Server '''nach''' den Claims des
Skripts gesetzt - ein Skript kann sie also auch mit ''_OBS_JWT_CLAIM_sid'' nicht
überschreiben. Für Fachlogik sind sie nicht gedacht; sie sind nützlich, um eine
Sitzung im Protokoll wiederzufinden.
 
===Pfad-Parameter===
 
Stammt der Endpunkt aus einem Pfad-Template mit Platzhaltern (siehe [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]), stehen die aus den Platzhaltern erfassten Werte als reservierte Parameter mit Präfix '''_OBS_PATH_''' in ''oParams'' bereit:
 
{| class="wikitable"
! Template !! Zugriff im Skript
|-
| <code>/orders/{uid}</code> || ''oParams.Values['_OBS_PATH_uid']''
|-
| <code>/orders/{uid}/modules/{code}</code> || ''oParams.Values['_OBS_PATH_uid']'', ''oParams.Values['_OBS_PATH_code']''
|}
 
Da der Präfix ''_OBS_'' für von aussen gelieferte Header- und Query-Parameter gesperrt ist, sind diese Werte nicht durch den Client fälschbar. Ein vollständiges Beispiel zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]].
 
===Datei-Uploads===
 
Ist der Endpunkt für Uploads freigeschaltet (<code>re_upload = 1</code>, siehe
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]), nimmt der Server
hochgeladene Dateien entgegen, legt sie in einem temporären Verzeichnis ab und
übergibt dem Skript Pfad, Originalname und Content-Type über reservierte
Parameter. Das Skript entscheidet selbst über die weitere Verarbeitung (z.B.
DMS-Ablage). Der Body wird in diesem Fall '''nicht''' als JSON geparst - ''oBody''
ist ''nil''. Es gilt nicht das JSON-Body-Limit (10&nbsp;MB), sondern die pro
Endpunkt konfigurierte Grösse (''re_upload_size'', Standard 25&nbsp;MB;
Überschreitung -> 413).
 
Beide Übertragungsarten - <code>multipart/form-data</code> und der resumable
<code>Content-Range</code>-Upload - liefern dem Skript dieselben Parameter:
 
{| class="wikitable"
! Parameter !! Inhalt
|-
| _OBS_UPLOAD_PATH || Vollständiger Pfad zur temporären Datei auf dem Server
|-
| _OBS_UPLOAD_NAME || Originaldateiname
|-
| _OBS_UPLOAD_CONTENTTYPE || Content-Type der Datei (Default <code>application/octet-stream</code>, wenn der Client keinen angibt)
|}
 
Der '''Dateiname ist Pflicht'''. Fehlt er, lehnt der Server den Upload mit
'''400 Bad Request''' ab und das Skript wird nicht ausgeführt - das Skript kann
sich also darauf verlassen, dass ''_OBS_UPLOAD_PATH'' und ''_OBS_UPLOAD_NAME''
gesetzt sind, sobald es läuft.
 
{{Hinweis|Die temporäre Datei wird nicht automatisch verschoben. Das Skript muss sie an ihren Zielort (DMS, Verzeichnis, ...) übernehmen.}}
 
====Einfacher Upload (multipart/form-data)====
 
Eine Datei pro Request. Name und Content-Type stammen aus der
<code>Content-Disposition</code> der Datei-Partie (<code>filename=</code>). Der
Server speichert die erste Datei-Partie und ruft das Skript auf:
 
<syntaxhighlight lang="pascal" line>
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
    cPfad: string;
    cName: string;
    cTyp : string;
begin
    oRes := TJSONObject.Create();
    try
        cPfad := oParams.Values['_OBS_UPLOAD_PATH'];
        cName := oParams.Values['_OBS_UPLOAD_NAME'];
        cTyp  := oParams.Values['_OBS_UPLOAD_CONTENTTYPE'];
 
        // ... Datei aus cPfad ins DMS / Zielverzeichnis uebernehmen ...
 
        oRes.AddPair('status'  , 'ok');
        oRes.AddPair('dateiname', cName);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
 
====Resumable/Chunked Upload (Content-Range)====
 
Für grosse Dateien überträgt der Client die Datei in Teilstücken (Chunks) mit dem
Header <code>Content-Range: bytes START-END/TOTAL</code>. Der Server hängt die
Chunks '''sequentiell''' an eine Session-Datei an und behandelt unvollständige
Uploads selbst - das Endpunkt-Skript läuft '''erst beim vollständigen Upload'''
(dann mit gesetztem ''_OBS_UPLOAD_PATH'', ''_OBS_UPLOAD_NAME'' und
''_OBS_UPLOAD_CONTENTTYPE'').
 
Name und Content-Type sind im <code>Content-Range</code>-Protokoll nicht enthalten;
der Client liefert sie deshalb '''im ersten Chunk''' über den Header
<code>Upload-Metadata</code> (Format wie bei tus: kommagetrennte Paare
<code>schlüssel base64wert</code>, Werte Base64-kodiert):
 
<pre>
Upload-Metadata: filename ZmlsZS5wZGY=,filetype YXBwbGljYXRpb24vcGRm
</pre>
 
Erkannt werden die Schlüssel <code>filename</code> (Pflicht) und
<code>filetype</code> (optional). Fehlt <code>filename</code> im ersten Chunk,
wird der Upload mit '''400''' abgelehnt, ohne dass eine Datei angelegt wird.
Folge-Chunks und Statusabfragen müssen die Metadaten nicht erneut senden.
 
Ablauf (vom Client gesteuert, der Server antwortet jeweils):
 
{| class="wikitable"
! Request !! Server-Antwort
|-
| Erster Chunk <code>Content-Range: bytes 0-1048575/5000000</code> + <code>Upload-Metadata</code> || '''308''' Resume Incomplete + Header <code>Upload-Id</code>, <code>Upload-Offset</code>, <code>Range</code>
|-
| weitere Chunks (jeweils <code>Upload-Id</code> mitsenden) || '''308''' mit aktualisiertem <code>Upload-Offset</code>
|-
| letzter Chunk (Bereich erreicht TOTAL) || normale Skript-Antwort (z.B. '''200'''/'''201''')
|-
| Statusabfrage <code>Content-Range: bytes */5000000</code> || aktueller <code>Upload-Offset</code>, ohne anzuhängen (Resume)
|}
 
{| class="wikitable"
! Header !! Richtung !! Bedeutung
|-
| Content-Range || Request || <code>bytes START-END/TOTAL</code>; <code>bytes */TOTAL</code> nur als Statusabfrage
|-
| Upload-Metadata || Request || tus-Metadaten im ersten Chunk: <code>filename</code> (Pflicht), <code>filetype</code> (optional), Base64-kodiert
|-
| Upload-Id || Request/Response || Session-Kennung. Fehlt sie im ersten Request, erzeugt der Server eine und gibt sie zurück; alle Folge-Chunks müssen sie mitsenden.
|-
| Upload-Offset || Response || Anzahl der bereits gespeicherten Bytes (= Startoffset des nächsten Chunks)
|-
| Range || Response || Bereits gespeicherter Bereich (<code>bytes=0-N</code>)
|}
 
Der Server hängt einen Chunk '''nur an, wenn START dem bereits gespeicherten Stand
entspricht'''. Bei Abweichung (doppelter Chunk, Lücke) wird nicht angehängt,
sondern der aktuelle ''Upload-Offset'' zurückgemeldet - der Client setzt ab dort
neu auf. Eine separate Statusabfrage ist für ein Resume daher nicht zwingend nötig.
 
Überschreitet ein Chunk bzw. die Gesamtgrösse das Limit (''re_upload_size''),
antwortet der Server mit '''413 Payload Too Large'''.
 
{{Hinweis|Der Upload-Status liegt prozesslokal im Speicher. Nach einem Neustart des Servers muss ein unvollständiger Upload neu begonnen werden.}}
 
==Rückgabe==
 
Die Rückgabe ist immer ein '''JSON-String'''. Wird ein leerer String zurückgegeben, antwortet der Server automatisch mit ''{}''. Der Server setzt Content-Type auf ''application/json; charset=utf-8'' und Status auf 200, sofern das Skript nicht selbst einen Fehler signalisiert.
 
Einfaches Beispiel:
 
<syntaxhighlight lang="pascal" line>
function Get(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        oRes.AddPair('wert_string', '123');
        oRes.AddPair('wert_int'  , 456);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
 
==Antwort steuern: Statuscode, Header, traceId==
 
Ein Skript kann den HTTP-Statuscode und beliebige Response-Header über reservierte Felder in der Antwort setzen. Der Server wertet sie aus und entfernt sie vor dem Senden aus dem Body.
 
{| class="wikitable"
! Antwort-Feld !! Wirkung
|-
| _OBS_HTTP_STATUS (Zahl) || HTTP-Statuscode (z.B. 201, 204, 400, 409, 422). Ohne Angabe: 200. Bei 204 wird kein Body gesendet.
|-
| _OBS_HEADERS (Objekt) || Beliebige Response-Header, z.B. ETag, Location, Retry-After. CR/LF in Namen/Werten werden entfernt (Schutz vor Header-Injection).
|-
| _OBS_JWT_REVOKE (Text) || Sperrt die Sitzung des vorgelegten Tokens (''session'') oder alle Sitzungen des Subjects (''all''). Damit wird ein Endpunkt zum Logout, ohne eigene Verwaltung. Scheitert das Sperren, antwortet der Server mit '''503''' statt mit der Antwort des Skripts - siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]], Abschnitt Abmelden.
|}
 
Zusätzlich trägt jede Antwort den Header '''X-Trace-Id''' (Korrelations-ID). Dieselbe ID liegt dem Skript als ''oParams.Values['_OBS_TRACE_ID']'' vor und erscheint in jeder Server-Fehler-Logzeile in '''RESTSRV_PROTO''' - so lässt sich ein Fehler ohne Gerätezugriff im Log wiederfinden.
 
===Beispiel: Anlegen mit 201, Location und ETag===
 
<syntaxhighlight lang="pascal" line>
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        // ... Datensatz anlegen, neue UID + Version (ETag) ermitteln ...
        oHdr := TJSONObject.Create();
        oHdr.AddPair('Location', '/v1/orders/' + cNeueUid);
        oHdr.AddPair('ETag', '1');
 
        oRes.AddPair('_OBS_HTTP_STATUS', 201);
        oRes.AddPair('_OBS_HEADERS', oHdr);
        oRes.AddPair('uid', cNeueUid);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
 
===Beispiel: Strukturierter Fehler mit Statuscode und traceId===
 
<syntaxhighlight lang="pascal" line>
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        oErr := TJSONObject.Create();
        oErr.AddPair('code', 'VALIDATION_FAILED');
        oErr.AddPair('message', 'Feld "menge" fehlt');
        oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
 
        oRes.AddPair('_OBS_HTTP_STATUS', 422);
        oRes.AddPair('error', oErr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
 
===Beispiel: Optimistic Concurrency (ETag / If-Match)===
 
Mit Statuscode, Headern und dem lesbaren Header ''If-Match'' führt das Skript je Datensatz einen Versionszähler und lehnt veraltete Schreibzugriffe mit 409 ab:
 
<syntaxhighlight lang="pascal" line>
function Put(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
    nAktuell, nIfMatch: integer;
begin
    oRes := TJSONObject.Create();
    try
        nAktuell := AuftragVersion(oParams.Values['_OBS_PATH_uid']);
        nIfMatch := iVal(oParams.Values['if-match']);
 
        if (nIfMatch <> nAktuell) then begin
            oHdr := TJSONObject.Create();
            oHdr.AddPair('ETag', xStr(nAktuell));
            oRes.AddPair('_OBS_HTTP_STATUS', 409);
            oRes.AddPair('_OBS_HEADERS', oHdr);
            oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'VERSION_CONFLICT'));
            result := oRes.ToJSON();
            exit;
        end;
 
        // ... speichern, Version hochzählen ...
        oHdr := TJSONObject.Create();
        oHdr.AddPair('ETag', xStr(nAktuell + 1));
        oRes.AddPair('_OBS_HEADERS', oHdr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
 
==Idempotenz - was das Skript beachten muss==
 
Sendet ein Konsument bei ''POST''/''PUT''/''PATCH''/''DELETE'' den Header
<code>Idempotency-Key</code>, fängt der '''Server''' doppelte Sendungen ab. Das
Skript braucht dafür '''keine eigene Logik''' - keine Schlüssel-Tabelle, keine
Prüfung am Anfang der Methode.
 
Was das Skript wissen muss:
 
{| class="wikitable"
! Antwort des Skripts !! Wirkung auf den Idempotenz-Speicher
|-
| '''2xx''' || Statuscode, Body und Header werden gespeichert. Eine Wiederholung mit demselben Schlüssel bekommt genau diese Antwort zurück, das Skript läuft nicht erneut
|-
| '''4xx''' || Der Schlüssel wird '''freigegeben'''. Der Konsument darf denselben Schlüssel nach Korrektur des Inhalts erneut verwenden
|-
| '''5xx''' oder Exception || Der Schlüssel bleibt belegt, der Fall wird protokolliert. Eine Wiederholung wird abgelehnt, bis der Fall geklärt ist
|}
 
Daraus folgen zwei Regeln für Endpunkt-Skripte:
 
* '''Fachliche Ablehnungen als 4xx melden''' (422, 409, 404), nicht als 2xx mit Fehlertext im Body. Sonst wird die Ablehnung gespeichert und der Konsument bekommt sie bei jedem weiteren Versuch erneut - auch nachdem er den Fehler behoben hat.
* '''Die Antwort muss vollständig sein.''' Was zurückgegeben wird, wird eingefroren. Ein Feld, das erst der zweite Aufruf ergänzen würde, kommt beim Konsumenten nie an.
 
{{Hinweis|Vor dieser Server-Funktion mussten Skripte die Idempotenz selbst
abbilden. Solche Skripte laufen unverändert weiter - die Doppelprüfung schadet
nicht, ist aber überflüssig und kann beim nächsten Überarbeiten entfallen.}}
 
Der vollständige Ablauf und die Statuscodes stehen unter
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].
 
==JWT-Authentifizierungs-Skript==
 
Bei Zugängen mit aktiver JWT-Pflicht wird über den JWT-Endpunkt das im Zugang hinterlegte Authentifizierungs-Skript aufgerufen (F7 in der Zugänge-Liste). Das Skript muss eine Methode ''Authenticate'' bereitstellen:
 
<syntaxhighlight lang="pascal" line>
function Authenticate(oParams: TStrings; oBody: TJSONObject): string;
var oRes  : TJSONObject;
    oVal  : TJSONValue;
    cUser : string;
    cPass : string;
begin
    oRes := TJSONObject.Create();
    try
        // Eingangsdaten lesen (Body oder Query-Param)
        cUser := '';
        cPass := '';
        if (Assigned(oBody)) then begin
            oVal := oBody.GetValue('username');
            if (Assigned(oVal)) then begin
                cUser := oVal.Value;
            end;
            oVal := oBody.GetValue('password');
            if (Assigned(oVal)) then begin
                cPass := oVal.Value;
            end;
        end;
 
        // Prüfung gegen eigene Tabelle, Hash-Verfahren, LDAP, ...
        if (PasswortPasst(cUser, cPass)) then begin
            oRes.AddPair('status'          , 1);
            oRes.AddPair('_OBS_JWT_ID'    , cUser);
            oRes.AddPair('_OBS_JWT_SUBJECT', cUser);
            oRes.AddPair('_OBS_JWT_AUDIENCE', 'mandant1');
        end else begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'Login fehlgeschlagen');
        end;
 
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
 
Rückgabewerte:
 
{| class="wikitable"
! status !! Bedeutung
|-
| 1      || Erfolgreich, der Server erzeugt aus den ''_OBS_JWT_*''-Werten einen Token (HS256)
|-
| 9      || Misserfolg, der Server antwortet mit '''401''' und reicht den Wert von ''error'' als ''error.message'' an den Client durch (zusätzlich protokolliert)
|}
 
{{Hinweis|Der Text aus ''error'' geht bei ''status'' = 9 '''an den Konsumenten'''
und wird typischerweise direkt unter dem Passwortfeld angezeigt. Er sollte für
Endanwender verständlich sein und keine internen Details verraten.}}
 
Liefert das Skript einen anderen Status als 1 oder 9, ist gar nicht lauffähig oder
gibt kein auswertbares JSON zurück, antwortet der Server mit '''403''' und einer
generischen Meldung - ein defektes Anmeldeskript soll dem Anwender nicht als
„Passwort falsch" erscheinen.
 
Kann der Server die '''Sitzungszeile''' zum ausgestellten Token nicht schreiben
(Tabelle fehlt, Datenbankproblem), liefert er '''kein Token''' aus und antwortet
mit '''503''' und ''Retry-After''. Auch das ist bewusst kein 401/403: die
Zugangsdaten waren richtig, der Versuch darf wiederholt werden.
 
===Aufbau der Token-Antwort===
 
Bei Erfolg baut der Server die Antwort aus den Token-Feldern '''und allen weiteren
Feldern, die das Skript zurückgibt''':
 
{"benutzerId": "4711", "tenant": "nord", "roles": ["TECHNIKER"],
  "token":      "eyJ...",
  "accessToken": "eyJ...",
  "refreshToken":"eyJ...",
  "expiresIn":  28800,
  "serverTime":  "2026-08-17T09:12:33+02:00"}
 
{| class="wikitable"
! Feld !! Herkunft
|-
| token / accessToken || Derselbe Access-Token unter zwei Namen. ''accessToken'' erwarten die meisten Client-Bibliotheken, ''token'' bleibt für bestehende Konsumenten erhalten
|-
| refreshToken || Nur wenn das Skript ''_OBS_JWT_REFRESH_ID'' geliefert hat
|-
| expiresIn || Laufzeit des Access-Tokens in '''Sekunden''' (''JWT-Exp'' × 60)
|-
| serverTime || Serverzeit ISO-8601 mit Offset
|-
| alle übrigen Felder || '''Frei vom Skript bestimmt.''' Übernommen wird alles ausser ''status'' und den ''_OBS_''-Steuerfeldern
|}
 
Damit kann das Anmeldeskript alles mitliefern, was der Client direkt nach dem Login
braucht (Benutzer-Id, Mandant, Rollen, Berechtigungen) - ohne dass dieser dafür
einen zweiten Aufruf absetzen muss. Ein Feld, das genauso heisst wie eines der
Token-Felder oben, wird verworfen; die Engine setzt diese selbst.
 
===Custom-Claims, serverTime und Refresh===
 
Das Authenticate-Skript kann dem Token zusätzlich '''beliebige Custom-Claims''' mitgeben - z.B. Mandant und Rollen - und optional ein '''Refresh-Token''' anstoßen:
 
{| class="wikitable"
! RÜckgabefeld !! Bedeutung
|-
| _OBS_JWT_CLAIM_&lt;name&gt; || Beliebiger Custom-Claim, z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles. Im Folge-Skript lesbar als oParams.Values['_OBS_JWT_CLAIM_&lt;name&gt;'].
|-
| _OBS_JWT_REFRESH_ID || (optional) jti des Refresh-Tokens. Nur wenn gesetzt, stellt der Server ein Refresh-Token aus.
|-
| _OBS_JWT_REFRESH_EXP || (optional) Lebensdauer des Refresh-Tokens in Minuten (Default 90 Tage = 129600).
|}
 
Jedes Token trägt automatisch den Claim ''token_use'' (''access'' bzw. ''refresh'') sowie die Sitzungskennungen ''sid'' und ''rid'' des Servers. Die Antwort enthält bei Erfolg ''serverTime'' (ISO-8601 mit Offset), bei ausgestelltem Refresh zusätzlich ''refreshToken'':
 
{"token":"...", "refreshToken":"...", "serverTime":"2026-06-29T15:30:12+02:00"}
 
{{Hinweis|Mandant und Rollen immer aus dem Token lesen (''_OBS_JWT_CLAIM_*''), nie aus Body oder Query.}}
 
====Refresh-Methode====
 
Der Refresh läuft über '''denselben JWT-Endpunkt''' und '''dasselbe Skript'''. Liegt am JWT-Endpunkt ein ''Authorization: Bearer <token>'' mit einem Refresh-Token vor, ruft der Server statt ''Authenticate'' die Methode '''Refresh''' auf (ohne Bearer: Login; ''DELETE'' mit Access-Token: Abmelden, ohne Skript).
 
Bevor das Skript läuft, hat der Server das vorgelegte Refresh-Token verifiziert
(Signatur, Ablauf, ''token_use=refresh'') und in der Sitzung '''entwertet'''.
Einmalgebrauch, Rotation und Sperrliste sind damit Sache des Servers. Das Skript
liefert nur noch die aktuellen Claims - genau das ist der Sinn des Aufrufs: eine
zwischenzeitlich geänderte Rolle oder ein gewechselter Mandant wirken spätestens
mit der nächsten Erneuerung.
 
{{Achtung|Eine '''eigene Sperrtabelle im Skript ist nicht mehr nötig''' und sollte
entfernt werden. Wer sie weiterführt, rotiert zweimal - einmal im Skript, einmal im
Server - und riskiert, dass beide Seiten unterschiedlicher Meinung sind. Der
Server sieht die Skript-Tabelle nicht, und das Skript sieht die Sitzung nicht.}}
 
Ein bereits benutztes Refresh-Token wird mit '''401''' abgelehnt. Erfolgt die
erneute Vorlage innerhalb von 60 Sekunden, bleibt die Sitzung bestehen (paralleler
Refresh einer App); danach gilt sie als Wiedervorlage und die '''gesamte Sitzung
wird gesperrt'''. Details in [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].
 
Das alte Refresh-jti steht weiterhin als ''oParams.Values['_OBS_JWT_ID']'' bereit,
falls das Skript es für eigene Protokollierung braucht.
 
<syntaxhighlight lang="pascal" line>
function Refresh(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
    cUser: string;
begin
    oRes := TJSONObject.Create();
    try
        cUser := oParams.Values['_OBS_JWT_SUBJECT'];
 
        // Der Server hat das vorgelegte Refresh-Token bereits geprueft und
        // entwertet. Hier wird nur entschieden, ob der Benutzer die Sitzung
        // fortsetzen darf - und mit welchen Rechten.
        if (not BenutzerAktiv(cUser)) then begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'Konto ist nicht mehr aktiv, bitte neu anmelden');
            result := oRes.ToJSON();
            exit;
        end;
 
        // Rolle und Mandant neu lesen, nicht aus dem alten Token uebernehmen -
        // sonst wirkt ein Rechteentzug erst beim naechsten Login.
        oRes.AddPair('status'              , 1);
        oRes.AddPair('_OBS_JWT_ID'          , cUser);
        oRes.AddPair('_OBS_JWT_SUBJECT'    , cUser);
        oRes.AddPair('_OBS_JWT_CLAIM_tenant', TechnikerMandant(cUser));
        oRes.AddPair('_OBS_JWT_CLAIM_roles' , TechnikerRollen(cUser));
        oRes.AddPair('_OBS_JWT_REFRESH_ID'  , GlobalUID());  // loest das neue Refresh-Token aus
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
 
===Abmelden (Logout)===
 
'''Für das Abmelden braucht das Skript keine Methode.''' Es läuft über ein
''DELETE'' auf denselben JWT-Endpunkt (Access-Token im ''Authorization''-Header)
und wird komplett in der Engine erledigt: die Sitzung wird gesperrt, die Antwort
ist 204. Eine Methode ''Logout'' gibt es nicht und wird nicht aufgerufen.
Details: [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]], Phase 4.
 
Wer beim Abmelden zusätzlich fachlich etwas tun will - eine Geräteregistrierung
löschen, einen Push-Token verwerfen, den Vorgang protokollieren - nimmt einen
'''gewöhnlichen Endpunkt''' und setzt dort ''_OBS_JWT_REVOKE''. Dasselbe gilt für
"auf allen Geräten abmelden", weil das eine Berechtigungsentscheidung braucht:
 
<syntaxhighlight lang="pascal" line>
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        GeraeteRegistrierungLoeschen(oParams.Values['_OBS_JWT_SUBJECT']);
 
        // 'session' = diese Sitzung, 'all' = alle Sitzungen des Subjects
        oRes.AddPair('_OBS_JWT_REVOKE' , 'session');
        oRes.AddPair('_OBS_HTTP_STATUS', 204);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
 
Gesperrt wird in beiden Wegen die '''ganze Sitzung''', also auch die noch nicht
abgelaufenen Access-Token vorheriger Erneuerungen. Ein wiederholter Aufruf ist
unschädlich. Lässt sich die Sitzung nicht sperren, überschreibt der Server die
Antwort des Skripts mit '''503''' - ein Client darf nicht glauben, er sei
abgemeldet, während sein Token weiterläuft.
 
==Fehlerbehandlung im Skript==
 
Tritt im Skript eine Exception auf oder schlägt die Syntax-Prüfung fehl, antwortet der Server mit 500 ''Interner Fehler'' und protokolliert die Detail-Meldung in '''RESTSRV_PROTO''' (mit Skript-Fehlertext). Der Konsument sieht keine internen Details.
 
Server-eigene Fehler (Routing, Rate-Limit, Token, Auffangnetz) haben ein
einheitliches Format mit einem ''error''-Objekt - Aufbau siehe
[[OBS/Kostenpflichtige Module/RESTServer|Übersicht]].
 
Will das Skript einen spezifischen Fehler an den Konsumenten zurückgeben, sollte es
'''dasselbe Format und einen passenden Statuscode''' verwenden:
 
<syntaxhighlight lang="pascal" line>
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        if (Empty(oParams.Values['kundennr'])) then begin
            oErr := TJSONObject.Create();
            oErr.AddPair('code'  , 'VALIDATION_FAILED');
            oErr.AddPair('message', 'Parameter ''kundennr'' fehlt');
            oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
 
            oRes.AddPair('_OBS_HTTP_STATUS', 422);
            oRes.AddPair('error', oErr);
            result := oRes.ToJSON();
            exit;
        end;
        // ...
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
 
{{Hinweis|Ein fachlicher Fehler gehört auf einen '''4xx'''-Statuscode, nicht auf
200 mit Fehlertext im Body. Nur so erkennt der Konsument den Fehlschlag ohne den
Body auszuwerten - und nur so gibt der Server einen belegten
''Idempotency-Key'' wieder frei (siehe Abschnitt Idempotenz).}}
 
Gängige Codes, die auch der Server selbst verwendet: ''VALIDATION_FAILED'' (422),
''NOT_FOUND'' (404), ''FORBIDDEN_ROLE'' (403), ''VERSION_CONFLICT'' (409).
Eigene, fachlich sprechende Codes sind erlaubt - wichtig ist, dass sie stabil
bleiben, weil Konsumenten darauf ihre Reaktion aufbauen.
 
==Gemeinsamen Code auslagern==
 
Wird Logik in mehreren Endpunkten gebraucht - Berechtigungsprüfungen,
JSON-Bausteine, Hilfsfunktionen -, muss sie nicht in jedes Skript kopiert werden.
Die Skript-Engine lädt Textbausteine beim Laden aus einer beliebigen Tabelle nach:
 
<pre>
{$L T="restsrv_endpoints" I="re_pathtemplate" V="/meinapp/lib" F="re_script"}
</pre>
 
{| class="wikitable"
! Attribut !! Bedeutung
|-
| '''T''' || Tabelle, aus der geladen wird
|-
| '''I''' || Spalte, über die gesucht wird
|-
| '''V''' || Wert, der in dieser Spalte stehen muss
|-
| '''F''' || Spalte, deren Inhalt an dieser Stelle eingesetzt wird
|}
 
Die Direktive steht üblicherweise direkt unter dem Kopfkommentar des Skripts. Der
Compiler sieht danach den zusammengesetzten Text - eine Funktion aus der Lib lässt
sich also aufrufen wie eine im Skript selbst geschriebene.
 
===Bewährtes Muster: Lib als eigene Endpunkt-Zeile===
 
Am wenigsten Verwaltung macht es, die Lib '''als eigene Zeile in
RESTSRV_ENDPOINTS''' abzulegen:
 
# Endpunkt anlegen mit einem eigenen Pfad-Template, z.B. <code>/meinapp/lib</code>.
# Den Lib-Quelltext ins Skript-Feld dieser Zeile schreiben (F7).
# '''Keine Berechtigung''' in der Berechtigungs-Liste vergeben - das ist der Schutz: ohne Berechtigung beantwortet der Server einen Aufruf mit 403.
# In jedem nutzenden Endpunkt die Direktive oben einfügen.
 
Damit ist die Lib reiner Ablageort und von aussen nicht nutzbar. Eine Änderung
wirkt auf alle einbindenden Endpunkte, ohne dass ein einziges Endpunkt-Skript
angefasst werden muss.
 
{{Hinweis|'''Lib und Skripte gemeinsam einspielen.''' Kommt in der Lib eine neue
Funktion hinzu, genügt es nicht, nur das nutzende Skript zu aktualisieren. Eine
ältere Lib lädt fehlerfrei - der Compiler meldet dann ausschliesslich die neuen
Namen als ''Unknown name'', was leicht wie ein Fehler im Skript aussieht.}}
 
{{Hinweis|Auch hier greift der Endpunkt-Cache: eine Änderung an der Lib wird erst
nach Ablauf des TTL (bis zu 60 Sekunden) wirksam.}}
 
==Fallstricke im Skript==
 
Die folgenden Punkte unterscheiden das Skript von normalem Delphi-Code und kosten
sonst eine Runde Fehlersuche:
 
{| class="wikitable"
! Stolperstein !! Richtig
|-
| ''DB_SOpen('Y00…', oDB, …)'' - der AD-UID-Parameter aus dem Delphi-Code || Im Skript '''ohne UID''': <code>DB_SOpen(oDB, cSql, q)</code>. Gleiches gilt für <code>qSqlInit(oDB, 'tab')</code> und <code>qSqlRead(oDB, 'tab', cWhere)</code> - jede dieser Funktionen beginnt mit ''oDB''
|-
| ''oBody.TryGetValue&lt;T&gt;(…)'' || Generics gibt es nicht - über ''GetValue'' lesen (siehe oBody)
|-
| ''EMPTY_DATE'' für ein leeres Datum || ''EMPTY_DATE'' ist im Skript ein '''String'''. Für Datumsvariablen und -vergleiche '''MINDATETIME''' verwenden
|-
| ''fVal'' auf einen JSON-Zahlenwert || JSON liefert den Dezimalpunkt, ''fVal'' erwartet die lokale Notation: vorher <code>StrTran(cVal, '.', ',')</code>
|-
| ''TStringList'' für Zwischenlisten || In Skripten unzuverlässig. Kleine Mengen über einen Delimiter-String führen (<code>'&#124;' + wert + '&#124;'</code>) und mit ''Pos'' prüfen
|-
| Default-Parameter in eigenen Funktionen || Werden nicht unterstützt - alle Parameter ausschreiben
|}
 
==Skript-Cache==
 
Der Server cached das kompilierte Skript pro Endpunkt (Schlüssel: ''sys_date'' des Endpunkts). Zusätzlich werden die Endpunkt-Definitionen pro Server-Profil in einem TTL-Cache (Standard 60 s) gehalten. Eine Skript-Änderung über F7 wird daher erst nach Ablauf dieses TTL (bzw. nach einer Cache-Invalidierung) wirksam - typischerweise innerhalb einer Minute, nicht zwingend sofort.
 
==Verfügbare Bibliotheken==
 
Im Skript können alle OBS-Standard-Bibliotheken verwendet werden. Typische Einstiegspunkte:
 
* ''Base.Tools'' - String-, Datum-, IIF-, Empty-Helper
* ''Base.DB'' / ''Base.xQuery'' - Datenbank-Operationen
* ''Base.qSqlReg'' - Schreibzugriffe
* ''System.JSON'' - JSON-Objekte und -Arrays
* ''Base.ToolsConst'' - Konstanten wie ''CRLF'', ''SINGELQUOTE'', ''MINDATETIME''
 
Die wichtigsten Aufrufe mit ihren Skript-Signaturen:
 
{| class="wikitable"
! Zweck !! Aufruf
|-
| Lesen || <code>DB_SOpen(oDB, cSql, q)</code>, dann <code>q.EoF</code>, <code>q.Next()</code>, <code>q.A2C('feld')</code> / <code>A2I</code> / <code>A2F</code> / <code>A2D</code> / <code>A2UID()</code>, am Ende <code>DB_Close(q)</code>
|-
| Neu anlegen || <code>q := qSqlInit(oDB, 'tab')</code>, <code>q.qSet('feld', wert)</code>, <code>q.SaveData(NEW_RECORD)</code>, <code>qSqlFree(q)</code>
|-
| Ändern || <code>q := qSqlRead(oDB, 'tab', cWhere)</code>, <code>q.qSet(…)</code>, <code>q.SaveData(UPDATE_RECORD)</code>, <code>qSqlFree(q)</code>
|-
| Direkt ausführen || <code>DB_SqlExec(oDB, cSql)</code>
|-
| Existenzprüfung || <code>DB_LSeek(oDB, 'tab', cWhere)</code>
|-
| Werte quoten (Pflicht) || <code>DB_SQLVal(wert)</code>
|}
 
{{Hinweis|Alle diese Funktionen beginnen im Skript mit ''oDB''. Ein zusätzlicher
AD-UID-Parameter als erstes Argument gehört zur Delphi-Variante und lässt sich im
Skript nicht übersetzen.}}
 
Konkrete Beispiele:
 
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel1|Beispiel 1: Daten-Abruf mit JWT]]
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel 2: Zeiterfassung als komplette Webanwendung]]
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel3|Beispiel 3: Datensatz anlegen mit JSON-Body (CRUD)]]
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4: Pfad-Parameter im Routing]]
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz]]
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6: Datei-Upload und Ablage im DMS]]

Aktuelle Version vom 19. August 2026, 13:00 Uhr

Kostenpflichtige Module

Internet-Shop
UPS
IMS Professional
SMS
Mehrlager-Verwaltung
Mehrsprachen Modul
Multilanguage Modul
EVA Marketing Tool
Termin-Projekte
Edifact-Schnittstelle
Backup Überwachung Email
OBS Geo Daten
DeliSprint / DPD
Filialen
Cashback
Moebelschnittstelle
Dokumenten Manager
DocuWare-Schnittstelle
OFML-Kalkulation
Versicherungsschaden
Gutschriftsanzeigen
Kameraverwaltung
DataInOut
OpenMasterData / IDS
Sammelpositionen



Anleitung für Endpunkt-Skripte

Jede Anfrage an einen Endpunkt wird nach erfolgreicher Authentifizierung und Autorisierung an das hinterlegte Skript weitergegeben. Das Skript ist in Object Pascal geschrieben und greift auf die OBS-Bibliothek (DB-Zugriff, Hilfsfunktionen) zu.

Sicherheitshinweise

HINWEIS: Endpunkt-Skripte verarbeiten Daten aus dem Internet. Übergabeparameter dürfen niemals ungeprüft in SQL-Anweisungen eingebaut werden. Werte immer über DB_SQLVal bzw. Parameter-Bindings absichern, Eingabewerte gegen Whitelists prüfen.
  • Eingaben gegen erwartete Werte prüfen (z.B. Pflichtfelder, erlaubte Typen, Wertebereiche).
  • Nur das zurückgeben, was der Konsument wirklich braucht - keine internen IDs, keine Sys-Felder, keine Passwörter.
  • Bei Fehlern keine internen Details an den Konsumenten zurückgeben; stattdessen schreibt der Server ohnehin Detail-Einträge in RESTSRV_PROTO.

Methoden-Signatur

Pro HTTP-Methode wird im Skript eine gleichnamige Funktion implementiert. Der Server ruft genau die Funktion auf, die zur Methode des eingehenden Requests passt.

function Get   (oParams: TStrings; oBody: TJSONObject): string;
function Post  (oParams: TStrings; oBody: TJSONObject): string;
function Put   (oParams: TStrings; oBody: TJSONObject): string;
function Delete(oParams: TStrings; oBody: TJSONObject): string;
function Patch (oParams: TStrings; oBody: TJSONObject): string;

Nicht implementierte Methoden liefern automatisch 405-ähnliche Fehler über die generische Skript-Antwort.

Parameter

oParams (TStrings)

Enthält alle Query-Parameter, POST-Parameter (form-urlencoded) sowie die durchgereichten HTTP-Header. Werte sind immer Strings.

Filterung durch den Server:

  • Geblockte Header werden nicht durchgereicht: authorization, cookie, proxy-authorization, x-forwarded-for, x-real-ip, apikey, api_key.
  • Parameter mit Präfix _OBS_ können von aussen nicht gesetzt werden; sie sind für interne Werte reserviert.
  • Werte werden auf max. 1024 Zeichen begrenzt.
  • Null-Bytes und Steuerzeichen (ausser Tab, CR, LF) werden entfernt.

Zugriff im Skript:

if (not Empty(oParams.Values['kundennr'])) then begin
    cKundenNr := oParams.Values['kundennr'];
end;

oBody (TJSONObject)

Enthält den Request-Body, sofern dieser ein gültiges JSON-Objekt ist. Bei leerem oder nicht-JSON-Body wird nil übergeben.

HINWEIS: Generics funktionieren im Skript nicht. Aus Delphi bekannte

Schreibweisen wie oBody.TryGetValue<string>('feld', cVar) lassen sich nicht

übersetzen. Gelesen wird über GetValue.

GetValue liefert nil, wenn das Feld im Body nicht enthalten ist - daran lässt sich „nicht gesendet" von „leer gesendet" unterscheiden. Der Wert ist immer ein String und wird bei Bedarf selbst gewandelt.

cUser := ;
cPass := ;
if (Assigned(oBody)) then begin
    oVal := oBody.GetValue('username');
    if (Assigned(oVal)) then begin
        cUser := oVal.Value;
    end;
    oVal := oBody.GetValue('password');
    if (Assigned(oVal)) then begin
        cPass := oVal.Value;
    end;
end;

Bei mehr als zwei Feldern lohnt eine kleine Hilfsfunktion, die das Muster einmal kapselt:

function _BodyStr(oBody: TJSONObject; const cFeld: string): string;
var oVal: TJSONValue;
begin
    result := '';
    if (oBody = nil) then begin
        exit;
    end;
    oVal := oBody.GetValue(cFeld);
    if (Assigned(oVal)) then begin
        result := oVal.Value;
    end;
end;

// Aufruf:
cBetreff := _BodyStr(oBody, 'betreff');

Andere Typen entstehen aus dem String:

Zieltyp Umwandlung
Ganzzahl iVal(_BodyStr(oBody, 'menge'))
Kommazahl fVal(StrTran(_BodyStr(oBody, 'preis'), '.', ',')) - JSON liefert den Dezimalpunkt, fVal erwartet die lokale Notation
Boolean Lower(_BodyStr(oBody, 'aktiv')) = 'true'
Datum/Zeit ISO-8601-String selbst zerlegen (CToDT erwartet das deutsche Format)

Maximale Body-Grösse: 10 MB.

Reservierte _OBS_-Parameter

Bei aktiver JWT-Authentifizierung stehen die Token-Claims als Parameter zur Verfügung:

Parameter Inhalt
_OBS_JWT_ID JWT-Id (jti-Claim) - typisch die User-Id. Muss nicht eindeutig sein: die Sitzung führt der Server über eigene Kennungen (siehe unten)
_OBS_JWT_SUBJECT Subject (sub-Claim) - typisch Benutzername
_OBS_JWT_AUDIENCE Audience (aud-Claim) - typisch Mandant / Rolle
_OBS_JWT_CLAIM_<name> Beliebiger Custom-Claim des Tokens (z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles); im Folge-Skript lesbar
_OBS_TRACE_ID Korrelations-ID des Requests (auch als Response-Header X-Trace-Id)

Unabhängig von JWT stehen ausserdem immer zur Verfügung:

Parameter Inhalt
_OBS_SERVER_TIME Aktuelle Serverzeit als ISO-8601 mit UTC-Offset (z.B. 2026-08-17T09:12:33+02:00). Nützlich, um Datumswerte in derselben Zeitzone auszuliefern, in der der Server arbeitet, und damit ein Client seinen Uhren-Versatz bestimmen kann
_OBS_JWT_EXP_SEC Laufzeit des Access-Tokens in Sekunden (JWT-Exp × 60). Damit kann ein Konfigurations-Endpunkt denselben Wert ausliefern, den der Token wirklich hat

Diese Werte werden vom Server gesetzt und können vom Skript für Berechtigungs- und Mandantenprüfungen verwendet werden.

Zusätzlich trägt jedes Token zwei Sitzungskennungen des Servers, lesbar als _OBS_JWT_CLAIM_sid und _OBS_JWT_CLAIM_rid:

Claim Inhalt
sid Sitzung. Bleibt über alle Erneuerungen (Refresh) hinweg gleich und ist der Schlüssel für das Abmelden
rid Zeilenkennung des einzelnen Token-Paares in RESTSRV_TOKEN. Bei jeder Erneuerung neu

Beide sind OBS-UIDs (10 Zeichen) und werden vom Server nach den Claims des Skripts gesetzt - ein Skript kann sie also auch mit _OBS_JWT_CLAIM_sid nicht überschreiben. Für Fachlogik sind sie nicht gedacht; sie sind nützlich, um eine Sitzung im Protokoll wiederzufinden.

Pfad-Parameter

Stammt der Endpunkt aus einem Pfad-Template mit Platzhaltern (siehe Endpunkte), stehen die aus den Platzhaltern erfassten Werte als reservierte Parameter mit Präfix _OBS_PATH_ in oParams bereit:

Template Zugriff im Skript
/orders/{uid} oParams.Values['_OBS_PATH_uid']
/orders/{uid}/modules/{code} oParams.Values['_OBS_PATH_uid'], oParams.Values['_OBS_PATH_code']

Da der Präfix _OBS_ für von aussen gelieferte Header- und Query-Parameter gesperrt ist, sind diese Werte nicht durch den Client fälschbar. Ein vollständiges Beispiel zeigt Beispiel 4 - Pfad-Parameter.

Datei-Uploads

Ist der Endpunkt für Uploads freigeschaltet (re_upload = 1, siehe Endpunkte), nimmt der Server hochgeladene Dateien entgegen, legt sie in einem temporären Verzeichnis ab und übergibt dem Skript Pfad, Originalname und Content-Type über reservierte Parameter. Das Skript entscheidet selbst über die weitere Verarbeitung (z.B. DMS-Ablage). Der Body wird in diesem Fall nicht als JSON geparst - oBody ist nil. Es gilt nicht das JSON-Body-Limit (10 MB), sondern die pro Endpunkt konfigurierte Grösse (re_upload_size, Standard 25 MB; Überschreitung -> 413).

Beide Übertragungsarten - multipart/form-data und der resumable Content-Range-Upload - liefern dem Skript dieselben Parameter:

Parameter Inhalt
_OBS_UPLOAD_PATH Vollständiger Pfad zur temporären Datei auf dem Server
_OBS_UPLOAD_NAME Originaldateiname
_OBS_UPLOAD_CONTENTTYPE Content-Type der Datei (Default application/octet-stream, wenn der Client keinen angibt)

Der Dateiname ist Pflicht. Fehlt er, lehnt der Server den Upload mit 400 Bad Request ab und das Skript wird nicht ausgeführt - das Skript kann sich also darauf verlassen, dass _OBS_UPLOAD_PATH und _OBS_UPLOAD_NAME gesetzt sind, sobald es läuft.

HINWEIS: Die temporäre Datei wird nicht automatisch verschoben. Das Skript muss sie an ihren Zielort (DMS, Verzeichnis, ...) übernehmen.

Einfacher Upload (multipart/form-data)

Eine Datei pro Request. Name und Content-Type stammen aus der Content-Disposition der Datei-Partie (filename=). Der Server speichert die erste Datei-Partie und ruft das Skript auf:

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
    cPfad: string;
    cName: string;
    cTyp : string;
begin
    oRes := TJSONObject.Create();
    try
        cPfad := oParams.Values['_OBS_UPLOAD_PATH'];
        cName := oParams.Values['_OBS_UPLOAD_NAME'];
        cTyp  := oParams.Values['_OBS_UPLOAD_CONTENTTYPE'];

        // ... Datei aus cPfad ins DMS / Zielverzeichnis uebernehmen ...

        oRes.AddPair('status'   , 'ok');
        oRes.AddPair('dateiname', cName);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Resumable/Chunked Upload (Content-Range)

Für grosse Dateien überträgt der Client die Datei in Teilstücken (Chunks) mit dem Header Content-Range: bytes START-END/TOTAL. Der Server hängt die Chunks sequentiell an eine Session-Datei an und behandelt unvollständige Uploads selbst - das Endpunkt-Skript läuft erst beim vollständigen Upload (dann mit gesetztem _OBS_UPLOAD_PATH, _OBS_UPLOAD_NAME und _OBS_UPLOAD_CONTENTTYPE).

Name und Content-Type sind im Content-Range-Protokoll nicht enthalten; der Client liefert sie deshalb im ersten Chunk über den Header Upload-Metadata (Format wie bei tus: kommagetrennte Paare schlüssel base64wert, Werte Base64-kodiert):

Upload-Metadata: filename ZmlsZS5wZGY=,filetype YXBwbGljYXRpb24vcGRm

Erkannt werden die Schlüssel filename (Pflicht) und filetype (optional). Fehlt filename im ersten Chunk, wird der Upload mit 400 abgelehnt, ohne dass eine Datei angelegt wird. Folge-Chunks und Statusabfragen müssen die Metadaten nicht erneut senden.

Ablauf (vom Client gesteuert, der Server antwortet jeweils):

Request Server-Antwort
Erster Chunk Content-Range: bytes 0-1048575/5000000 + Upload-Metadata 308 Resume Incomplete + Header Upload-Id, Upload-Offset, Range
weitere Chunks (jeweils Upload-Id mitsenden) 308 mit aktualisiertem Upload-Offset
letzter Chunk (Bereich erreicht TOTAL) normale Skript-Antwort (z.B. 200/201)
Statusabfrage Content-Range: bytes */5000000 aktueller Upload-Offset, ohne anzuhängen (Resume)
Header Richtung Bedeutung
Content-Range Request bytes START-END/TOTAL; bytes */TOTAL nur als Statusabfrage
Upload-Metadata Request tus-Metadaten im ersten Chunk: filename (Pflicht), filetype (optional), Base64-kodiert
Upload-Id Request/Response Session-Kennung. Fehlt sie im ersten Request, erzeugt der Server eine und gibt sie zurück; alle Folge-Chunks müssen sie mitsenden.
Upload-Offset Response Anzahl der bereits gespeicherten Bytes (= Startoffset des nächsten Chunks)
Range Response Bereits gespeicherter Bereich (bytes=0-N)

Der Server hängt einen Chunk nur an, wenn START dem bereits gespeicherten Stand entspricht. Bei Abweichung (doppelter Chunk, Lücke) wird nicht angehängt, sondern der aktuelle Upload-Offset zurückgemeldet - der Client setzt ab dort neu auf. Eine separate Statusabfrage ist für ein Resume daher nicht zwingend nötig.

Überschreitet ein Chunk bzw. die Gesamtgrösse das Limit (re_upload_size), antwortet der Server mit 413 Payload Too Large.

HINWEIS: Der Upload-Status liegt prozesslokal im Speicher. Nach einem Neustart des Servers muss ein unvollständiger Upload neu begonnen werden.

Rückgabe

Die Rückgabe ist immer ein JSON-String. Wird ein leerer String zurückgegeben, antwortet der Server automatisch mit {}. Der Server setzt Content-Type auf application/json; charset=utf-8 und Status auf 200, sofern das Skript nicht selbst einen Fehler signalisiert.

Einfaches Beispiel:

function Get(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        oRes.AddPair('wert_string', '123');
        oRes.AddPair('wert_int'   , 456);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Antwort steuern: Statuscode, Header, traceId

Ein Skript kann den HTTP-Statuscode und beliebige Response-Header über reservierte Felder in der Antwort setzen. Der Server wertet sie aus und entfernt sie vor dem Senden aus dem Body.

Antwort-Feld Wirkung
_OBS_HTTP_STATUS (Zahl) HTTP-Statuscode (z.B. 201, 204, 400, 409, 422). Ohne Angabe: 200. Bei 204 wird kein Body gesendet.
_OBS_HEADERS (Objekt) Beliebige Response-Header, z.B. ETag, Location, Retry-After. CR/LF in Namen/Werten werden entfernt (Schutz vor Header-Injection).
_OBS_JWT_REVOKE (Text) Sperrt die Sitzung des vorgelegten Tokens (session) oder alle Sitzungen des Subjects (all). Damit wird ein Endpunkt zum Logout, ohne eigene Verwaltung. Scheitert das Sperren, antwortet der Server mit 503 statt mit der Antwort des Skripts - siehe Zugänge, Abschnitt Abmelden.

Zusätzlich trägt jede Antwort den Header X-Trace-Id (Korrelations-ID). Dieselbe ID liegt dem Skript als oParams.Values['_OBS_TRACE_ID'] vor und erscheint in jeder Server-Fehler-Logzeile in RESTSRV_PROTO - so lässt sich ein Fehler ohne Gerätezugriff im Log wiederfinden.

Beispiel: Anlegen mit 201, Location und ETag

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        // ... Datensatz anlegen, neue UID + Version (ETag) ermitteln ...
        oHdr := TJSONObject.Create();
        oHdr.AddPair('Location', '/v1/orders/' + cNeueUid);
        oHdr.AddPair('ETag', '1');

        oRes.AddPair('_OBS_HTTP_STATUS', 201);
        oRes.AddPair('_OBS_HEADERS', oHdr);
        oRes.AddPair('uid', cNeueUid);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Beispiel: Strukturierter Fehler mit Statuscode und traceId

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        oErr := TJSONObject.Create();
        oErr.AddPair('code', 'VALIDATION_FAILED');
        oErr.AddPair('message', 'Feld "menge" fehlt');
        oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);

        oRes.AddPair('_OBS_HTTP_STATUS', 422);
        oRes.AddPair('error', oErr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Beispiel: Optimistic Concurrency (ETag / If-Match)

Mit Statuscode, Headern und dem lesbaren Header If-Match führt das Skript je Datensatz einen Versionszähler und lehnt veraltete Schreibzugriffe mit 409 ab:

function Put(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
    nAktuell, nIfMatch: integer;
begin
    oRes := TJSONObject.Create();
    try
        nAktuell := AuftragVersion(oParams.Values['_OBS_PATH_uid']);
        nIfMatch := iVal(oParams.Values['if-match']);

        if (nIfMatch <> nAktuell) then begin
            oHdr := TJSONObject.Create();
            oHdr.AddPair('ETag', xStr(nAktuell));
            oRes.AddPair('_OBS_HTTP_STATUS', 409);
            oRes.AddPair('_OBS_HEADERS', oHdr);
            oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'VERSION_CONFLICT'));
            result := oRes.ToJSON();
            exit;
        end;

        // ... speichern, Version hochzählen ...
        oHdr := TJSONObject.Create();
        oHdr.AddPair('ETag', xStr(nAktuell + 1));
        oRes.AddPair('_OBS_HEADERS', oHdr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Idempotenz - was das Skript beachten muss

Sendet ein Konsument bei POST/PUT/PATCH/DELETE den Header Idempotency-Key, fängt der Server doppelte Sendungen ab. Das Skript braucht dafür keine eigene Logik - keine Schlüssel-Tabelle, keine Prüfung am Anfang der Methode.

Was das Skript wissen muss:

Antwort des Skripts Wirkung auf den Idempotenz-Speicher
2xx Statuscode, Body und Header werden gespeichert. Eine Wiederholung mit demselben Schlüssel bekommt genau diese Antwort zurück, das Skript läuft nicht erneut
4xx Der Schlüssel wird freigegeben. Der Konsument darf denselben Schlüssel nach Korrektur des Inhalts erneut verwenden
5xx oder Exception Der Schlüssel bleibt belegt, der Fall wird protokolliert. Eine Wiederholung wird abgelehnt, bis der Fall geklärt ist

Daraus folgen zwei Regeln für Endpunkt-Skripte:

  • Fachliche Ablehnungen als 4xx melden (422, 409, 404), nicht als 2xx mit Fehlertext im Body. Sonst wird die Ablehnung gespeichert und der Konsument bekommt sie bei jedem weiteren Versuch erneut - auch nachdem er den Fehler behoben hat.
  • Die Antwort muss vollständig sein. Was zurückgegeben wird, wird eingefroren. Ein Feld, das erst der zweite Aufruf ergänzen würde, kommt beim Konsumenten nie an.
HINWEIS: Vor dieser Server-Funktion mussten Skripte die Idempotenz selbst

abbilden. Solche Skripte laufen unverändert weiter - die Doppelprüfung schadet

nicht, ist aber überflüssig und kann beim nächsten Überarbeiten entfallen.

Der vollständige Ablauf und die Statuscodes stehen unter Endpunkte.

JWT-Authentifizierungs-Skript

Bei Zugängen mit aktiver JWT-Pflicht wird über den JWT-Endpunkt das im Zugang hinterlegte Authentifizierungs-Skript aufgerufen (F7 in der Zugänge-Liste). Das Skript muss eine Methode Authenticate bereitstellen:

function Authenticate(oParams: TStrings; oBody: TJSONObject): string;
var oRes  : TJSONObject;
    oVal  : TJSONValue;
    cUser : string;
    cPass : string;
begin
    oRes := TJSONObject.Create();
    try
        // Eingangsdaten lesen (Body oder Query-Param)
        cUser := '';
        cPass := '';
        if (Assigned(oBody)) then begin
            oVal := oBody.GetValue('username');
            if (Assigned(oVal)) then begin
                cUser := oVal.Value;
            end;
            oVal := oBody.GetValue('password');
            if (Assigned(oVal)) then begin
                cPass := oVal.Value;
            end;
        end;

        // Prüfung gegen eigene Tabelle, Hash-Verfahren, LDAP, ...
        if (PasswortPasst(cUser, cPass)) then begin
            oRes.AddPair('status'          , 1);
            oRes.AddPair('_OBS_JWT_ID'     , cUser);
            oRes.AddPair('_OBS_JWT_SUBJECT', cUser);
            oRes.AddPair('_OBS_JWT_AUDIENCE', 'mandant1');
        end else begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'Login fehlgeschlagen');
        end;

        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Rückgabewerte:

status Bedeutung
1 Erfolgreich, der Server erzeugt aus den _OBS_JWT_*-Werten einen Token (HS256)
9 Misserfolg, der Server antwortet mit 401 und reicht den Wert von error als error.message an den Client durch (zusätzlich protokolliert)
HINWEIS: {{{1}}}

Liefert das Skript einen anderen Status als 1 oder 9, ist gar nicht lauffähig oder gibt kein auswertbares JSON zurück, antwortet der Server mit 403 und einer generischen Meldung - ein defektes Anmeldeskript soll dem Anwender nicht als „Passwort falsch" erscheinen.

Kann der Server die Sitzungszeile zum ausgestellten Token nicht schreiben (Tabelle fehlt, Datenbankproblem), liefert er kein Token aus und antwortet mit 503 und Retry-After. Auch das ist bewusst kein 401/403: die Zugangsdaten waren richtig, der Versuch darf wiederholt werden.

Aufbau der Token-Antwort

Bei Erfolg baut der Server die Antwort aus den Token-Feldern und allen weiteren Feldern, die das Skript zurückgibt:

{"benutzerId": "4711", "tenant": "nord", "roles": ["TECHNIKER"],
 "token":       "eyJ...",
 "accessToken": "eyJ...",
 "refreshToken":"eyJ...",
 "expiresIn":   28800,
 "serverTime":  "2026-08-17T09:12:33+02:00"}
Feld Herkunft
token / accessToken Derselbe Access-Token unter zwei Namen. accessToken erwarten die meisten Client-Bibliotheken, token bleibt für bestehende Konsumenten erhalten
refreshToken Nur wenn das Skript _OBS_JWT_REFRESH_ID geliefert hat
expiresIn Laufzeit des Access-Tokens in Sekunden (JWT-Exp × 60)
serverTime Serverzeit ISO-8601 mit Offset
alle übrigen Felder Frei vom Skript bestimmt. Übernommen wird alles ausser status und den _OBS_-Steuerfeldern

Damit kann das Anmeldeskript alles mitliefern, was der Client direkt nach dem Login braucht (Benutzer-Id, Mandant, Rollen, Berechtigungen) - ohne dass dieser dafür einen zweiten Aufruf absetzen muss. Ein Feld, das genauso heisst wie eines der Token-Felder oben, wird verworfen; die Engine setzt diese selbst.

Custom-Claims, serverTime und Refresh

Das Authenticate-Skript kann dem Token zusätzlich beliebige Custom-Claims mitgeben - z.B. Mandant und Rollen - und optional ein Refresh-Token anstoßen:

RÜckgabefeld Bedeutung
_OBS_JWT_CLAIM_<name> Beliebiger Custom-Claim, z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles. Im Folge-Skript lesbar als oParams.Values['_OBS_JWT_CLAIM_<name>'].
_OBS_JWT_REFRESH_ID (optional) jti des Refresh-Tokens. Nur wenn gesetzt, stellt der Server ein Refresh-Token aus.
_OBS_JWT_REFRESH_EXP (optional) Lebensdauer des Refresh-Tokens in Minuten (Default 90 Tage = 129600).

Jedes Token trägt automatisch den Claim token_use (access bzw. refresh) sowie die Sitzungskennungen sid und rid des Servers. Die Antwort enthält bei Erfolg serverTime (ISO-8601 mit Offset), bei ausgestelltem Refresh zusätzlich refreshToken:

{"token":"...", "refreshToken":"...", "serverTime":"2026-06-29T15:30:12+02:00"}
HINWEIS: Mandant und Rollen immer aus dem Token lesen (_OBS_JWT_CLAIM_*), nie aus Body oder Query.

Refresh-Methode

Der Refresh läuft über denselben JWT-Endpunkt und dasselbe Skript. Liegt am JWT-Endpunkt ein Authorization: Bearer <token> mit einem Refresh-Token vor, ruft der Server statt Authenticate die Methode Refresh auf (ohne Bearer: Login; DELETE mit Access-Token: Abmelden, ohne Skript).

Bevor das Skript läuft, hat der Server das vorgelegte Refresh-Token verifiziert (Signatur, Ablauf, token_use=refresh) und in der Sitzung entwertet. Einmalgebrauch, Rotation und Sperrliste sind damit Sache des Servers. Das Skript liefert nur noch die aktuellen Claims - genau das ist der Sinn des Aufrufs: eine zwischenzeitlich geänderte Rolle oder ein gewechselter Mandant wirken spätestens mit der nächsten Erneuerung.

ACHTUNG: Eine eigene Sperrtabelle im Skript ist nicht mehr nötig und sollte

entfernt werden. Wer sie weiterführt, rotiert zweimal - einmal im Skript, einmal im Server - und riskiert, dass beide Seiten unterschiedlicher Meinung sind. Der

Server sieht die Skript-Tabelle nicht, und das Skript sieht die Sitzung nicht.

Ein bereits benutztes Refresh-Token wird mit 401 abgelehnt. Erfolgt die erneute Vorlage innerhalb von 60 Sekunden, bleibt die Sitzung bestehen (paralleler Refresh einer App); danach gilt sie als Wiedervorlage und die gesamte Sitzung wird gesperrt. Details in Zugänge.

Das alte Refresh-jti steht weiterhin als oParams.Values['_OBS_JWT_ID'] bereit, falls das Skript es für eigene Protokollierung braucht.

function Refresh(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
    cUser: string;
begin
    oRes := TJSONObject.Create();
    try
        cUser := oParams.Values['_OBS_JWT_SUBJECT'];

        // Der Server hat das vorgelegte Refresh-Token bereits geprueft und
        // entwertet. Hier wird nur entschieden, ob der Benutzer die Sitzung
        // fortsetzen darf - und mit welchen Rechten.
        if (not BenutzerAktiv(cUser)) then begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'Konto ist nicht mehr aktiv, bitte neu anmelden');
            result := oRes.ToJSON();
            exit;
        end;

        // Rolle und Mandant neu lesen, nicht aus dem alten Token uebernehmen -
        // sonst wirkt ein Rechteentzug erst beim naechsten Login.
        oRes.AddPair('status'               , 1);
        oRes.AddPair('_OBS_JWT_ID'          , cUser);
        oRes.AddPair('_OBS_JWT_SUBJECT'     , cUser);
        oRes.AddPair('_OBS_JWT_CLAIM_tenant', TechnikerMandant(cUser));
        oRes.AddPair('_OBS_JWT_CLAIM_roles' , TechnikerRollen(cUser));
        oRes.AddPair('_OBS_JWT_REFRESH_ID'  , GlobalUID());   // loest das neue Refresh-Token aus
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Abmelden (Logout)

Für das Abmelden braucht das Skript keine Methode. Es läuft über ein DELETE auf denselben JWT-Endpunkt (Access-Token im Authorization-Header) und wird komplett in der Engine erledigt: die Sitzung wird gesperrt, die Antwort ist 204. Eine Methode Logout gibt es nicht und wird nicht aufgerufen. Details: Zugänge, Phase 4.

Wer beim Abmelden zusätzlich fachlich etwas tun will - eine Geräteregistrierung löschen, einen Push-Token verwerfen, den Vorgang protokollieren - nimmt einen gewöhnlichen Endpunkt und setzt dort _OBS_JWT_REVOKE. Dasselbe gilt für "auf allen Geräten abmelden", weil das eine Berechtigungsentscheidung braucht:

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        GeraeteRegistrierungLoeschen(oParams.Values['_OBS_JWT_SUBJECT']);

        // 'session' = diese Sitzung, 'all' = alle Sitzungen des Subjects
        oRes.AddPair('_OBS_JWT_REVOKE' , 'session');
        oRes.AddPair('_OBS_HTTP_STATUS', 204);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Gesperrt wird in beiden Wegen die ganze Sitzung, also auch die noch nicht abgelaufenen Access-Token vorheriger Erneuerungen. Ein wiederholter Aufruf ist unschädlich. Lässt sich die Sitzung nicht sperren, überschreibt der Server die Antwort des Skripts mit 503 - ein Client darf nicht glauben, er sei abgemeldet, während sein Token weiterläuft.

Fehlerbehandlung im Skript

Tritt im Skript eine Exception auf oder schlägt die Syntax-Prüfung fehl, antwortet der Server mit 500 Interner Fehler und protokolliert die Detail-Meldung in RESTSRV_PROTO (mit Skript-Fehlertext). Der Konsument sieht keine internen Details.

Server-eigene Fehler (Routing, Rate-Limit, Token, Auffangnetz) haben ein einheitliches Format mit einem error-Objekt - Aufbau siehe Übersicht.

Will das Skript einen spezifischen Fehler an den Konsumenten zurückgeben, sollte es dasselbe Format und einen passenden Statuscode verwenden:

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        if (Empty(oParams.Values['kundennr'])) then begin
            oErr := TJSONObject.Create();
            oErr.AddPair('code'   , 'VALIDATION_FAILED');
            oErr.AddPair('message', 'Parameter ''kundennr'' fehlt');
            oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);

            oRes.AddPair('_OBS_HTTP_STATUS', 422);
            oRes.AddPair('error', oErr);
            result := oRes.ToJSON();
            exit;
        end;
        // ...
    finally
        MyFreeAndNil(oRes);
    end;
end;
HINWEIS: Ein fachlicher Fehler gehört auf einen 4xx-Statuscode, nicht auf

200 mit Fehlertext im Body. Nur so erkennt der Konsument den Fehlschlag ohne den Body auszuwerten - und nur so gibt der Server einen belegten

Idempotency-Key wieder frei (siehe Abschnitt Idempotenz).

Gängige Codes, die auch der Server selbst verwendet: VALIDATION_FAILED (422), NOT_FOUND (404), FORBIDDEN_ROLE (403), VERSION_CONFLICT (409). Eigene, fachlich sprechende Codes sind erlaubt - wichtig ist, dass sie stabil bleiben, weil Konsumenten darauf ihre Reaktion aufbauen.

Gemeinsamen Code auslagern

Wird Logik in mehreren Endpunkten gebraucht - Berechtigungsprüfungen, JSON-Bausteine, Hilfsfunktionen -, muss sie nicht in jedes Skript kopiert werden. Die Skript-Engine lädt Textbausteine beim Laden aus einer beliebigen Tabelle nach:

{$L T="restsrv_endpoints" I="re_pathtemplate" V="/meinapp/lib" F="re_script"}
Attribut Bedeutung
T Tabelle, aus der geladen wird
I Spalte, über die gesucht wird
V Wert, der in dieser Spalte stehen muss
F Spalte, deren Inhalt an dieser Stelle eingesetzt wird

Die Direktive steht üblicherweise direkt unter dem Kopfkommentar des Skripts. Der Compiler sieht danach den zusammengesetzten Text - eine Funktion aus der Lib lässt sich also aufrufen wie eine im Skript selbst geschriebene.

Bewährtes Muster: Lib als eigene Endpunkt-Zeile

Am wenigsten Verwaltung macht es, die Lib als eigene Zeile in RESTSRV_ENDPOINTS abzulegen:

  1. Endpunkt anlegen mit einem eigenen Pfad-Template, z.B. /meinapp/lib.
  2. Den Lib-Quelltext ins Skript-Feld dieser Zeile schreiben (F7).
  3. Keine Berechtigung in der Berechtigungs-Liste vergeben - das ist der Schutz: ohne Berechtigung beantwortet der Server einen Aufruf mit 403.
  4. In jedem nutzenden Endpunkt die Direktive oben einfügen.

Damit ist die Lib reiner Ablageort und von aussen nicht nutzbar. Eine Änderung wirkt auf alle einbindenden Endpunkte, ohne dass ein einziges Endpunkt-Skript angefasst werden muss.

HINWEIS: Lib und Skripte gemeinsam einspielen. Kommt in der Lib eine neue

Funktion hinzu, genügt es nicht, nur das nutzende Skript zu aktualisieren. Eine ältere Lib lädt fehlerfrei - der Compiler meldet dann ausschliesslich die neuen

Namen als Unknown name, was leicht wie ein Fehler im Skript aussieht.
HINWEIS: Auch hier greift der Endpunkt-Cache: eine Änderung an der Lib wird erst nach Ablauf des TTL (bis zu 60 Sekunden) wirksam.

Fallstricke im Skript

Die folgenden Punkte unterscheiden das Skript von normalem Delphi-Code und kosten sonst eine Runde Fehlersuche:

Stolperstein Richtig
DB_SOpen('Y00…', oDB, …) - der AD-UID-Parameter aus dem Delphi-Code Im Skript ohne UID: DB_SOpen(oDB, cSql, q). Gleiches gilt für qSqlInit(oDB, 'tab') und qSqlRead(oDB, 'tab', cWhere) - jede dieser Funktionen beginnt mit oDB
oBody.TryGetValue<T>(…) Generics gibt es nicht - über GetValue lesen (siehe oBody)
EMPTY_DATE für ein leeres Datum EMPTY_DATE ist im Skript ein String. Für Datumsvariablen und -vergleiche MINDATETIME verwenden
fVal auf einen JSON-Zahlenwert JSON liefert den Dezimalpunkt, fVal erwartet die lokale Notation: vorher StrTran(cVal, '.', ',')
TStringList für Zwischenlisten In Skripten unzuverlässig. Kleine Mengen über einen Delimiter-String führen ('|' + wert + '|') und mit Pos prüfen
Default-Parameter in eigenen Funktionen Werden nicht unterstützt - alle Parameter ausschreiben

Skript-Cache

Der Server cached das kompilierte Skript pro Endpunkt (Schlüssel: sys_date des Endpunkts). Zusätzlich werden die Endpunkt-Definitionen pro Server-Profil in einem TTL-Cache (Standard 60 s) gehalten. Eine Skript-Änderung über F7 wird daher erst nach Ablauf dieses TTL (bzw. nach einer Cache-Invalidierung) wirksam - typischerweise innerhalb einer Minute, nicht zwingend sofort.

Verfügbare Bibliotheken

Im Skript können alle OBS-Standard-Bibliotheken verwendet werden. Typische Einstiegspunkte:

  • Base.Tools - String-, Datum-, IIF-, Empty-Helper
  • Base.DB / Base.xQuery - Datenbank-Operationen
  • Base.qSqlReg - Schreibzugriffe
  • System.JSON - JSON-Objekte und -Arrays
  • Base.ToolsConst - Konstanten wie CRLF, SINGELQUOTE, MINDATETIME

Die wichtigsten Aufrufe mit ihren Skript-Signaturen:

Zweck Aufruf
Lesen DB_SOpen(oDB, cSql, q), dann q.EoF, q.Next(), q.A2C('feld') / A2I / A2F / A2D / A2UID(), am Ende DB_Close(q)
Neu anlegen q := qSqlInit(oDB, 'tab'), q.qSet('feld', wert), q.SaveData(NEW_RECORD), qSqlFree(q)
Ändern q := qSqlRead(oDB, 'tab', cWhere), q.qSet(…), q.SaveData(UPDATE_RECORD), qSqlFree(q)
Direkt ausführen DB_SqlExec(oDB, cSql)
Existenzprüfung DB_LSeek(oDB, 'tab', cWhere)
Werte quoten (Pflicht) DB_SQLVal(wert)
HINWEIS: Alle diese Funktionen beginnen im Skript mit oDB. Ein zusätzlicher

AD-UID-Parameter als erstes Argument gehört zur Delphi-Variante und lässt sich im

Skript nicht übersetzen.

Konkrete Beispiele: