Add org leave/disband bridge flow across client and server

- Introduce `OrgUIBridge` to centralize UI event/request/response handling
- Add leave and disband org requests with server handlers and client bridge events
- Enforce portal permissions for leaving/disbanding and protect owner/self from member removal
This commit is contained in:
Jacob Schmidt 2026-03-09 23:06:26 -05:00
parent 6eb6ac79d1
commit 9cd7278746
16 changed files with 843 additions and 127 deletions

View File

@ -1,3 +1,4 @@
PREP(handleUIEvents);
PREP(initOrgClass);
PREP(initOrgUIBridge);
PREP(openUI);

View File

@ -1,6 +1,7 @@
#include "script_component.hpp"
if (isNil QGVAR(OrgClass)) then { call FUNC(initOrgClass); };
if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initOrgUIBridge); };
[QGVAR(initOrg), {
GVAR(OrgClass) call ["init", []];
@ -21,27 +22,19 @@ if (isNil QGVAR(OrgClass)) then { call FUNC(initOrgClass); };
[QGVAR(responseCreateOrg), {
params [["_payload", createHashMap, [createHashMap]]];
private _control = uiNamespace getVariable [QGVAR(PendingBrowserControl), controlNull];
uiNamespace setVariable [QGVAR(PendingBrowserControl), controlNull];
GVAR(OrgUIBridge) call ["handleCreateResponse", [_payload]];
}] call CFUNC(addEventHandler);
private _success = _payload getOrDefault ["success", false];
if (!_success) exitWith {
if (_control isNotEqualTo controlNull) then {
private _json = toJSON (createHashMapFromArray [
["message", _payload getOrDefault ["message", "Organization registration failed."]]
]);
[QGVAR(responseDisbandOrg), {
params [["_payload", createHashMap, [createHashMap]]];
_control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.receiveCreateFailure(%1)", _json]];
};
};
GVAR(OrgUIBridge) call ["handleDisbandResponse", [_payload]];
}] call CFUNC(addEventHandler);
private _orgData = _payload getOrDefault ["org", createHashMap];
GVAR(OrgClass) call ["sync", [_orgData, true]];
[QGVAR(responseLeaveOrg), {
params [["_payload", createHashMap, [createHashMap]]];
if (_control isNotEqualTo controlNull) then {
private _json = toJSON (GVAR(OrgClass) call ["buildPortalPayload", []]);
_control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.receiveCreateSuccess(%1)", _json]];
};
GVAR(OrgUIBridge) call ["handleLeaveResponse", [_payload]];
}] call CFUNC(addEventHandler);
[{

View File

@ -21,52 +21,25 @@ params ["_control", "_isConfirmDialog", "_message"];
private _alert = fromJSON _message;
private _event = _alert get "event";
private _data = _alert get "data";
// private _display = displayChild findDisplay 46;
private _fnc_execBridge = {
params ["_control", "_fnName", "_payload"];
private _json = toJSON _payload;
_control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.%1(%2)", _fnName, _json]];
};
diag_log format ["[FORGE:Client:Org] Handling UI event: %1 with data: %2", _event, _data];
switch (_event) do {
case "org::close": { closeDialog 1; };
case "org::login::request": {
private _orgData = GVAR(OrgClass) get "org";
private _orgId = _orgData getOrDefault ["id", ""];
private _orgName = _orgData getOrDefault ["name", ""];
if (_orgId isEqualTo "" && {_orgName isEqualTo ""}) exitWith {
[_control, "receiveLoginFailure", createHashMapFromArray [
["message", "No organization data is available for this player."]
]] call _fnc_execBridge;
};
private _payload = GVAR(OrgClass) call ["buildPortalPayload", []];
[_control, "receiveLoginSuccess", _payload] call _fnc_execBridge;
GVAR(OrgUIBridge) call ["handleLoginRequest", [_control]];
};
case "org::create::request": {
private _orgName = _data getOrDefault ["orgName", ""];
if (_orgName isEqualTo "") exitWith {
[_control, "receiveCreateFailure", createHashMapFromArray [
["message", "Enter an organization name."]
]] call _fnc_execBridge;
GVAR(OrgUIBridge) call ["handleCreateRequest", [_control, _data]];
};
uiNamespace setVariable [QGVAR(PendingBrowserControl), _control];
[SRPC(org,requestCreateOrg), [getPlayerUID player, _orgName]] call CFUNC(serverEvent);
case "org::disband::request": {
GVAR(OrgUIBridge) call ["requestDisband", []];
};
case "org::leave::request": {
GVAR(OrgUIBridge) call ["requestLeave", []];
};
case "org::ready": {
[_control, "receive", createHashMapFromArray [
["event", "org::ready"],
["data", createHashMapFromArray [
["loaded", true]
]]
]] call _fnc_execBridge;
GVAR(OrgUIBridge) call ["handleReady", [_control]];
};
default { hint format ["Unhandled UI event: %1", _event]; };
};

View File

@ -101,7 +101,10 @@ GVAR(OrgBaseClass) = compileFinal createHashMapFromArray [
if (_memberUid isEqualTo _ownerUid && {_ownerName isEqualTo ""}) then { _ownerName = _memberName; };
if (_memberUid isEqualTo _playerUid) then { _sessionRole = "Member"; };
_membersList pushBack (createHashMapFromArray [["name", _memberName]]);
_membersList pushBack (createHashMapFromArray [
["uid", _memberUid],
["name", _memberName]
]);
} forEach _membersRaw;
if (_ownerName isEqualTo "" && { _ownerUid isEqualTo _playerUid }) then { _ownerName = _playerName; };

View File

@ -0,0 +1,173 @@
#include "..\script_component.hpp"
/*
* File: fnc_initOrgUIBridge.sqf
* Author: IDSolutions
* Date: 2026-03-10
* Last Update: 2026-03-10
* Public: No
*
* Description:
* Initializes the org UI bridge for browser control state and event routing.
*
* Arguments:
* None
*
* Return Value:
* Org UI bridge object [HASHMAP OBJECT]
*
* Examples:
* call forge_client_org_fnc_initOrgUIBridge
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [
["#type", "OrgUIBridgeBaseClass"],
["#create", compileFinal {
_self set ["pendingBrowserControl", controlNull];
}],
["setPendingBrowserControl", compileFinal {
params [["_control", controlNull, [controlNull]]];
_self set ["pendingBrowserControl", _control];
_control
}],
["consumePendingBrowserControl", compileFinal {
private _control = _self getOrDefault ["pendingBrowserControl", controlNull];
_self set ["pendingBrowserControl", controlNull];
_control
}],
["getActiveBrowserControl", compileFinal {
private _display = uiNamespace getVariable ["RscOrg", displayNull];
if (isNull _display) exitWith { controlNull };
_display displayCtrl 1003
}],
["execBridge", compileFinal {
params [
["_control", controlNull, [controlNull]],
["_fnName", "", [""]],
["_payload", createHashMap, [createHashMap]]
];
if (isNull _control || { _fnName isEqualTo "" }) exitWith { false };
private _json = toJSON _payload;
_control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.%1(%2)", _fnName, _json]];
true
}],
["sendBridgeEvent", compileFinal {
params [
["_event", "", [""]],
["_data", createHashMap, [createHashMap]],
["_control", controlNull, [controlNull]]
];
if (_event isEqualTo "") exitWith { false };
private _targetControl = _control;
if (isNull _targetControl) then {
_targetControl = _self call ["getActiveBrowserControl", []];
};
if (isNull _targetControl) exitWith { false };
_self call ["execBridge", [_targetControl, "receive", createHashMapFromArray [
["event", _event],
["data", _data]
]]]
}],
["handleLoginRequest", compileFinal {
params [["_control", controlNull, [controlNull]]];
private _orgData = GVAR(OrgClass) get "org";
private _orgId = _orgData getOrDefault ["id", ""];
private _orgName = _orgData getOrDefault ["name", ""];
if (_orgId isEqualTo "" && { _orgName isEqualTo "" }) exitWith {
_self call ["execBridge", [_control, "receiveLoginFailure", createHashMapFromArray [
["message", "No organization data is available for this player."]
]]];
};
_self call ["execBridge", [_control, "receiveLoginSuccess", GVAR(OrgClass) call ["buildPortalPayload", []]]];
}],
["handleCreateRequest", compileFinal {
params [
["_control", controlNull, [controlNull]],
["_data", createHashMap, [createHashMap]]
];
private _orgName = _data getOrDefault ["orgName", ""];
if (_orgName isEqualTo "") exitWith {
_self call ["execBridge", [_control, "receiveCreateFailure", createHashMapFromArray [
["message", "Enter an organization name."]
]]];
};
_self call ["setPendingBrowserControl", [_control]];
[SRPC(org,requestCreateOrg), [getPlayerUID player, _orgName]] call CFUNC(serverEvent);
}],
["handleCreateResponse", compileFinal {
params [["_payload", createHashMap, [createHashMap]]];
private _control = _self call ["consumePendingBrowserControl", []];
private _success = _payload getOrDefault ["success", false];
if (!_success) exitWith {
if (isNull _control) exitWith {};
_self call ["execBridge", [_control, "receiveCreateFailure", createHashMapFromArray [
["message", _payload getOrDefault ["message", "Organization registration failed."]]
]]];
};
private _orgData = _payload getOrDefault ["org", createHashMap];
GVAR(OrgClass) call ["sync", [_orgData, true]];
if (isNull _control) exitWith {};
_self call ["execBridge", [_control, "receiveCreateSuccess", GVAR(OrgClass) call ["buildPortalPayload", []]]];
}],
["handleDisbandResponse", compileFinal {
params [["_payload", createHashMap, [createHashMap]]];
private _eventName = if (_payload getOrDefault ["success", false]) then {
["org::portal::revoked", "org::disband::success"] select (_payload getOrDefault ["requester", false])
} else {
"org::disband::failure"
};
_self call ["sendBridgeEvent", [_eventName, _payload]];
}],
["handleLeaveResponse", compileFinal {
params [["_payload", createHashMap, [createHashMap]]];
private _eventName = [
"org::leave::failure",
"org::leave::success"
] select (_payload getOrDefault ["success", false]);
_self call ["sendBridgeEvent", [_eventName, _payload]];
}],
["requestDisband", compileFinal {
[SRPC(org,requestDisbandOrg), [getPlayerUID player]] call CFUNC(serverEvent);
}],
["requestLeave", compileFinal {
[SRPC(org,requestLeaveOrg), [getPlayerUID player]] call CFUNC(serverEvent);
}],
["handleReady", compileFinal {
params [["_control", controlNull, [controlNull]]];
_self call ["sendBridgeEvent", [
"org::ready",
createHashMapFromArray [
["loaded", true]
],
_control
]];
}]
];
GVAR(OrgUIBridge) = createHashMapObject [GVAR(OrgUIBridgeBaseClass)];
GVAR(OrgUIBridge)

View File

@ -41,6 +41,36 @@
store.failCreate("Arma registration bridge is unavailable.");
}
function requestDisbandOrg() {
const sent = sendEvent("org::disband::request", {});
if (sent) {
return;
}
const OrgPortal = window.OrgPortal;
if (OrgPortal && OrgPortal.actions) {
OrgPortal.actions.showTreasuryNotice(
"error",
"Arma disband bridge is unavailable.",
);
}
}
function requestLeaveOrg() {
const sent = sendEvent("org::leave::request", {});
if (sent) {
return;
}
const OrgPortal = window.OrgPortal;
if (OrgPortal && OrgPortal.actions) {
OrgPortal.actions.showTreasuryNotice(
"error",
"Arma leave bridge is unavailable.",
);
}
}
function receive(eventOrPayload, data = {}) {
const event =
typeof eventOrPayload === "object" && eventOrPayload !== null
@ -70,12 +100,76 @@
store.failCreate(
payloadData.message || "Organization registration failed.",
);
return;
}
const OrgPortal = window.OrgPortal;
if (event === "org::disband::success") {
if (OrgPortal && OrgPortal.store) {
OrgPortal.store.setModal(null);
OrgPortal.store.setOrgDisbanded(true);
}
return;
}
if (event === "org::disband::failure") {
if (OrgPortal && OrgPortal.store) {
OrgPortal.store.setModal(null);
}
if (OrgPortal && OrgPortal.actions) {
OrgPortal.actions.showTreasuryNotice(
"error",
payloadData.message || "Organization disbanding failed.",
);
}
return;
}
if (event === "org::leave::success") {
if (OrgPortal && OrgPortal.store) {
OrgPortal.store.setModal(null);
}
store.failLogin(
payloadData.message || "You have left the organization.",
);
store.setView("home");
return;
}
if (event === "org::leave::failure") {
if (OrgPortal && OrgPortal.store) {
OrgPortal.store.setModal(null);
}
if (OrgPortal && OrgPortal.actions) {
OrgPortal.actions.showTreasuryNotice(
"error",
payloadData.message || "Unable to leave the organization.",
);
}
return;
}
if (event === "org::portal::revoked") {
if (OrgPortal && OrgPortal.store) {
OrgPortal.store.setModal(null);
}
store.failLogin(
payloadData.message ||
"Organization access is no longer available.",
);
store.setView("home");
}
}
RegistryApp.bridge = {
requestLogin,
requestCreateOrg,
requestDisbandOrg,
requestLeaveOrg,
receive,
sendEvent,
};
@ -83,6 +177,8 @@
window.OrgUIBridge = {
requestLogin,
requestCreateOrg,
requestDisbandOrg,
requestLeaveOrg,
receive,
receiveLoginSuccess: (data) => receive("org::login::success", data),
receiveLoginFailure: (data) => receive("org::login::failure", data),

View File

@ -71,6 +71,14 @@
: null;
const view = store.getView();
const portalPermissions =
window.OrgPortal && window.OrgPortal.permissions
? window.OrgPortal.permissions
: null;
const portalActions =
window.OrgPortal && window.OrgPortal.actions
? window.OrgPortal.actions
: null;
const viewLabel =
view === "create"
? "Organization Registration"
@ -116,6 +124,11 @@
}
if (view === "portal" && PortalApp) {
const canLeaveOrg =
portalPermissions &&
typeof portalPermissions.canLeaveOrg === "function" &&
portalPermissions.canLeaveOrg();
return h(
"div",
{ className: "app-shell" },
@ -126,6 +139,13 @@
Navbar({
title: "Global Organization Network",
viewLabel,
actionLabel: canLeaveOrg ? "Leave Organization" : "",
onAction:
canLeaveOrg &&
portalActions &&
typeof portalActions.openModal === "function"
? () => portalActions.openModal("leave")
: null,
}),
PortalApp(),
);

View File

@ -3,10 +3,26 @@
const { h, ensureScopedStyle } = OrgPortal.runtime;
const scopeAttr = "data-ui-future-card";
const ROADMAP = [
{ name: "Contracts Board", status: "Planned", detail: "Track payouts, assignments, and claim approvals." },
{ name: "Diplomacy", status: "Future Review", detail: "Possible future module pending a full design and scope review." },
{ name: "Logistics Queue", status: "Future Review", detail: "Possible future module pending a full design and scope review." },
{ name: "Permissions", status: "Future Review", detail: "Possible future module pending a full design and scope review." },
{
name: "Contracts Board",
status: "Planned",
detail: "Track payouts, assignments, and claim approvals.",
},
{
name: "Diplomacy",
status: "Future Review",
detail: "Possible future module pending a full design and scope review.",
},
{
name: "Logistics Queue",
status: "Future Review",
detail: "Possible future module pending a full design and scope review.",
},
{
name: "Permissions",
status: "Future Review",
detail: "Possible future module pending a full design and scope review.",
},
];
const scopeSelector = `[${scopeAttr}]`;
const futureCardCss = `

View File

@ -63,7 +63,7 @@ ${scopeSelector} .org-name-row button {
className: "org-scroll-panel org-span-5",
title: "Members",
subtitle:
"Current roster listing. The organization owner cannot be removed.",
"Current roster listing. The organization owner and your own member entry cannot be removed.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
@ -71,7 +71,7 @@ ${scopeSelector} .org-name-row button {
...members.map((member) => {
const canRemoveMember =
allowMemberManagement &&
!actions.isOwnerMember(member.name);
!actions.isProtectedMember(member);
return h(
"article",
@ -86,7 +86,7 @@ ${scopeSelector} .org-name-row button {
title: `Remove ${member.name}`,
"aria-label": `Remove ${member.name}`,
onClick: () =>
actions.removeMember(member.name),
actions.removeMember(member),
},
h(
"svg",

View File

@ -246,6 +246,41 @@
),
),
);
} else if (modal.type === "leave") {
title = "Leave Organization";
body = h(
"div",
{ className: "app-modal-danger" },
h(
"p",
null,
"Leave ",
portalData.org.name,
" and return to the default organization?",
),
h(
"div",
{ className: "app-modal-danger-actions" },
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.closeModal(),
},
"Cancel",
),
h(
"button",
{
type: "button",
className: "org-danger-btn",
onClick: () => actions.leaveOrganization(),
},
"Confirm Leave",
),
),
);
}
return Modal({

View File

@ -88,17 +88,58 @@
return el ? el.value : "";
}
isOwnerMember(memberName) {
getMemberName(member) {
if (member && typeof member === "object") {
return String(member.name || "");
}
return String(member || "");
}
getMemberUid(member) {
if (member && typeof member === "object") {
return String(member.uid || "");
}
return "";
}
isOwnerMember(member) {
return (
String(memberName || "")
.trim()
.toLowerCase() ===
this.getMemberName(member).trim().toLowerCase() ===
String(portalData.org.owner || "")
.trim()
.toLowerCase()
);
}
isCurrentMember(member) {
const session = window.OrgPortal?.data?.session || {};
const memberUid = this.getMemberUid(member)
.trim()
.toLowerCase();
const actorUid = String(session.actorUid || "")
.trim()
.toLowerCase();
if (memberUid && actorUid) {
return memberUid === actorUid;
}
return (
this.getMemberName(member).trim().toLowerCase() ===
String(session.actorName || "")
.trim()
.toLowerCase()
);
}
isProtectedMember(member) {
return (
this.isOwnerMember(member) || this.isCurrentMember(member)
);
}
closePortal() {
if (
typeof A3API !== "undefined" &&
@ -136,6 +177,10 @@
return;
}
if (type === "leave" && !permissions.canLeaveOrg()) {
return;
}
store.setModal({ type });
}
@ -143,18 +188,23 @@
store.setModal(null);
}
removeMember(memberName) {
removeMember(member) {
if (!permissions.canManageMembers()) {
return false;
}
if (this.isOwnerMember(memberName)) {
if (this.isProtectedMember(member)) {
return false;
}
const memberUid = this.getMemberUid(member);
const memberName = this.getMemberName(member);
store.setMembers((currentMembers) =>
currentMembers.filter(
(member) => member.name !== memberName,
currentMembers.filter((entry) =>
memberUid
? entry.uid !== memberUid
: entry.name !== memberName,
),
);
store.setCreditLines((currentLines) =>
@ -168,8 +218,42 @@
return false;
}
store.setOrgDisbanded(true);
const bridge = window.RegistryApp
? window.RegistryApp.bridge
: null;
if (!bridge || typeof bridge.requestDisbandOrg !== "function") {
this.showTreasuryNotice(
"error",
"Disband bridge is unavailable.",
);
return false;
}
this.closeModal();
bridge.requestDisbandOrg();
return true;
}
leaveOrganization() {
if (!permissions.canLeaveOrg()) {
return false;
}
const bridge = window.RegistryApp
? window.RegistryApp.bridge
: null;
if (!bridge || typeof bridge.requestLeaveOrg !== "function") {
this.showTreasuryNotice(
"error",
"Leave bridge is unavailable.",
);
return false;
}
this.closeModal();
bridge.requestLeaveOrg();
return true;
}

View File

@ -66,7 +66,11 @@
}
canDisbandOrg() {
return this.isOrgLeaderOrCeo();
return this.isOrgOwner() && !this.isDefaultOrg();
}
canLeaveOrg() {
return !this.isDefaultOrg() && !this.isOrgOwner();
}
}

View File

@ -25,11 +25,8 @@ PREP_RECOMPILE_END;
private _result = GVAR(OrgStore) call ["register", [_uid, _orgName]];
if (_result getOrDefault ["success", false]) then {
private _org = _result getOrDefault ["org", createHashMap];
private _orgID = _org getOrDefault ["id", ""];
if (_orgID isNotEqualTo "") then {
private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, true]];
private _actorPatch = _result getOrDefault ["actorPatch", createHashMap];
if (_actorPatch isNotEqualTo createHashMap) then {
[CRPC(actor,responseSyncActor), [_actorPatch], _player] call CFUNC(targetEvent);
};
};
@ -89,5 +86,85 @@ PREP_RECOMPILE_END;
private _index = GVAR(IndexRegistry) get _uid;
private _key = _index get "orgID";
GVAR(OrgStore) call ["remove", [GVAR(Registry), "org:update", _key]];
GVAR(OrgStore) call ["delete", [_key]];
}] call CFUNC(addEventHandler);
[QGVAR(requestLeaveOrg), {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith {
diag_log "[FORGE:Server:Org] Empty/Invalid UID for leave request!"
};
private _player = [_uid] call EFUNC(common,getPlayer);
if (_player isEqualTo objNull) exitWith {};
private _result = GVAR(OrgStore) call ["leave", [_uid]];
if (_result getOrDefault ["success", false]) then {
private _actorPatch = _result getOrDefault ["actorPatch", createHashMap];
if (_actorPatch isNotEqualTo createHashMap) then {
[CRPC(actor,responseSyncActor), [_actorPatch], _player] call CFUNC(targetEvent);
};
GVAR(OrgStore) call ["init", [_uid]];
private _notificationParams = _result getOrDefault ["notification", []];
if (_notificationParams isEqualType [] && { count _notificationParams > 0 }) then {
[CRPC(notifications,recieveNotification), _notificationParams, _player] call CFUNC(targetEvent);
};
};
[CRPC(org,responseLeaveOrg), [createHashMapFromArray [
["success", _result getOrDefault ["success", false]],
["message", _result getOrDefault ["message", "Unable to leave the organization."]]
]], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestDisbandOrg), {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith {
diag_log "[FORGE:Server:Org] Empty/Invalid UID for disband request!"
};
private _requester = [_uid] call EFUNC(common,getPlayer);
if (_requester isEqualTo objNull) exitWith {};
private _result = GVAR(OrgStore) call ["disband", [_uid]];
if !(_result getOrDefault ["success", false]) exitWith {
[CRPC(org,responseDisbandOrg), [createHashMapFromArray [
["success", false],
["message", _result getOrDefault ["message", "Failed to disband organization."]],
["requester", true]
]], _requester] call CFUNC(targetEvent);
};
{
[_x, _result] call {
params [["_member", createHashMap, [createHashMap]], ["_disbandResult", createHashMap, [createHashMap]]];
private _memberUid = _member getOrDefault ["uid", ""];
if (_memberUid isEqualTo "") exitWith {};
private _memberPlayer = [_memberUid] call EFUNC(common,getPlayer);
if (_memberPlayer isEqualTo objNull) exitWith {};
private _actorPatch = _member getOrDefault ["actorPatch", createHashMap];
if (_actorPatch isNotEqualTo createHashMap) then {
[CRPC(actor,responseSyncActor), [_actorPatch], _memberPlayer] call CFUNC(targetEvent);
};
GVAR(OrgStore) call ["init", [_memberUid]];
[CRPC(org,responseDisbandOrg), [createHashMapFromArray [
["success", true],
["message", _member getOrDefault ["message", _disbandResult getOrDefault ["message", "Organization disbanded."]]],
["requester", _member getOrDefault ["requester", false]]
]], _memberPlayer] call CFUNC(targetEvent);
private _notificationParams = _member getOrDefault ["notification", []];
if (_notificationParams isEqualType [] && { count _notificationParams > 0 }) then {
[CRPC(notifications,recieveNotification), _notificationParams, _memberPlayer] call CFUNC(targetEvent);
};
};
} forEach (_result getOrDefault ["members", []]);
}] call CFUNC(addEventHandler);

View File

@ -140,13 +140,281 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_org
}],
["loadById", compileFinal {
params [["_orgID", "", [""]]];
if (_orgID isEqualTo "") exitWith { createHashMap };
private _cached = GVAR(Registry) getOrDefault [_orgID, createHashMap];
if (_cached isNotEqualTo createHashMap) exitWith { _cached };
["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_existsResult", "_existsSuccess"];
if (!_existsSuccess || { _existsResult isNotEqualTo "true" }) exitWith { createHashMap };
private _org = _self call ["fetch", ["org:get", _orgID]];
if (_org isEqualTo createHashMap) exitWith { _org };
private _memberRows = _self call ["fetch", ["org:members:get", _orgID]];
if !(_memberRows isEqualType []) then {
_memberRows = [];
};
private _memberMap = createHashMap;
{
private _memberUid = _x getOrDefault ["uid", ""];
if (_memberUid isNotEqualTo "") then {
_memberMap set [_memberUid, _x];
};
} forEach _memberRows;
_org set ["members", _memberMap];
GVAR(Registry) set [_orgID, _org, true];
_org
}],
["addMember", compileFinal {
params [
["_orgID", "", [""]],
["_uid", "", [""]],
["_player", objNull, [objNull]],
["_actor", createHashMap, [createHashMap]]
];
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap };
private _org = _self call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith { _org };
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
GVAR(Registry) set [_orgID, _org, true];
_org
}],
["removeMember", compileFinal {
params [
["_orgID", "", [""]],
["_uid", "", [""]]
];
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap };
private _org = _self call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith { _org };
["org:members:remove", [_orgID, _uid]] call EFUNC(extension,extCall) params ["_memberResult", "_memberSuccess"];
if (!_memberSuccess) exitWith {
["WARNING", format ["Failed to remove %1 from org %2 members: %3", _uid, _orgID, _memberResult]] call EFUNC(common,log);
createHashMap
};
private _members = +(_org getOrDefault ["members", createHashMap]);
_members deleteAt _uid;
_org set ["members", _members];
GVAR(Registry) set [_orgID, _org, true];
_org
}],
["delete", compileFinal {
params [["_orgID", "", [""]]];
private _result = createHashMapFromArray [
["success", false],
["message", ""]
];
if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith {
_result set ["message", "Invalid organization ID."];
_result
};
["org:delete", [_orgID]] call EFUNC(extension,extCall) params ["_deleteResult", "_deleteSuccess"];
if (!_deleteSuccess || { _deleteResult isNotEqualTo "OK" }) exitWith {
_result set ["message", format ["Failed to delete organization: %1", _deleteResult]];
_result
};
GVAR(Registry) deleteAt _orgID;
_result set ["success", true];
_result
}],
["restoreDefaultMembership", compileFinal {
params [
["_uid", "", [""]],
["_player", objNull, [objNull]],
["_actor", createHashMap, [createHashMap]]
];
private _result = createHashMapFromArray [
["success", false],
["message", ""],
["actorPatch", createHashMap]
];
if (_uid isEqualTo "") exitWith {
_result set ["message", "A valid player UID is required."];
_result
};
private _resolvedPlayer = _player;
if (_resolvedPlayer isEqualTo objNull) then {
_resolvedPlayer = [_uid] call EFUNC(common,getPlayer);
};
private _resolvedActor = EGVAR(actor,Registry) getOrDefault [_uid, _actor];
private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", "default", true]];
private _defaultOrg = _self call ["addMember", ["default", _uid, _resolvedPlayer, EGVAR(actor,Registry) getOrDefault [_uid, _resolvedActor]]];
if (_defaultOrg isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to restore default organization membership."];
_result
};
GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", "default"]]];
_result set ["success", true];
_result set ["actorPatch", _actorPatch];
_result
}],
["leave", compileFinal {
params [["_uid", "", [""]]];
private _result = createHashMapFromArray [
["success", false],
["message", ""],
["actorPatch", createHashMap],
["notification", []]
];
if (_uid isEqualTo "") exitWith {
_result set ["message", "A valid player UID is required."];
_result
};
private _player = [_uid] call EFUNC(common,getPlayer);
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _orgID = _actor getOrDefault ["organization", ""];
if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith {
_result set ["message", "You are already assigned to the default organization."];
_result
};
private _org = _self call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith {
_result set ["message", "Unable to load organization data for leave request."];
_result
};
private _ownerUid = _org getOrDefault ["owner", ""];
if (_ownerUid isEqualTo _uid) exitWith {
_result set ["message", "Organization owners must disband the organization instead of leaving it."];
_result
};
private _orgName = _org getOrDefault ["name", "Organization"];
private _updatedOrg = _self call ["removeMember", [_orgID, _uid]];
if (_updatedOrg isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to remove you from the organization roster."];
_result
};
private _defaultResult = _self call ["restoreDefaultMembership", [_uid, _player, _actor]];
if !(_defaultResult getOrDefault ["success", false]) exitWith {
_result set ["message", _defaultResult getOrDefault ["message", "Failed to restore default organization membership."]];
_result
};
private _message = format ["You left %1 and returned to the default organization.", _orgName];
_result set ["success", true];
_result set ["message", _message];
_result set ["actorPatch", _defaultResult getOrDefault ["actorPatch", createHashMap]];
_result set ["notification", ["info", "Organization Left", _message, 6000]];
_result
}],
["disband", compileFinal {
params [["_uid", "", [""]]];
private _result = createHashMapFromArray [
["success", false],
["message", ""],
["members", []]
];
if (_uid isEqualTo "") exitWith {
_result set ["message", "A valid player UID is required."];
_result
};
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _orgID = _actor getOrDefault ["organization", ""];
if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith {
_result set ["message", "Only active player organizations can be disbanded."];
_result
};
private _org = _self call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith {
_result set ["message", "Unable to load organization data for disbanding."];
_result
};
private _ownerUid = _org getOrDefault ["owner", ""];
if (_ownerUid isEqualTo "" || { _ownerUid isNotEqualTo _uid }) exitWith {
_result set ["message", "Only the organization owner can disband this organization."];
_result
};
private _orgName = _org getOrDefault ["name", "Organization"];
private _memberMap = _org getOrDefault ["members", createHashMap];
private _memberUids = keys _memberMap;
if !(_uid in _memberUids) then {
_memberUids pushBack _uid;
};
private _deleteResult = _self call ["delete", [_orgID]];
if !(_deleteResult getOrDefault ["success", false]) exitWith {
_result set ["message", _deleteResult getOrDefault ["message", "Failed to disband organization."]];
_result
};
private _memberResults = [];
{
private _memberUid = _x;
if (_memberUid isNotEqualTo "") then {
private _memberPlayer = [_memberUid] call EFUNC(common,getPlayer);
private _defaultResult = _self call ["restoreDefaultMembership", [_memberUid, _memberPlayer, EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap]]];
if !(_defaultResult getOrDefault ["success", false]) then {
["WARNING", format ["Failed to restore default org for %1 after disbanding %2: %3", _memberUid, _orgID, _defaultResult getOrDefault ["message", "Unknown error."]]] call EFUNC(common,log);
};
private _responseMessage = [
format ["%1 has been disbanded.", _orgName],
format ["Your organization, %1, has been disbanded.", _orgName]
] select (_memberUid isEqualTo _uid);
private _notificationParams = [
["warning", "Organization Disbanded", _responseMessage, 6000],
["success", "Organization Disbanded", _responseMessage, 6000]
] select (_memberUid isEqualTo _uid);
_memberResults pushBack (createHashMapFromArray [
["uid", _memberUid],
["requester", _memberUid isEqualTo _uid],
["message", _responseMessage],
["notification", _notificationParams],
["actorPatch", _defaultResult getOrDefault ["actorPatch", createHashMap]]
]);
};
} forEach _memberUids;
_result set ["success", true];
_result set ["message", format ["%1 has been disbanded.", _orgName]];
_result set ["members", _memberResults];
_result
}],
["register", compileFinal {
params [["_uid", "", [""]], ["_orgName", "", [""]]];
private _result = createHashMapFromArray [
["success", false],
["message", ""],
["org", createHashMap]
["org", createHashMap],
["actorPatch", createHashMap]
];
if (_uid isEqualTo "" || { _orgName isEqualTo "" }) exitWith {
@ -158,7 +426,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _existingOrgID = _actor getOrDefault ["organization", ""];
if (_existingOrgID isNotEqualTo "") exitWith {
if (_existingOrgID isNotEqualTo "" && { toLower _existingOrgID isNotEqualTo "default" }) exitWith {
_result set ["message", "Player already belongs to an organization."];
_result
};
@ -205,21 +473,20 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_org set ["members", createHashMap];
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
["org:members:remove", ["default", _uid]] call EFUNC(extension,extCall);
private _defaultOrg = GVAR(Registry) getOrDefault ["default", createHashMap];
if (_defaultOrg isNotEqualTo createHashMap) then {
private _defaultMembers = _defaultOrg getOrDefault ["members", createHashMap];
_defaultMembers deleteAt _uid;
_defaultOrg set ["members", _defaultMembers];
GVAR(Registry) set ["default", _defaultOrg, true];
if (toLower _existingOrgID isEqualTo "default") then {
private _defaultOrg = _self call ["removeMember", ["default", _uid]];
if (_defaultOrg isEqualTo createHashMap) then {
["WARNING", format ["Failed to remove %1 from default org members after creating org %2.", _uid, _orgID]] call EFUNC(common,log);
};
};
private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, true]];
GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]];
GVAR(Registry) set [_orgID, _org, true];
_result set ["success", true];
_result set ["org", _org];
_result set ["actorPatch", _actorPatch];
_result
}],
["init", compileFinal {
@ -258,12 +525,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
};
private _finalOrg = createHashMap;
private _finalAssets = createHashMap;
private _finalFleet = createHashMap;
private _finalMembers = createHashMap;
if (_result == "true") then {
_finalOrg = _self call ["fetch", ["org:get", _orgID]];
_finalOrg = _self call ["loadById", [_orgID]];
["INFO", format ["Found org for %1", _orgID]] call EFUNC(common,log);
} else {
["WARNING", format ["No existing org found for %1, using default org.", _uid]] call EFUNC(common,log);
@ -274,26 +538,6 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
private _entry = createHashMapFromArray [["orgID", _orgID]];
GVAR(IndexRegistry) set [_uid, _entry];
// private _assets = _self call ["fetch", ["org:assets:get", _orgID]];
// private _fleet = _self call ["fetch", ["org:fleet:get", _orgID]];
private _members = _self call ["fetch", ["org:members:get", _orgID]];
{
private _key = _x get "uid";
private _value = _x;
_finalMembers set [_key, _value];
} forEach _members;
// {
// private _key = _x get "classname";
// private _value = _x;
// _finalAssets set [_key, _value];
// } forEach _assets;
_finalOrg set ["assets", _finalAssets];
_finalOrg set ["fleet", _finalFleet];
_finalOrg set ["members", _finalMembers];
private _finalOwner = _finalOrg getOrDefault ["owner", ""];
if (_orgID isEqualTo "default" || { _finalOwner isEqualTo _uid }) then {
_finalOrg = _self call ["verifyMember", [_finalOrg, _orgID, _uid, _player, _actor]];

View File

@ -147,15 +147,21 @@ impl<C: RedisClient> OrgRepository for RedisOrgRepository<C> {
/// Permanently deletes an organization and all associated data from Redis.
///
/// Removes the organization hash and the associated members list.
/// Removes the organization hash and related subordinate keys.
/// This operation is irreversible.
fn delete(&self, id: &str) -> Result<(), String> {
// Generate Redis key using organization ID
let redis_key = format!("org:{}", id);
let redis_keys = [
format!("org:{}", id),
format!("org:{}:members", id),
format!("org:{}:assets", id),
format!("org:{}:fleet", id),
];
// Delete the organization hash key from Redis
// Note: This does NOT delete member data stored separately
self.client.delete_key(redis_key)
for redis_key in redis_keys {
self.client.delete_key(redis_key)?;
}
Ok(())
}
/// Checks if an organization exists in Redis without retrieving the data.

View File

@ -163,16 +163,7 @@ impl<R: OrgRepository> OrgService<R> {
///
/// Irreversible operation. Delegates to repository.
pub fn delete_org(&self, key: String) -> Result<(), String> {
let redis_key = format!("org:{}", key);
let assets_key = format!("org:{}:assets", key);
let members_key = format!("org:{}:members", key);
// Delegate deletion to repository layer
self.repository.delete(&redis_key)?;
self.repository.delete(&assets_key)?;
self.repository.delete(&members_key)?;
Ok(())
self.repository.delete(&key)
}
/// Checks if an organization exists in the system.