FIX - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: АДМИНКА РОЛЕЙ TASKER
This commit is contained in:
parent
d4eba0ff3a
commit
5f461d57ea
|
|
@ -14,7 +14,14 @@
|
||||||
"contactEmail": "dcctouch@gmail.com",
|
"contactEmail": "dcctouch@gmail.com",
|
||||||
"notes": "Live-клиент NODE.DC для первичной проверки control-plane, SSO и доступа к сервисам.",
|
"notes": "Live-клиент NODE.DC для первичной проверки control-plane, SSO и доступа к сервисам.",
|
||||||
"createdAt": "2026-05-04T00:00:00.000Z",
|
"createdAt": "2026-05-04T00:00:00.000Z",
|
||||||
"updatedAt": "2026-05-04T12:55:13.842Z"
|
"updatedAt": "2026-05-06T08:44:44.882Z",
|
||||||
|
"integrations": {
|
||||||
|
"taskManager": {
|
||||||
|
"workspaceSlug": "nodedc",
|
||||||
|
"workspaceName": "NODE DC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inn": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"users": [
|
"users": [
|
||||||
|
|
@ -491,10 +498,10 @@
|
||||||
"objectName": "DCTOUCH",
|
"objectName": "DCTOUCH",
|
||||||
"objectType": "client",
|
"objectType": "client",
|
||||||
"target": "authentik",
|
"target": "authentik",
|
||||||
"state": "synced",
|
"state": "pending",
|
||||||
"lastSyncAt": "2026-05-04T12:55:13.842Z",
|
"lastSyncAt": "2026-05-04T12:55:13.842Z",
|
||||||
"error": null,
|
"error": null,
|
||||||
"updatedAt": "2026-05-04T12:55:13.842Z"
|
"updatedAt": "2026-05-06T08:44:44.887Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sync_dc_touch_authentik",
|
"id": "sync_dc_touch_authentik",
|
||||||
|
|
@ -1964,6 +1971,198 @@
|
||||||
"clientId": null,
|
"clientId": null,
|
||||||
"result": "success",
|
"result": "success",
|
||||||
"details": "Logo link: http://launcher.local.nodedc/; Tasker workspace policy: task_admins_only"
|
"details": "Logo link: http://launcher.local.nodedc/; Tasker workspace policy: task_admins_only"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dctouch",
|
||||||
|
"at": "2026-05-06T08:44:44.887Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Обновлён клиент",
|
||||||
|
"objectType": "client",
|
||||||
|
"objectName": "DCTOUCH",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_constr",
|
||||||
|
"at": "2026-05-06T09:02:42.183Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC CONSTR",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: 15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_constr_2",
|
||||||
|
"at": "2026-05-06T09:02:45.197Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC CONSTR",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: 15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_constr_3",
|
||||||
|
"at": "2026-05-06T09:02:57.971Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC CONSTR",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: 15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_constr_4",
|
||||||
|
"at": "2026-05-06T09:02:59.293Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC CONSTR",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: 15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_constr_5",
|
||||||
|
"at": "2026-05-06T09:09:09.389Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC CONSTR",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: 15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_silver",
|
||||||
|
"at": "2026-05-06T09:46:34.612Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC SILVER",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_constr_6",
|
||||||
|
"at": "2026-05-06T09:46:41.427Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC CONSTR",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: member"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_sudo",
|
||||||
|
"at": "2026-05-06T10:17:58.710Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC SUDO",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_sudo_2",
|
||||||
|
"at": "2026-05-06T10:21:44.717Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Снят Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC SUDO",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: nodedc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_support",
|
||||||
|
"at": "2026-05-06T10:38:27.410Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC SUPPORT",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: member"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_silver_2",
|
||||||
|
"at": "2026-05-06T10:51:20.914Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC SILVER",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: member"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_sudo_3",
|
||||||
|
"at": "2026-05-06T10:54:33.543Z",
|
||||||
|
"actorUserId": "system",
|
||||||
|
"actorName": "NODE.DC System",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC SUDO",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_silver007",
|
||||||
|
"at": "2026-05-06T11:20:45.826Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC SILVER007",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: member"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_abramov",
|
||||||
|
"at": "2026-05-06T11:20:47.255Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC ABRAMOV",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: member"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audit_dc_constrictions",
|
||||||
|
"at": "2026-05-06T11:20:48.841Z",
|
||||||
|
"actorUserId": "user_root",
|
||||||
|
"actorName": "DC SUDO",
|
||||||
|
"action": "Назначен Tasker workspace",
|
||||||
|
"objectType": "task-manager-membership",
|
||||||
|
"objectName": "DC CONSTRICTIONS",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"result": "success",
|
||||||
|
"details": "Workspace: NODE DC; Role: member"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|
@ -1973,5 +2172,84 @@
|
||||||
"taskManager": {
|
"taskManager": {
|
||||||
"workspaceCreationPolicy": "task_admins_only"
|
"workspaceCreationPolicy": "task_admins_only"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"taskManagerMemberships": [
|
||||||
|
{
|
||||||
|
"id": "tasker_mem_client_romashka_user_silver_psih_nodedc",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"userId": "user_silver_psih",
|
||||||
|
"workspaceSlug": "nodedc",
|
||||||
|
"workspaceName": "NODE DC",
|
||||||
|
"role": "member",
|
||||||
|
"planeUserId": "7315d59a-50e1-4d26-8de8-ae632777b46e",
|
||||||
|
"planeRole": 15,
|
||||||
|
"updatedAt": "2026-05-06T10:51:20.911Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tasker_mem_client_romashka_user_constr_dc_yahoo_com_nodedc",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"userId": "user_constr_dc_yahoo_com",
|
||||||
|
"workspaceSlug": "nodedc",
|
||||||
|
"workspaceName": "NODE DC",
|
||||||
|
"role": "member",
|
||||||
|
"planeUserId": "62aa744d-32ef-427f-8ad2-184d2ec2f5e5",
|
||||||
|
"planeRole": 15,
|
||||||
|
"updatedAt": "2026-05-06T09:46:41.427Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tasker_mem_client_romashka_user_support_dctouch_ru_nodedc",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"userId": "user_support_dctouch_ru",
|
||||||
|
"workspaceSlug": "nodedc",
|
||||||
|
"workspaceName": "NODE DC",
|
||||||
|
"role": "member",
|
||||||
|
"planeUserId": "53e56870-8772-4dfd-9c8c-2eeacfb53caa",
|
||||||
|
"planeRole": 15,
|
||||||
|
"updatedAt": "2026-05-06T10:38:27.409Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tasker_mem_client_romashka_user_root_nodedc",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"userId": "user_root",
|
||||||
|
"workspaceSlug": "nodedc",
|
||||||
|
"workspaceName": "NODE DC",
|
||||||
|
"role": "admin",
|
||||||
|
"planeUserId": "844d7f18-285d-4671-8371-8ca9ca5ffa39",
|
||||||
|
"planeRole": 20,
|
||||||
|
"updatedAt": "2026-05-06T10:54:33.542Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tasker_mem_client_romashka_user_silverpsih007_gmail_com_nodedc",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"userId": "user_silverpsih007_gmail_com",
|
||||||
|
"workspaceSlug": "nodedc",
|
||||||
|
"workspaceName": "NODE DC",
|
||||||
|
"role": "member",
|
||||||
|
"planeUserId": "52817493-1ff4-44f9-aae4-463ecd512d51",
|
||||||
|
"planeRole": 15,
|
||||||
|
"updatedAt": "2026-05-06T11:20:45.826Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tasker_mem_client_romashka_user_abramov_dcconstructions_ru_nodedc",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"userId": "user_abramov_dcconstructions_ru",
|
||||||
|
"workspaceSlug": "nodedc",
|
||||||
|
"workspaceName": "NODE DC",
|
||||||
|
"role": "member",
|
||||||
|
"planeUserId": "d28a2d28-da56-4625-a211-d9bb3d06b0d3",
|
||||||
|
"planeRole": 15,
|
||||||
|
"updatedAt": "2026-05-06T11:20:47.255Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tasker_mem_client_romashka_user_support_dcconstructions_ru_nodedc",
|
||||||
|
"clientId": "client_romashka",
|
||||||
|
"userId": "user_support_dcconstructions_ru",
|
||||||
|
"workspaceSlug": "nodedc",
|
||||||
|
"workspaceName": "NODE DC",
|
||||||
|
"role": "member",
|
||||||
|
"planeUserId": "1cc7ae3a-1f42-41ac-8cc2-1ff0fce59554",
|
||||||
|
"planeRole": 15,
|
||||||
|
"updatedAt": "2026-05-06T11:20:48.841Z"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ const collectionKeys = [
|
||||||
"invites",
|
"invites",
|
||||||
"syncStatuses",
|
"syncStatuses",
|
||||||
"auditEvents",
|
"auditEvents",
|
||||||
|
"taskManagerMemberships",
|
||||||
];
|
];
|
||||||
|
|
||||||
const clientTypes = new Set(["company", "person"]);
|
const clientTypes = new Set(["company", "person"]);
|
||||||
|
|
@ -167,13 +168,47 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||||
const membership = typeof taskManager.membership === "object" && taskManager.membership !== null ? taskManager.membership : {};
|
const membership = typeof taskManager.membership === "object" && taskManager.membership !== null ? taskManager.membership : {};
|
||||||
const workspace = typeof membership.workspace === "object" && membership.workspace !== null ? membership.workspace : {};
|
const workspace = typeof membership.workspace === "object" && membership.workspace !== null ? membership.workspace : {};
|
||||||
|
|
||||||
|
upsertTaskManagerMembership(data, {
|
||||||
|
clientId: client.id,
|
||||||
|
userId: user.id,
|
||||||
|
workspaceSlug: workspace.slug ?? payload?.workspaceSlug,
|
||||||
|
workspaceName: workspace.name ?? payload?.workspaceName ?? null,
|
||||||
|
role: normalizeTaskManagerMembershipRole(payload?.role),
|
||||||
|
planeUserId: membership.member?.id ?? null,
|
||||||
|
planeRole: typeof membership.role === "number" ? membership.role : null,
|
||||||
|
});
|
||||||
|
|
||||||
addAuditEvent(data, actor, {
|
addAuditEvent(data, actor, {
|
||||||
action: "Назначен Tasker workspace",
|
action: "Назначен Tasker workspace",
|
||||||
objectType: "task-manager-membership",
|
objectType: "task-manager-membership",
|
||||||
objectName: user.name,
|
objectName: user.name,
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
result: "success",
|
result: "success",
|
||||||
details: `Workspace: ${workspace.name ?? workspace.slug ?? payload?.workspaceSlug}; Role: ${membership.role ?? payload?.role ?? "member"}`,
|
details: `Workspace: ${workspace.name ?? workspace.slug ?? payload?.workspaceSlug}; Role: ${payload?.role ?? "member"}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await writeData(data);
|
||||||
|
return { data };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTaskManagerWorkspaceMembership(payload, identity) {
|
||||||
|
const data = readData();
|
||||||
|
const actor = resolveActor(data, identity);
|
||||||
|
const client = findById(data.clients, payload?.clientId, "client");
|
||||||
|
const user = findById(data.users, payload?.userId, "user");
|
||||||
|
const workspaceSlug = requireString(payload?.workspaceSlug ?? client.integrations?.taskManager?.workspaceSlug, "workspaceSlug");
|
||||||
|
|
||||||
|
data.taskManagerMemberships = data.taskManagerMemberships.filter(
|
||||||
|
(membership) => !(membership.clientId === client.id && membership.userId === user.id && membership.workspaceSlug === workspaceSlug)
|
||||||
|
);
|
||||||
|
|
||||||
|
addAuditEvent(data, actor, {
|
||||||
|
action: "Снят Tasker workspace",
|
||||||
|
objectType: "task-manager-membership",
|
||||||
|
objectName: user.name,
|
||||||
|
clientId: client.id,
|
||||||
|
result: "success",
|
||||||
|
details: `Workspace: ${workspaceSlug}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await writeData(data);
|
await writeData(data);
|
||||||
|
|
@ -1055,6 +1090,7 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||||
retrySync,
|
retrySync,
|
||||||
markUserAuthentikProvisioned,
|
markUserAuthentikProvisioned,
|
||||||
recordTaskManagerWorkspaceMembership,
|
recordTaskManagerWorkspaceMembership,
|
||||||
|
removeTaskManagerWorkspaceMembership,
|
||||||
setUserServiceAccess,
|
setUserServiceAccess,
|
||||||
updateClient,
|
updateClient,
|
||||||
updateGroup,
|
updateGroup,
|
||||||
|
|
@ -1120,6 +1156,36 @@ function normalizeClientIntegrations(payload, fallback = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeTaskManagerMembershipRole(value) {
|
||||||
|
return value === "guest" || value === "admin" || value === "member" ? value : "member";
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertTaskManagerMembership(data, payload) {
|
||||||
|
const workspaceSlug = requireString(payload.workspaceSlug, "workspaceSlug");
|
||||||
|
const existingMembership = data.taskManagerMemberships.find(
|
||||||
|
(membership) => membership.clientId === payload.clientId && membership.userId === payload.userId && membership.workspaceSlug === workspaceSlug
|
||||||
|
);
|
||||||
|
const nextMembership = {
|
||||||
|
id: existingMembership?.id ?? uniqueId(data.taskManagerMemberships, "tasker_mem", `${payload.clientId}-${payload.userId}-${workspaceSlug}`),
|
||||||
|
clientId: payload.clientId,
|
||||||
|
userId: payload.userId,
|
||||||
|
workspaceSlug,
|
||||||
|
workspaceName: nullableStringWithFallback(payload.workspaceName, existingMembership?.workspaceName ?? null),
|
||||||
|
role: normalizeTaskManagerMembershipRole(payload.role),
|
||||||
|
planeUserId: nullableStringWithFallback(payload.planeUserId, existingMembership?.planeUserId ?? null),
|
||||||
|
planeRole: typeof payload.planeRole === "number" ? payload.planeRole : (existingMembership?.planeRole ?? null),
|
||||||
|
updatedAt: isoNow(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingMembership) {
|
||||||
|
Object.assign(existingMembership, nextMembership);
|
||||||
|
return existingMembership;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.taskManagerMemberships.push(nextMembership);
|
||||||
|
return nextMembership;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveActor(data, identity) {
|
function resolveActor(data, identity) {
|
||||||
const user = data.users.find(
|
const user = data.users.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
|
|
|
||||||
|
|
@ -301,7 +301,7 @@ app.post("/api/internal/handoff/consume", (req, res) => {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
avatarUrl: user.avatarUrl ?? null,
|
avatarUrl: resolveUserAvatarPublicUrl(user),
|
||||||
subject: user.authentikUserId || handoff.user.sub,
|
subject: user.authentikUserId || handoff.user.sub,
|
||||||
authentikUserId: user.authentikUserId ?? null,
|
authentikUserId: user.authentikUserId ?? null,
|
||||||
groups,
|
groups,
|
||||||
|
|
@ -557,6 +557,7 @@ app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncher
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
subject: user.authentikUserId ?? undefined,
|
subject: user.authentikUserId ?? undefined,
|
||||||
|
avatarUrl: resolveUserAvatarPublicUrl(user),
|
||||||
role,
|
role,
|
||||||
companyRole: membership?.role ?? null,
|
companyRole: membership?.role ?? null,
|
||||||
setLastWorkspace: req.body?.setLastWorkspace !== false,
|
setLastWorkspace: req.body?.setLastWorkspace !== false,
|
||||||
|
|
@ -578,6 +579,81 @@ app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncher
|
||||||
res.json({ ...result, taskManager });
|
res.json({ ...result, taskManager });
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
app.post("/api/admin/task-manager/workspace-memberships/remove", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||||
|
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||||
|
const clientId = typeof req.body?.clientId === "string" ? req.body.clientId : "";
|
||||||
|
const userId = typeof req.body?.userId === "string" ? req.body.userId : "";
|
||||||
|
const client = snapshot.data.clients.find((candidate) => candidate.id === clientId);
|
||||||
|
const user = snapshot.data.users.find((candidate) => candidate.id === userId);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
res.status(404).json({ ok: false, error: "client_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({ ok: false, error: "user_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const membership = snapshot.data.memberships.find((candidate) => candidate.clientId === client.id && candidate.userId === user.id);
|
||||||
|
const workspaceSlug = client.integrations?.taskManager?.workspaceSlug ?? null;
|
||||||
|
|
||||||
|
if (!workspaceSlug) {
|
||||||
|
res.status(400).json({ ok: false, error: "task_manager_workspace_not_configured" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membership?.role === "client_owner") {
|
||||||
|
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspace-memberships/ensure/", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
workspaceSlug,
|
||||||
|
email: user.email,
|
||||||
|
subject: user.authentikUserId ?? undefined,
|
||||||
|
avatarUrl: resolveUserAvatarPublicUrl(user),
|
||||||
|
role: "admin",
|
||||||
|
companyRole: membership.role,
|
||||||
|
setLastWorkspace: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await controlPlaneStore.recordTaskManagerWorkspaceMembership(
|
||||||
|
{
|
||||||
|
clientId: client.id,
|
||||||
|
userId: user.id,
|
||||||
|
workspaceSlug,
|
||||||
|
role: "admin",
|
||||||
|
taskManager,
|
||||||
|
},
|
||||||
|
req.nodedcSession.user
|
||||||
|
);
|
||||||
|
|
||||||
|
publishControlPlaneEvent("admin.task-manager.workspace-membership.updated", [user.id]);
|
||||||
|
res.json({ ...result, taskManager, protected: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspace-memberships/remove/", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
workspaceSlug,
|
||||||
|
email: user.email,
|
||||||
|
subject: user.authentikUserId ?? undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await controlPlaneStore.removeTaskManagerWorkspaceMembership(
|
||||||
|
{
|
||||||
|
clientId: client.id,
|
||||||
|
userId: user.id,
|
||||||
|
workspaceSlug,
|
||||||
|
},
|
||||||
|
req.nodedcSession.user
|
||||||
|
);
|
||||||
|
|
||||||
|
publishControlPlaneEvent("admin.task-manager.workspace-membership.updated", [user.id]);
|
||||||
|
res.json({ ...result, taskManager });
|
||||||
|
}));
|
||||||
|
|
||||||
app.post("/api/admin/clients", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
app.post("/api/admin/clients", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||||
const result = await controlPlaneStore.createClient(req.body, req.nodedcSession.user);
|
const result = await controlPlaneStore.createClient(req.body, req.nodedcSession.user);
|
||||||
res.status(201).json(result);
|
res.status(201).json(result);
|
||||||
|
|
@ -1289,7 +1365,7 @@ function normalizeOptionalText(value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTaskManagerRole(value) {
|
function normalizeTaskManagerRole(value) {
|
||||||
return value === "admin" || value === "member" ? value : null;
|
return value === "guest" || value === "admin" || value === "member" ? value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTaskManagerRoleForMembership(role) {
|
function resolveTaskManagerRoleForMembership(role) {
|
||||||
|
|
@ -1598,6 +1674,11 @@ function resolvePublicUrl(value, baseUrl) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveUserAvatarPublicUrl(user) {
|
||||||
|
if (!user?.avatarUrl) return null;
|
||||||
|
return resolvePublicUrl(user.avatarUrl, config.appBaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
async function saveUploadedFile(payload) {
|
async function saveUploadedFile(payload) {
|
||||||
if (!isUploadPayload(payload)) {
|
if (!isUploadPayload(payload)) {
|
||||||
throw new Error("Некорректный payload загрузки");
|
throw new Error("Некорректный payload загрузки");
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
fetchControlPlaneSnapshot,
|
fetchControlPlaneSnapshot,
|
||||||
reorderAdminServices,
|
reorderAdminServices,
|
||||||
retryAdminSync,
|
retryAdminSync,
|
||||||
|
removeAdminTaskManagerWorkspaceMembership,
|
||||||
setAdminUserServiceAccess,
|
setAdminUserServiceAccess,
|
||||||
updateAdminClient,
|
updateAdminClient,
|
||||||
updateAdminGroup,
|
updateAdminGroup,
|
||||||
|
|
@ -29,6 +30,7 @@ import {
|
||||||
updateAdminSettings,
|
updateAdminSettings,
|
||||||
updateAdminUserProfile,
|
updateAdminUserProfile,
|
||||||
type ControlPlaneMutationResult,
|
type ControlPlaneMutationResult,
|
||||||
|
type TaskManagerWorkspaceMemberRole,
|
||||||
type TaskManagerWorkspaceSummary,
|
type TaskManagerWorkspaceSummary,
|
||||||
} from "../shared/api/adminApi";
|
} from "../shared/api/adminApi";
|
||||||
import {
|
import {
|
||||||
|
|
@ -467,7 +469,7 @@ export function LauncherApp() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEnsureTaskManagerWorkspaceMember(command: { clientId: string; userId: string; role?: "member" | "admin" }) {
|
function handleSetTaskManagerWorkspaceMemberRole(command: { clientId: string; userId: string; role: TaskManagerWorkspaceMemberRole }) {
|
||||||
const membershipKey = `${command.clientId}:${command.userId}`;
|
const membershipKey = `${command.clientId}:${command.userId}`;
|
||||||
|
|
||||||
if (pendingTaskManagerMemberships[membershipKey]) {
|
if (pendingTaskManagerMemberships[membershipKey]) {
|
||||||
|
|
@ -475,12 +477,22 @@ export function LauncherApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setPendingTaskManagerMemberships((current) => ({ ...current, [membershipKey]: true }));
|
setPendingTaskManagerMemberships((current) => ({ ...current, [membershipKey]: true }));
|
||||||
ensureAdminTaskManagerWorkspaceMembership({ ...command, setLastWorkspace: true })
|
const request =
|
||||||
|
command.role === "unset"
|
||||||
|
? removeAdminTaskManagerWorkspaceMembership({ clientId: command.clientId, userId: command.userId })
|
||||||
|
: ensureAdminTaskManagerWorkspaceMembership({
|
||||||
|
clientId: command.clientId,
|
||||||
|
userId: command.userId,
|
||||||
|
role: command.role,
|
||||||
|
setLastWorkspace: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
request
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
setData(syncLauncherServiceLinks(result.data));
|
setData(syncLauncherServiceLinks(result.data));
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
console.warn(error instanceof Error ? error.message : "Не удалось назначить workspace Operational Core");
|
console.warn(error instanceof Error ? error.message : "Не удалось синхронизировать Tasker");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setPendingTaskManagerMemberships((current) => {
|
setPendingTaskManagerMemberships((current) => {
|
||||||
|
|
@ -737,7 +749,7 @@ export function LauncherApp() {
|
||||||
taskManagerWorkspacesError={taskManagerWorkspacesError}
|
taskManagerWorkspacesError={taskManagerWorkspacesError}
|
||||||
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
|
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
|
||||||
onRefreshTaskManagerWorkspaces={() => void refreshTaskManagerWorkspaces()}
|
onRefreshTaskManagerWorkspaces={() => void refreshTaskManagerWorkspaces()}
|
||||||
onEnsureTaskManagerWorkspaceMember={handleEnsureTaskManagerWorkspaceMember}
|
onSetTaskManagerWorkspaceMemberRole={handleSetTaskManagerWorkspaceMemberRole}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{profileSettingsOpen && activeProfileUser ? (
|
{profileSettingsOpen && activeProfileUser ? (
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ export interface TaskManagerWorkspaceMembershipResult {
|
||||||
isBanned: boolean;
|
isBanned: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TaskManagerWorkspaceMemberRole = "unset" | "guest" | "member" | "admin";
|
||||||
|
|
||||||
export interface TaskManagerWorkspaceMembershipMutationResult extends ControlPlaneMutationResult {
|
export interface TaskManagerWorkspaceMembershipMutationResult extends ControlPlaneMutationResult {
|
||||||
taskManager: {
|
taskManager: {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
|
|
@ -59,6 +61,19 @@ export interface TaskManagerWorkspaceMembershipMutationResult extends ControlPla
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TaskManagerWorkspaceMembershipRemoveMutationResult extends ControlPlaneMutationResult {
|
||||||
|
taskManager: {
|
||||||
|
ok: boolean;
|
||||||
|
removed: boolean;
|
||||||
|
workspace: TaskManagerWorkspaceSummary;
|
||||||
|
member: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
displayName: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchControlPlaneSnapshot(): Promise<ControlPlaneSnapshot> {
|
export async function fetchControlPlaneSnapshot(): Promise<ControlPlaneSnapshot> {
|
||||||
return requestJson<ControlPlaneSnapshot>("/api/admin/control-plane");
|
return requestJson<ControlPlaneSnapshot>("/api/admin/control-plane");
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +150,7 @@ export async function deleteAdminMembership(membershipId: string): Promise<Contr
|
||||||
export async function ensureAdminTaskManagerWorkspaceMembership(payload: {
|
export async function ensureAdminTaskManagerWorkspaceMembership(payload: {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
role?: "member" | "admin";
|
role?: Exclude<TaskManagerWorkspaceMemberRole, "unset">;
|
||||||
setLastWorkspace?: boolean;
|
setLastWorkspace?: boolean;
|
||||||
}): Promise<TaskManagerWorkspaceMembershipMutationResult> {
|
}): Promise<TaskManagerWorkspaceMembershipMutationResult> {
|
||||||
return requestJson<TaskManagerWorkspaceMembershipMutationResult>("/api/admin/task-manager/workspace-memberships/ensure", {
|
return requestJson<TaskManagerWorkspaceMembershipMutationResult>("/api/admin/task-manager/workspace-memberships/ensure", {
|
||||||
|
|
@ -144,6 +159,16 @@ export async function ensureAdminTaskManagerWorkspaceMembership(payload: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function removeAdminTaskManagerWorkspaceMembership(payload: {
|
||||||
|
clientId: string;
|
||||||
|
userId: string;
|
||||||
|
}): Promise<TaskManagerWorkspaceMembershipRemoveMutationResult> {
|
||||||
|
return requestJson<TaskManagerWorkspaceMembershipRemoveMutationResult>("/api/admin/task-manager/workspace-memberships/remove", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function createAdminGroup(payload: Pick<ClientGroup, "clientId"> & Partial<ClientGroup>): Promise<ControlPlaneMutationResult> {
|
export async function createAdminGroup(payload: Pick<ClientGroup, "clientId"> & Partial<ClientGroup>): Promise<ControlPlaneMutationResult> {
|
||||||
return requestJson<ControlPlaneMutationResult>("/api/admin/groups", {
|
return requestJson<ControlPlaneMutationResult>("/api/admin/groups", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,22 @@ export interface LauncherData {
|
||||||
invites: Invite[];
|
invites: Invite[];
|
||||||
syncStatuses: SyncStatus[];
|
syncStatuses: SyncStatus[];
|
||||||
auditEvents: typeof mockAuditEvents;
|
auditEvents: typeof mockAuditEvents;
|
||||||
|
taskManagerMemberships: TaskManagerMembershipAssignment[];
|
||||||
settings: LauncherSettings;
|
settings: LauncherSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TaskManagerMembershipAssignment {
|
||||||
|
id: string;
|
||||||
|
clientId: string;
|
||||||
|
userId: string;
|
||||||
|
workspaceSlug: string;
|
||||||
|
workspaceName?: string | null;
|
||||||
|
role: "guest" | "member" | "admin";
|
||||||
|
planeUserId?: string | null;
|
||||||
|
planeRole?: number | null;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LauncherSettings {
|
export interface LauncherSettings {
|
||||||
brand: {
|
brand: {
|
||||||
logoLinkUrl: string;
|
logoLinkUrl: string;
|
||||||
|
|
@ -160,6 +173,7 @@ export function normalizeLauncherData(data: Partial<LauncherData> | null | undef
|
||||||
invites: Array.isArray(payload.invites) ? payload.invites : mockInvites,
|
invites: Array.isArray(payload.invites) ? payload.invites : mockInvites,
|
||||||
syncStatuses: Array.isArray(payload.syncStatuses) ? payload.syncStatuses : mockSyncStatuses,
|
syncStatuses: Array.isArray(payload.syncStatuses) ? payload.syncStatuses : mockSyncStatuses,
|
||||||
auditEvents: Array.isArray(payload.auditEvents) ? payload.auditEvents : mockAuditEvents,
|
auditEvents: Array.isArray(payload.auditEvents) ? payload.auditEvents : mockAuditEvents,
|
||||||
|
taskManagerMemberships: Array.isArray(payload.taskManagerMemberships) ? payload.taskManagerMemberships : [],
|
||||||
settings: normalizeLauncherSettings(payload.settings),
|
settings: normalizeLauncherSettings(payload.settings),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export interface NodeDcSelectOption<T extends string> {
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
tone?: string;
|
tone?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NodeDcSelectTriggerApi<T extends string> {
|
interface NodeDcSelectTriggerApi<T extends string> {
|
||||||
|
|
@ -59,14 +60,15 @@ export function NodeDcSelect<T extends string>({
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const selectedOption = options.find((option) => option.value === value) ?? options[0];
|
const selectedOption = options.find((option) => option.value === value) ?? options[0];
|
||||||
const normalizedQuery = query.trim().toLowerCase();
|
const normalizedQuery = query.trim().toLowerCase();
|
||||||
|
const menuOptions = useMemo(() => options.filter((option) => !option.hidden), [options]);
|
||||||
const visibleOptions = useMemo(() => {
|
const visibleOptions = useMemo(() => {
|
||||||
if (!normalizedQuery) return options;
|
if (!normalizedQuery) return menuOptions;
|
||||||
|
|
||||||
return options.filter((option) => {
|
return menuOptions.filter((option) => {
|
||||||
const haystack = `${option.label} ${option.description ?? ""}`.toLowerCase();
|
const haystack = `${option.label} ${option.description ?? ""}`.toLowerCase();
|
||||||
return haystack.includes(normalizedQuery);
|
return haystack.includes(normalizedQuery);
|
||||||
});
|
});
|
||||||
}, [normalizedQuery, options]);
|
}, [menuOptions, normalizedQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeDcDropdown
|
<NodeDcDropdown
|
||||||
|
|
|
||||||
|
|
@ -2070,6 +2070,53 @@ code {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-data-table--users {
|
||||||
|
min-width: 82rem;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-data-table--users th,
|
||||||
|
.admin-data-table--users td {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-data-table--users th:nth-child(1),
|
||||||
|
.admin-data-table--users td:nth-child(1) {
|
||||||
|
width: 17rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-data-table--users th:nth-child(2),
|
||||||
|
.admin-data-table--users td:nth-child(2) {
|
||||||
|
width: 8.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-data-table--users th:nth-child(3),
|
||||||
|
.admin-data-table--users td:nth-child(3) {
|
||||||
|
width: 8.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-data-table--users th:nth-child(4),
|
||||||
|
.admin-data-table--users td:nth-child(4) {
|
||||||
|
width: 13rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-data-table--users th:nth-child(5),
|
||||||
|
.admin-data-table--users td:nth-child(5),
|
||||||
|
.admin-data-table--users th:nth-child(6),
|
||||||
|
.admin-data-table--users td:nth-child(6) {
|
||||||
|
width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-data-table--users th:nth-child(7),
|
||||||
|
.admin-data-table--users td:nth-child(7) {
|
||||||
|
width: 9.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-data-table--users th:nth-child(8),
|
||||||
|
.admin-data-table--users td:nth-child(8) {
|
||||||
|
width: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.table-toolbar {
|
.table-toolbar {
|
||||||
margin-bottom: 0.7rem;
|
margin-bottom: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
@ -2194,29 +2241,9 @@ code {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-inline-action {
|
|
||||||
display: inline-flex;
|
|
||||||
min-height: 2.1rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--launcher-radius-circle);
|
|
||||||
background: rgba(181, 255, 90, 0.92);
|
|
||||||
color: #050805;
|
|
||||||
padding: 0 0.85rem;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 850;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-inline-action:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-field-row {
|
.admin-field-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) 2.85rem;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ import {
|
||||||
type MeResponse,
|
type MeResponse,
|
||||||
type TaskManagerWorkspaceCreationPolicy,
|
type TaskManagerWorkspaceCreationPolicy,
|
||||||
} from "../../shared/api/mockApi";
|
} from "../../shared/api/mockApi";
|
||||||
import type { TaskManagerWorkspaceSummary } from "../../shared/api/adminApi";
|
import type { TaskManagerWorkspaceMemberRole, TaskManagerWorkspaceSummary } from "../../shared/api/adminApi";
|
||||||
import { uploadStorageFile } from "../../shared/api/storageApi";
|
import { uploadStorageFile } from "../../shared/api/storageApi";
|
||||||
import { cn } from "../../shared/lib/cn";
|
import { cn } from "../../shared/lib/cn";
|
||||||
import { formatDate, formatDateTime } from "../../shared/lib/format";
|
import { formatDate, formatDateTime } from "../../shared/lib/format";
|
||||||
|
|
@ -87,6 +87,7 @@ type AdminSection =
|
||||||
|
|
||||||
type AccessAssignmentRole = Exclude<ServiceAppRole, "owner">;
|
type AccessAssignmentRole = Exclude<ServiceAppRole, "owner">;
|
||||||
export type AccessAssignmentValue = AccessAssignmentRole | "deny" | "unset";
|
export type AccessAssignmentValue = AccessAssignmentRole | "deny" | "unset";
|
||||||
|
type TaskManagerRoleSelectValue = TaskManagerWorkspaceMemberRole | "pending";
|
||||||
|
|
||||||
export interface SetUserServiceAccessCommand {
|
export interface SetUserServiceAccessCommand {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
@ -107,7 +108,7 @@ export interface CreateUserCommand {
|
||||||
export interface EnsureTaskManagerWorkspaceMemberCommand {
|
export interface EnsureTaskManagerWorkspaceMemberCommand {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
role?: "member" | "admin";
|
role: TaskManagerWorkspaceMemberRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootSections: Array<{ id: AdminSection; label: string; icon: React.ReactNode }> = [
|
const rootSections: Array<{ id: AdminSection; label: string; icon: React.ReactNode }> = [
|
||||||
|
|
@ -164,7 +165,7 @@ export function AdminOverlay({
|
||||||
taskManagerWorkspacesError,
|
taskManagerWorkspacesError,
|
||||||
pendingTaskManagerMemberships,
|
pendingTaskManagerMemberships,
|
||||||
onRefreshTaskManagerWorkspaces,
|
onRefreshTaskManagerWorkspaces,
|
||||||
onEnsureTaskManagerWorkspaceMember,
|
onSetTaskManagerWorkspaceMemberRole,
|
||||||
}: {
|
}: {
|
||||||
data: LauncherData;
|
data: LauncherData;
|
||||||
me: MeResponse;
|
me: MeResponse;
|
||||||
|
|
@ -196,7 +197,7 @@ export function AdminOverlay({
|
||||||
taskManagerWorkspacesError: string | null;
|
taskManagerWorkspacesError: string | null;
|
||||||
pendingTaskManagerMemberships: Record<string, boolean>;
|
pendingTaskManagerMemberships: Record<string, boolean>;
|
||||||
onRefreshTaskManagerWorkspaces: () => void;
|
onRefreshTaskManagerWorkspaces: () => void;
|
||||||
onEnsureTaskManagerWorkspaceMember: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
|
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
|
||||||
}) {
|
}) {
|
||||||
const isRoot = me.launcherRole === "root_admin";
|
const isRoot = me.launcherRole === "root_admin";
|
||||||
const sections = isRoot ? rootSections : clientSections;
|
const sections = isRoot ? rootSections : clientSections;
|
||||||
|
|
@ -329,7 +330,7 @@ export function AdminOverlay({
|
||||||
onUpdateMembership={onUpdateMembership}
|
onUpdateMembership={onUpdateMembership}
|
||||||
onDeleteMembership={onDeleteMembership}
|
onDeleteMembership={onDeleteMembership}
|
||||||
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
|
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
|
||||||
onEnsureTaskManagerWorkspaceMember={onEnsureTaskManagerWorkspaceMember}
|
onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{activeSection === "groups" ? (
|
{activeSection === "groups" ? (
|
||||||
|
|
@ -570,7 +571,7 @@ function UsersSection({
|
||||||
onUpdateMembership,
|
onUpdateMembership,
|
||||||
onDeleteMembership,
|
onDeleteMembership,
|
||||||
pendingTaskManagerMemberships,
|
pendingTaskManagerMemberships,
|
||||||
onEnsureTaskManagerWorkspaceMember,
|
onSetTaskManagerWorkspaceMemberRole,
|
||||||
}: {
|
}: {
|
||||||
data: LauncherData;
|
data: LauncherData;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
|
|
@ -580,7 +581,7 @@ function UsersSection({
|
||||||
onUpdateMembership: (membershipId: string, patch: Partial<ClientMembership>) => void;
|
onUpdateMembership: (membershipId: string, patch: Partial<ClientMembership>) => void;
|
||||||
onDeleteMembership: (membershipId: string) => void;
|
onDeleteMembership: (membershipId: string) => void;
|
||||||
pendingTaskManagerMemberships: Record<string, boolean>;
|
pendingTaskManagerMemberships: Record<string, boolean>;
|
||||||
onEnsureTaskManagerWorkspaceMember: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
|
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
|
||||||
}) {
|
}) {
|
||||||
const [editingMembershipId, setEditingMembershipId] = useState<string | null>(null);
|
const [editingMembershipId, setEditingMembershipId] = useState<string | null>(null);
|
||||||
const [newUserEmail, setNewUserEmail] = useState("");
|
const [newUserEmail, setNewUserEmail] = useState("");
|
||||||
|
|
@ -661,11 +662,11 @@ function UsersSection({
|
||||||
</div>
|
</div>
|
||||||
</GlassSurface>
|
</GlassSurface>
|
||||||
|
|
||||||
<GlassSurface className="table-shell">
|
<GlassSurface className="table-shell table-shell--users">
|
||||||
<div className="table-toolbar">
|
<div className="table-toolbar">
|
||||||
<h3>Участники</h3>
|
<h3>Участники</h3>
|
||||||
</div>
|
</div>
|
||||||
<table className="admin-data-table">
|
<table className="admin-data-table admin-data-table--users">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Пользователь</th>
|
<th>Пользователь</th>
|
||||||
|
|
@ -683,7 +684,15 @@ function UsersSection({
|
||||||
const taskManagerWorkspace = client.integrations?.taskManager?.workspaceSlug ?? null;
|
const taskManagerWorkspace = client.integrations?.taskManager?.workspaceSlug ?? null;
|
||||||
const pendingKey = `${client.id}:${user.id}`;
|
const pendingKey = `${client.id}:${user.id}`;
|
||||||
const pendingTaskerAssignment = Boolean(pendingTaskManagerMemberships[pendingKey]);
|
const pendingTaskerAssignment = Boolean(pendingTaskManagerMemberships[pendingKey]);
|
||||||
const taskManagerRole = membership.role === "client_owner" || membership.role === "client_admin" ? "admin" : "member";
|
const forcedTaskManagerAdmin = membership.role === "client_owner";
|
||||||
|
const taskManagerAssignment = data.taskManagerMemberships.find(
|
||||||
|
(candidate) => candidate.clientId === client.id && candidate.userId === user.id && candidate.workspaceSlug === taskManagerWorkspace
|
||||||
|
);
|
||||||
|
const taskManagerRole = taskManagerAssignment?.role ?? (forcedTaskManagerAdmin ? "admin" : "unset");
|
||||||
|
const taskManagerRoleOptions = buildTaskManagerRoleOptions({
|
||||||
|
hasWorkspace: Boolean(taskManagerWorkspace),
|
||||||
|
disabled: pendingTaskerAssignment || forcedTaskManagerAdmin || membership.status !== "active" || user.globalStatus !== "active",
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={membership.id}>
|
<tr key={membership.id}>
|
||||||
|
|
@ -736,15 +745,25 @@ function UsersSection({
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<NodeDcSelect
|
||||||
className="admin-inline-action"
|
className="admin-table-select-wrap"
|
||||||
type="button"
|
triggerClassName="admin-table-select-trigger"
|
||||||
disabled={!taskManagerWorkspace || pendingTaskerAssignment || membership.status !== "active" || user.globalStatus !== "active"}
|
value={pendingTaskerAssignment ? "pending" : taskManagerRole}
|
||||||
title={taskManagerWorkspace ? `Workspace: ${taskManagerWorkspace}` : "У клиента не выбран workspace Operational Core"}
|
options={taskManagerRoleOptions}
|
||||||
onClick={() => onEnsureTaskManagerWorkspaceMember({ clientId: client.id, userId: user.id, role: taskManagerRole })}
|
label={`Роль Tasker ${user.name}`}
|
||||||
>
|
minMenuWidth={180}
|
||||||
{pendingTaskerAssignment ? "Назначаем" : taskManagerWorkspace ? "Назначить" : "Нет workspace"}
|
disabled={
|
||||||
</button>
|
!taskManagerWorkspace ||
|
||||||
|
pendingTaskerAssignment ||
|
||||||
|
forcedTaskManagerAdmin ||
|
||||||
|
membership.status !== "active" ||
|
||||||
|
user.globalStatus !== "active"
|
||||||
|
}
|
||||||
|
onChange={(role) => {
|
||||||
|
if (role === "pending") return;
|
||||||
|
onSetTaskManagerWorkspaceMemberRole({ clientId: client.id, userId: user.id, role });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="services-admin-table__actions">
|
<td className="services-admin-table__actions">
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
@ -951,6 +970,22 @@ const accessAssignmentOptions: Array<NodeDcSelectOption<AccessAssignmentValue>>
|
||||||
{ value: "deny", label: "Deny", description: "Исключение", tone: "red" },
|
{ value: "deny", label: "Deny", description: "Исключение", tone: "red" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function buildTaskManagerRoleOptions({
|
||||||
|
hasWorkspace,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
hasWorkspace: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
}): Array<NodeDcSelectOption<TaskManagerRoleSelectValue>> {
|
||||||
|
return [
|
||||||
|
{ value: "unset", label: hasWorkspace ? "—" : "Workspace не выбран" },
|
||||||
|
{ value: "guest", label: "Гость", disabled: !hasWorkspace || disabled },
|
||||||
|
{ value: "member", label: "Участник", disabled: !hasWorkspace || disabled },
|
||||||
|
{ value: "admin", label: "Админ", disabled: !hasWorkspace || disabled },
|
||||||
|
{ value: "pending", label: "Сохраняем...", disabled: true, hidden: true },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const taskManagerWorkspacePolicyOptions: Array<NodeDcSelectOption<TaskManagerWorkspaceCreationPolicy>> = [
|
const taskManagerWorkspacePolicyOptions: Array<NodeDcSelectOption<TaskManagerWorkspaceCreationPolicy>> = [
|
||||||
{
|
{
|
||||||
value: "any_authorized_user",
|
value: "any_authorized_user",
|
||||||
|
|
@ -1651,9 +1686,15 @@ function ClientEditorModal({
|
||||||
minMenuWidth={280}
|
minMenuWidth={280}
|
||||||
onChange={updateTaskManagerWorkspace}
|
onChange={updateTaskManagerWorkspace}
|
||||||
/>
|
/>
|
||||||
<button className="admin-inline-action" type="button" disabled={taskManagerWorkspacesLoading} onClick={onRefreshTaskManagerWorkspaces}>
|
<IconButton
|
||||||
{taskManagerWorkspacesLoading ? "Обновляем" : "Обновить"}
|
label={taskManagerWorkspacesLoading ? "Обновляем workspace Operational Core" : "Обновить workspace Operational Core"}
|
||||||
</button>
|
className="admin-circle-action admin-circle-action--solid"
|
||||||
|
type="button"
|
||||||
|
disabled={taskManagerWorkspacesLoading}
|
||||||
|
onClick={onRefreshTaskManagerWorkspaces}
|
||||||
|
>
|
||||||
|
<RefreshCw size={16} />
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<small>
|
<small>
|
||||||
{taskManagerWorkspacesError
|
{taskManagerWorkspacesError
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue