unit PM.Handler.Folders; (* GET /folders -> JSON array of folder names POST /folders body {name} -> {message,name} DELETE /folders/{name} -> {message} *) interface implementation uses System.SysUtils, System.JSON, System.NetEncoding, FireDAC.Comp.Client, FireDAC.Stan.Param, IdCustomHTTPServer, PM.Router, PM.JSON, PM.Database, PM.Session, PM.Audit, PM.RateLimit; // ===== GET /folders ========================================================== procedure HandleGetFolders(ARequest: TIdHTTPRequestInfo; AResponse: TIdHTTPResponseInfo; const AParams: TArray); var LUserId: Integer; LQ: TFDQuery; LArr: TJSONArray; begin try LUserId := Authenticate(ARequest, AResponse); except on ESessionRejected do Exit; end; LArr := TJSONArray.Create; DB.Lock; try LQ := TFDQuery.Create(nil); try LQ.Connection := DB.Connection; LQ.SQL.Text := 'SELECT name FROM folders WHERE user_id = :uid ORDER BY name'; LQ.ParamByName('uid').AsInteger := LUserId; LQ.Open; while not LQ.Eof do begin LArr.Add(LQ.FieldByName('name').AsString); LQ.Next; end; finally LQ.Free; end; finally DB.Unlock; end; TJSONHelper.SendJSON(AResponse, LArr); end; // ===== POST /folders ========================================================= procedure HandleCreateFolder(ARequest: TIdHTTPRequestInfo; AResponse: TIdHTTPResponseInfo; const AParams: TArray); var LUserId: Integer; LBody: TJSONObject; LName: string; LQ: TFDQuery; LObj: TJSONObject; begin try LUserId := Authenticate(ARequest, AResponse); RequireCSRF(ARequest, AResponse, LUserId); except on ESessionRejected do Exit; end; LBody := TJSONHelper.ReadBody(ARequest); try LName := Trim(LBody.GetValue('name', '')); finally LBody.Free; end; if LName = '' then begin TJSONHelper.SendError(AResponse, 400, 'Folder name required'); Exit; end; if SameText(LName, 'All') then begin TJSONHelper.SendError(AResponse, 400, 'Cannot use All'); Exit; end; DB.Lock; try LQ := TFDQuery.Create(nil); try LQ.Connection := DB.Connection; LQ.SQL.Text := 'INSERT INTO folders (user_id, name) VALUES (:uid, :name)'; LQ.ParamByName('uid').AsInteger := LUserId; LQ.ParamByName('name').AsString := LName; try LQ.ExecSQL; except on E: Exception do begin TJSONHelper.SendError(AResponse, 409, 'Folder exists'); Exit; end; end; finally LQ.Free; end; finally DB.Unlock; end; LogAudit(LUserId, 'add_folder', GetClientIP(ARequest)); LObj := TJSONObject.Create; LObj.AddPair('message', 'Created'); LObj.AddPair('name', LName); TJSONHelper.SendJSON(AResponse, LObj); end; // ===== DELETE /folders/{name} ================================================ procedure HandleDeleteFolder(ARequest: TIdHTTPRequestInfo; AResponse: TIdHTTPResponseInfo; const AParams: TArray); var LUserId: Integer; LName: string; LQ: TFDQuery; LChanges: Integer; begin try LUserId := Authenticate(ARequest, AResponse); RequireCSRF(ARequest, AResponse, LUserId); except on ESessionRejected do Exit; end; if Length(AParams) < 1 then begin TJSONHelper.SendError(AResponse, 400, 'Folder name required'); Exit; end; LName := TNetEncoding.URL.Decode(AParams[0]); if SameText(LName, 'All') then begin TJSONHelper.SendError(AResponse, 400, 'Cannot delete All'); Exit; end; DB.Lock; try LQ := TFDQuery.Create(nil); try LQ.Connection := DB.Connection; LQ.SQL.Text := 'DELETE FROM folders WHERE user_id = :uid AND name = :name'; LQ.ParamByName('uid').AsInteger := LUserId; LQ.ParamByName('name').AsString := LName; LQ.ExecSQL; LChanges := LQ.RowsAffected; finally LQ.Free; end; if LChanges = 0 then begin TJSONHelper.SendError(AResponse, 404, 'Not found'); Exit; end; // Reassign entries from the deleted folder to 'All' LQ := TFDQuery.Create(nil); try LQ.Connection := DB.Connection; LQ.SQL.Text := 'UPDATE vault_entries SET folder = ''All'' ' + 'WHERE user_id = :uid AND folder = :name'; LQ.ParamByName('uid').AsInteger := LUserId; LQ.ParamByName('name').AsString := LName; LQ.ExecSQL; finally LQ.Free; end; finally DB.Unlock; end; LogAudit(LUserId, 'delete_folder', GetClientIP(ARequest)); TJSONHelper.SendOK(AResponse, 'Deleted'); end; initialization Router.Register('GET', '/folders', HandleGetFolders); Router.Register('POST', '/folders', HandleCreateFolder); Router.Register('DELETE', '/folders/(.+)', HandleDeleteFolder); end.