unit PM.Handler.Settings; (* GET /settings -> {} PUT /settings body: {} -> {message:"OK"} Persists a per-user preferences blob (users.settings_json). The server treats the body as opaque JSON — schema lives in the JS layer. Any client reading it should tolerate unknown keys for forward compatibility. Device-specific toggles (quick-unlock DPAPI, autofill hotkey) deliberately stay in localStorage on the client and are NOT included in this blob. *) interface implementation uses System.SysUtils, System.JSON, System.Classes, Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Param, IdCustomHTTPServer, PM.Router, PM.JSON, PM.Session, PM.Database; procedure HandleGetSettings(ARequest: TIdHTTPRequestInfo; AResponse: TIdHTTPResponseInfo; const AParams: TArray); var LUserId: Integer; LQ: TFDQuery; LRaw: string; LObj: TJSONValue; begin LUserId := Authenticate(ARequest, AResponse); DB.Lock; try LQ := TFDQuery.Create(nil); try LQ.Connection := DB.Connection; LQ.SQL.Text := 'SELECT settings_json FROM users WHERE id = :uid'; LQ.ParamByName('uid').AsInteger := LUserId; LQ.Open; if LQ.IsEmpty then LRaw := '{}' else LRaw := LQ.FieldByName('settings_json').AsString; finally LQ.Free; end; finally DB.Unlock; end; if Trim(LRaw) = '' then LRaw := '{}'; // Validate so a corrupt row doesn't return malformed JSON to the client. LObj := TJSONObject.ParseJSONValue(LRaw); if LObj = nil then LObj := TJSONObject.Create; TJSONHelper.SendJSON(AResponse, LObj); // SendJSON frees the object end; procedure HandlePutSettings(ARequest: TIdHTTPRequestInfo; AResponse: TIdHTTPResponseInfo; const AParams: TArray); var LUserId: Integer; LBody: TJSONObject; LSerialized: string; LQ: TFDQuery; begin LUserId := Authenticate(ARequest, AResponse); RequireCSRF(ARequest, AResponse, LUserId); LBody := TJSONHelper.ReadBody(ARequest); try // Re-serialize to a canonical compact form (strips comments / extra // whitespace, and guarantees what we store is valid JSON). LSerialized := LBody.ToJSON; finally LBody.Free; end; // Soft cap to protect the row from a runaway client (typical settings // blob is a few hundred bytes; 16 KB leaves room for future flags). if Length(LSerialized) > 16384 then begin TJSONHelper.SendError(AResponse, 413, 'Settings payload too large'); Exit; end; DB.Lock; try LQ := TFDQuery.Create(nil); try LQ.Connection := DB.Connection; LQ.SQL.Text := 'UPDATE users SET settings_json = :s WHERE id = :uid'; LQ.ParamByName('s').AsString := LSerialized; LQ.ParamByName('uid').AsInteger := LUserId; LQ.ExecSQL; finally LQ.Free; end; finally DB.Unlock; end; TJSONHelper.SendOK(AResponse); end; initialization Router.Register('GET', '/settings', HandleGetSettings); Router.Register('PUT', '/settings', HandlePutSettings); end.