OBS/Kostenpflichtige Module/RESTServer/Beispiel5
- A Preise aktualisieren
- C Personen übertragen
- E Kategorien verwalten
- G Kataloge verwalten
- I Merkliste übertragen
- K Varianten übertragen
- L Artikelvarianten übertragen
- M Referenzarten übertragen
- N Lagerbestände verwalten
- U Bestellungen einlesen
- V leere Passworte füllen
- W Update-Informationen zurücksetzen
- X Konfiguration
- Z Protokoll
Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz
Dieses Beispiel zeigt einen schreibenden Endpunkt fÜr eine mobile App: AuftrÄge werden mit echten HTTP-Statuscodes aktualisiert, konkurrierende Änderungen Über ETag/If-Match abgesichert (Optimistic Concurrency) und doppelte Sendungen Über einen Idempotency-Key abgefangen. Die Anmeldung nutzt JWT mit Custom-Claims (Mandant, Rollen) und einem Refresh-Token.
Einrichtung in OBS
- Server-Profil: Standard-TLS-Profil Public-API (Port 443)
- Zugang: Mobile-App
- API-Key: zufÄllig generiert
- JWT aktiv, JWT-Endpunkt auth, JWT-Key zufÄllig, JWT-Exp 60 (Minuten)
- Endpunkte (beide dem Profil Public-API zugeordnet):
- orders/{uid} - Auftrag lesen/Ändern
- orders/{uid}/material - Material erfassen
- Berechtigung: Zugang Mobile-App fÜr beide Endpunkte freigeschaltet
JWT-Authentifizierungs-Skript (Zugang)
Stellt Mandant und Rollen als Custom-Claims aus und legt ein Refresh-Token an (das jti wird in einer eigenen Sperrtabelle abgelegt). Bei einem Refresh ruft der Server im selben Skript die Methode Refresh auf. Die Hilfsfunktionen (PasswortPasst, RefreshJti...) sind illustrativ und projektabhÄngig.
function Authenticate(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
oVal : TJSONValue;
cUser, cPass, cRefreshJti: string;
begin
oRes := TJSONObject.Create();
try
cUser := ''; cPass := '';
if (Assigned(oBody)) then begin
oVal := oBody.GetValue('username'); if (Assigned(oVal)) then cUser := oVal.Value;
oVal := oBody.GetValue('password'); if (Assigned(oVal)) then cPass := oVal.Value;
end;
if (PasswortPasst(cUser, cPass)) then begin
cRefreshJti := GlobalUID();
RefreshJtiAblegen(cUser, cRefreshJti); // eigene Sperrtabelle
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)); // z.B. "tech,lead"
oRes.AddPair('_OBS_JWT_REFRESH_ID' , cRefreshJti);
// _OBS_JWT_REFRESH_EXP weggelassen -> Default 90 Tage
end else begin
oRes.AddPair('status', 9);
oRes.AddPair('error' , 'Login fehlgeschlagen');
end;
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
//------------------------------------------------------------------------------
function Refresh(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
cAltesJti, cNeuesJti, cUser: string;
begin
oRes := TJSONObject.Create();
try
cAltesJti := oParams.Values['_OBS_JWT_ID']; // altes Refresh-jti
cUser := oParams.Values['_OBS_JWT_SUBJECT'];
if (not RefreshJtiGueltig(cAltesJti)) then begin
oRes.AddPair('status', 9);
oRes.AddPair('error' , 'AUTH_EXPIRED');
result := oRes.ToJSON();
exit;
end;
cNeuesJti := GlobalUID();
RefreshJtiRotieren(cAltesJti, cNeuesJti); // altes sperren, neues ablegen
oRes.AddPair('status' , 1);
oRes.AddPair('_OBS_JWT_ID' , cUser);
oRes.AddPair('_OBS_JWT_SUBJECT' , cUser);
oRes.AddPair('_OBS_JWT_CLAIM_tenant', oParams.Values['_OBS_JWT_CLAIM_tenant']);
oRes.AddPair('_OBS_JWT_CLAIM_roles' , oParams.Values['_OBS_JWT_CLAIM_roles']);
oRes.AddPair('_OBS_JWT_REFRESH_ID' , cNeuesJti);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Endpunkt orders/{uid} - ändern mit ETag / If-Match
GET liefert den Auftrag samt ETag (Version), PUT prÜft Rolle und If-Match.
function Get(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
cUid: string;
nVer: integer;
begin
oRes := TJSONObject.Create();
try
cUid := oParams.Values['_OBS_PATH_uid'];
if (not AuftragLesen(oParams.Values['_OBS_JWT_CLAIM_tenant'], cUid, oRes, nVer)) then begin
oRes.AddPair('_OBS_HTTP_STATUS', 404);
oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'NOT_FOUND'));
result := oRes.ToJSON();
exit;
end;
oHdr := TJSONObject.Create();
oHdr.AddPair('ETag', xStr(nVer));
oRes.AddPair('_OBS_HEADERS', oHdr);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
//------------------------------------------------------------------------------
function Put(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
cUid: string;
nAktuell, nIfMatch: integer;
begin
oRes := TJSONObject.Create();
try
// nur Rolle "lead" darf Ändern - Rolle kommt aus dem Token, nicht aus dem Body
if (Pos('lead', oParams.Values['_OBS_JWT_CLAIM_roles']) = 0) then begin
oRes.AddPair('_OBS_HTTP_STATUS', 403);
oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'FORBIDDEN_ROLE'));
result := oRes.ToJSON();
exit;
end;
cUid := oParams.Values['_OBS_PATH_uid'];
nAktuell := AuftragVersion(cUid);
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;
AuftragSpeichern(cUid, oBody); // setzt Version auf nAktuell + 1
oHdr := TJSONObject.Create();
oHdr.AddPair('ETag', xStr(nAktuell + 1));
oRes.AddPair('_OBS_HEADERS', oHdr);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Endpunkt orders/{uid}/material - idempotentes Anlegen
POST erfasst eine Materialposition: Validierungsfehler -> 422, erfolgreiches Anlegen -> 201. Ein wiederholter Aufruf mit gleichem Idempotency-Key liefert dieselbe Antwort ohne Zweitbuchung. Der Idempotenz-Eintrag wird in derselben Transaktion wie die Buchung gespeichert.
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
oVal: TJSONValue;
cKey, cUid, cArtikel, cGespeichert, cNeueUid: string;
nMenge: integer;
begin
oRes := TJSONObject.Create();
try
cUid := oParams.Values['_OBS_PATH_uid'];
cKey := oParams.Values['idempotency-key'];
// 1) Idempotenz: gleicher Key bereits verarbeitet?
if ((not Empty(cKey)) and IdempotenzAntwort(cKey, cGespeichert)) then begin
result := cGespeichert; // identische Antwort, keine Zweitbuchung
exit;
end;
// 2) Validierung -> 422 mit traceId
cArtikel := '';
nMenge := 0;
if (Assigned(oBody)) then begin
oVal := oBody.GetValue('artikel'); if (Assigned(oVal)) then cArtikel := oVal.Value;
oVal := oBody.GetValue('menge'); if (Assigned(oVal)) then nMenge := iVal(oVal.Value);
end;
if ((Empty(cArtikel)) or (nMenge <= 0)) then begin
oErr := TJSONObject.Create();
oErr.AddPair('code' , 'VALIDATION_FAILED');
oErr.AddPair('message', 'artikel und menge sind Pflicht');
oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
oRes.AddPair('_OBS_HTTP_STATUS', 422);
oRes.AddPair('error', oErr);
result := oRes.ToJSON();
exit;
end;
// 3) In EINER Transaktion: Position anlegen + Idempotenz-Eintrag committen
// (Transaktionssteuerung Über die OBS-Skript-DB-API)
cNeueUid := MaterialAnlegen(cUid, cArtikel, nMenge);
oRes.AddPair('_OBS_HTTP_STATUS', 201);
oRes.AddPair('uid', cNeueUid);
result := oRes.ToJSON();
if (not Empty(cKey)) then begin
IdempotenzSpeichern(cKey, result); // in derselben Transaktion
end;
finally
MyFreeAndNil(oRes);
end;
end;
Test mit curl
Token holen (Antwort enthÄlt token, refreshToken und serverTime):
curl -X POST -H "apikey: [API-KEY]" -H "Content-Type: application/json" ^
-d "{\"username\":\"tech1\",\"password\":\"geheim\"}" ^
https://api.meinserver.de/auth/
{"token":"eyJ...","refreshToken":"eyJ...","serverTime":"2026-06-29T15:30:12+02:00"}
Auftrag lesen (liefert den ETag-Header):
curl -i -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
https://api.meinserver.de/orders/4711
... ETag: 7
ändern mit korrektem If-Match -> 200, mit veraltetem If-Match -> 409:
curl -i -X PUT -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
-H "If-Match: 7" -H "Content-Type: application/json" ^
-d "{\"status\":\"erledigt\"}" ^
https://api.meinserver.de/orders/4711
Material idempotent erfassen (zweiter Aufruf mit gleichem Key -> gleiche Antwort, keine Doppelbuchung):
curl -i -X POST -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
-H "Idempotency-Key: 9c84-7f2a-..." -H "Content-Type: application/json" ^
-d "{\"artikel\":\"A100\",\"menge\":3}" ^
https://api.meinserver.de/orders/4711/material
Token erneuern (Refresh-Token im Authorization-Header an denselben JWT-Endpunkt):
curl -X POST -H "apikey: [API-KEY]" -H "Authorization: Bearer <refreshToken>" ^
https://api.meinserver.de/auth/
Was zeigt das Beispiel?
- Echte HTTP-Statuscodes aus dem Skript: 201, 403, 404, 409, 422.
- Optimistic Concurrency Über ETag (GET) und If-Match (PUT) -> 409 VERSION_CONFLICT.
- Idempotente Schreibzugriffe Über den Idempotency-Key (Schutz gegen Doppelbuchung).
- RollenprÜfung aus dem Token-Claim _OBS_JWT_CLAIM_roles, nicht aus dem Body.
- JWT mit Custom-Claims (tenant/roles) und Refresh-Token mit Rotation.
- traceId im Fehler-Body fÜr die Support-Nachverfolgung (auch als Header X-Trace-Id).