Files
richard-churchman 7e4850a4e7 Implemented role‑based data isolation for Case Management and Visualisation:
* Added role‑based data isolation across all entities related to Case Workflows and Visualisation.
* Introduced a common UI JavaScript widget for assigning roles to supported entities; updated existing JS components and generic CRUD logic to integrate the new widget.
* Added role‑permission tables and corresponding migrations, POCOs, repositories, controllers, DTOs, and validations for all in‑scope entities.
* Updated all end‑user queries to include joins through role allocations, enforcing data isolation for both Case Management and Visualisation.
* Included role allocations in preservation/backup logic to ensure they can be exported and restored correctly.
* Added documentation describing the isolation model and validation behaviour for Case Management and Visualisation.
* Renamed various architectural methods where ActiveOnly semantics now also imply role‑allocation filtering.
* Improved “no data found” handling on the case page to avoid appearing as though data is still loading.
* Updated the case query compilation routine to enforce role‑based isolation.
* Forced session cache invalidation via migrations to rebuild SQL with new filtering rules.
2026-02-04 13:42:31 +02:00

449 lines
17 KiB
JavaScript

/* Copyright (C) 2022-present Jube Holdings Limited.
*
* This file is part of Jube™ software.
*
* Jube™ is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* Jube™ is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License along with Jube™. If not,
* see <https://www.gnu.org/licenses/>.
*/
(function ($) {
var kendo = window.kendo,
ui = kendo.ui,
Widget = ui.Widget;
var RoleManager = Widget.extend({
init: function (element, options) {
var that = this;
Widget.fn.init.call(that, element, options);
that._pendingChanges = {
toAssign: [],
toUnassign: []
};
that._create();
that._fetchRoles();
},
options: {
name: "RoleManager",
baseEndpoint: "/api/CaseWorkflowStatusRole",
parentKeyField: "caseWorkflowStatusGuid",
width: 700,
height: 350,
availableTitle: "Available Roles",
assignedTitle: "Assigned Roles",
showSaveButton: true,
showCancelButton: true
},
_create: function () {
var that = this,
options = that.options;
that.element.addClass("k-role-manager");
var html = `
<div class="k-role-manager-wrapper">
<div class="k-role-manager-container" style="width: ${options.width}px;">
<div class="k-role-manager-panel">
<div class="k-role-manager-header">${options.availableTitle}</div>
<select id="availableRoles" multiple="multiple" style="height: ${options.height}px;"></select>
</div>
<div class="k-role-manager-buttons">
<button type="button" class="k-button k-button-md k-rounded-md k-button-solid k-button-solid-primary assign-btn" title="Assign selected roles">
<span class="k-icon k-i-arrow-chevron-right"></span>
</button>
<button type="button" class="k-button k-button-md k-rounded-md k-button-solid k-button-solid-primary unassign-btn" title="Unassign selected roles">
<span class="k-icon k-i-arrow-chevron-left"></span>
</button>
</div>
<div class="k-role-manager-panel">
<div class="k-role-manager-header">${options.assignedTitle}</div>
<select id="assignedRoles" multiple="multiple" style="height: ${options.height}px;"></select>
</div>
</div>
`;
if (options.showSaveButton || options.showCancelButton) {
html += '<div class="k-role-manager-actions">';
if (options.showSaveButton) {
html += '<button type="button" class="k-button save-btn">Update</button>';
}
if (options.showCancelButton) {
html += '<button type="button" class="k-button cancel-btn">Cancel</button>';
}
html += '</div>';
}
html += '</div>';
that.element.html(html);
that.availableListBox = that.element.find("#availableRoles").kendoListBox({
dataTextField: "name",
dataValueField: "guid",
toolbar: {
tools: []
}
}).data("kendoListBox");
that.assignedListBox = that.element.find("#assignedRoles").kendoListBox({
dataTextField: "name",
dataValueField: "guid",
toolbar: {
tools: []
}
}).data("kendoListBox");
that.element.find(".assign-btn").on("click", function (e) {
e.preventDefault();
that._moveToAssigned();
});
that.element.find(".unassign-btn").on("click", function (e) {
e.preventDefault();
that._moveToAvailable();
});
that.element.find(".save-btn").on("click", function (e) {
e.preventDefault();
that._saveChanges();
});
that.element.find(".cancel-btn").on("click", function (e) {
e.preventDefault();
that._cancelChanges();
});
that._addStyles();
that._updateButtonStates();
},
_fetchRoles: function () {
var that = this,
options = that.options;
$.ajax({
url: "/api/RoleRegistry",
method: "GET",
success: function (allRoles) {
$.ajax({
url: options.baseEndpoint + "/By" + options.parentKeyField + "/" + guid,
method: "GET",
success: function (assignedRoles) {
that._populateLists(allRoles, assignedRoles);
},
error: function (xhr, status, error) {
console.error("Error fetching assigned roles:", error);
that._populateLists(allRoles, []);
}
});
},
error: function (xhr, status, error) {
console.error("Error fetching roles:", error);
}
});
},
_populateLists: function (allRoles, assignedRoles) {
var that = this, options = that.options;
var assignedGuidSet = new Set();
var guidToAssignmentId = new Map();
assignedRoles.forEach(function (ar) {
var guid = ar.RoleRegistryGuid || ar.roleRegistryGuid || ar["guid"];
var assignmentId = ar.Id || ar.id || ar["id"];
if (guid) {
assignedGuidSet.add(guid);
if (assignmentId != null) guidToAssignmentId.set(guid, assignmentId);
}
});
that._originalAssignedIds = Array.from(assignedGuidSet);
var available = [];
var assigned = [];
allRoles.forEach(function (role) {
var guid = role.RoleRegistryGuid || role.roleRegistryGuid || role["guid"];
var name = role.Name || role.name || role["name"];
if (!guid) return;
var item = {};
item["guid"] = guid;
item["name"] = name;
if (assignedGuidSet.has(guid)) {
item["id"] = guidToAssignmentId.get(guid);
assigned.push(item);
} else {
available.push(item);
}
});
that.availableListBox.setDataSource(new kendo.data.DataSource({data: available}));
that.assignedListBox.setDataSource(new kendo.data.DataSource({data: assigned}));
that._pendingChanges = {toAssign: [], toUnassign: []};
that._updateButtonStates();
},
_moveToAssigned: function () {
var that = this, options = that.options, selected = that.availableListBox.select();
if (!selected || selected.length === 0) return;
selected.each(function () {
var dataItem = that.availableListBox.dataItem($(this));
var availableDS = that.availableListBox.dataSource;
var assignedDS = that.assignedListBox.dataSource;
var itemCopy = {};
itemCopy["guid"] = dataItem["guid"];
itemCopy["name"] = dataItem["name"];
availableDS.remove(dataItem);
assignedDS.add(itemCopy);
that._trackChange(itemCopy, 'assign');
});
that._updateButtonStates();
},
_moveToAvailable: function () {
var that = this, options = that.options, selected = that.assignedListBox.select();
if (!selected || selected.length === 0) return;
selected.each(function () {
var dataItem = that.assignedListBox.dataItem($(this));
var availableDS = that.availableListBox.dataSource;
var assignedDS = that.assignedListBox.dataSource;
var itemCopy = {};
itemCopy["guid"] = dataItem["guid"];
itemCopy["name"] = dataItem["name"];
if (dataItem["id"] != null) {
itemCopy["id"] = dataItem["id"];
}
assignedDS.remove(dataItem);
availableDS.add(itemCopy);
that._trackChange(dataItem, 'unassign');
});
that._updateButtonStates();
},
_trackChange: function (dataItem, action) {
var that = this, options = that.options;
var guid = dataItem["guid"];
var assignmentId = dataItem["id"];
var wasOriginallyAssigned = that._originalAssignedIds.indexOf(guid) >= 0;
if (action === 'assign') {
if (assignmentId != null) {
var idxUn = that._pendingChanges.toUnassign.indexOf(assignmentId);
if (idxUn >= 0) that._pendingChanges.toUnassign.splice(idxUn, 1);
}
if (!wasOriginallyAssigned && that._pendingChanges.toAssign.indexOf(guid) < 0) {
that._pendingChanges.toAssign.push(guid);
}
} else {
var idxAsn = that._pendingChanges.toAssign.indexOf(guid);
if (idxAsn >= 0) {
that._pendingChanges.toAssign.splice(idxAsn, 1);
return;
}
if (wasOriginallyAssigned && assignmentId != null && that._pendingChanges.toUnassign.indexOf(assignmentId) < 0) {
that._pendingChanges.toUnassign.push(assignmentId);
}
}
},
_saveChanges: function () {
var that = this, options = that.options;
if (!that._hasPendingChanges()) return;
var promises = [];
that._pendingChanges.toAssign.forEach(function (roleGuid) {
var data = {
[options.parentKeyField]: guid,
RoleRegistryGuid: roleGuid
};
promises.push($.ajax({
url: options.baseEndpoint,
method: "POST",
contentType: "application/json",
data: JSON.stringify(data)
}));
that._pendingChanges.toUnassign.forEach(function (assignmentId) {
promises.push($.ajax({
url: options.baseEndpoint + "/" + encodeURIComponent(assignmentId),
method: "DELETE",
contentType: "application/json"
}));
});
$.when.apply($, promises).done(function () {
that._originalAssignedIds = [];
that.assignedListBox.dataSource.data().forEach(function (item) {
that._originalAssignedIds.push(item["guid"]);
});
that._pendingChanges = {toAssign: [], toUnassign: []};
that._updateButtonStates();
that.trigger("save", {success: true});
}).fail(function (xhr, status, error) {
console.error("Error saving changes:", error);
alert("Error saving changes. Please try again.");
that.trigger("save", {success: false, error: error});
});
});
that._pendingChanges.toUnassign.forEach(function (id) {
promises.push($.ajax({
url: options.baseEndpoint + "/" + id,
method: "DELETE",
contentType: "application/json"
}));
});
$.when.apply($, promises).done(function () {
that._originalAssignedIds = [];
that.assignedListBox.dataSource.data().forEach(function (item) {
that._originalAssignedIds.push(item["guid"]);
});
that._pendingChanges = {
toAssign: [],
toUnassign: []
};
that._updateButtonStates();
that.trigger("save", {success: true});
}).fail(function (xhr, status, error) {
console.error("Error saving changes:", error);
alert("Error saving changes. Please try again.");
that.trigger("save", {success: false, error: error});
});
},
_cancelChanges: function () {
var that = this;
if (!that._hasPendingChanges()) {
return;
}
that._fetchRoles();
that.trigger("cancel");
},
_hasPendingChanges: function () {
var that = this;
return that._pendingChanges.toAssign.length > 0 ||
that._pendingChanges.toUnassign.length > 0;
},
_updateButtonStates: function () {
var that = this,
hasPending = that._hasPendingChanges();
var saveBtn = that.element.find(".save-btn");
var cancelBtn = that.element.find(".cancel-btn");
if (hasPending) {
saveBtn.removeClass("k-disabled").prop("disabled", false);
cancelBtn.removeClass("k-disabled").prop("disabled", false);
} else {
saveBtn.addClass("k-disabled").prop("disabled", true);
cancelBtn.addClass("k-disabled").prop("disabled", true);
}
},
_addStyles: function () {
if ($("#role-manager-styles").length === 0) {
var styles = `
<style id="role-manager-styles">
.k-role-manager-wrapper {
display: inline-block;
}
.k-role-manager-container {
display: flex;
gap: 10px;
align-items: flex-start;
}
.k-role-manager-panel {
flex: 1;
}
.k-role-manager-header {
font-weight: bold;
padding: 8px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-bottom: none;
}
.k-role-manager-buttons {
display: flex;
flex-direction: column;
gap: 10px;
padding-top: 40px;
}
.k-role-manager .k-listbox {
width: 100%;
}
.k-role-manager-actions {
margin-top: 15px;
text-align: right;
padding-right: 5px;
}
.k-role-manager-actions .k-button {
margin-left: 10px;
}
</style>
`;
$("head").append(styles);
}
},
refresh: function () {
this._fetchRoles();
},
destroy: function () {
var that = this;
that.availableListBox.destroy();
that.assignedListBox.destroy();
that.element.find(".assign-btn").off("click");
that.element.find(".unassign-btn").off("click");
that.element.find(".save-btn").off("click");
that.element.find(".cancel-btn").off("click");
Widget.fn.destroy.call(that);
}
});
ui.plugin(RoleManager);
})(jQuery);
//# sourceURL=RoleAllocation.js