FIX - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: АДМИНКА РОЛЕЙ TASKER

This commit is contained in:
DCCONSTRUCTIONS
2026-05-06 15:30:56 +03:00
parent d4eba0ff3a
commit 5f461d57ea
9 changed files with 604 additions and 58 deletions
+67 -1
View File
@@ -14,6 +14,7 @@ const collectionKeys = [
"invites",
"syncStatuses",
"auditEvents",
"taskManagerMemberships",
];
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 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, {
action: "Назначен Tasker workspace",
objectType: "task-manager-membership",
objectName: user.name,
clientId: client.id,
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);
@@ -1055,6 +1090,7 @@ export function createControlPlaneStore({ projectRoot }) {
retrySync,
markUserAuthentikProvisioned,
recordTaskManagerWorkspaceMembership,
removeTaskManagerWorkspaceMembership,
setUserServiceAccess,
updateClient,
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) {
const user = data.users.find(
(item) =>
+83 -2
View File
@@ -301,7 +301,7 @@ app.post("/api/internal/handoff/consume", (req, res) => {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl ?? null,
avatarUrl: resolveUserAvatarPublicUrl(user),
subject: user.authentikUserId || handoff.user.sub,
authentikUserId: user.authentikUserId ?? null,
groups,
@@ -557,6 +557,7 @@ app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncher
workspaceSlug,
email: user.email,
subject: user.authentikUserId ?? undefined,
avatarUrl: resolveUserAvatarPublicUrl(user),
role,
companyRole: membership?.role ?? null,
setLastWorkspace: req.body?.setLastWorkspace !== false,
@@ -578,6 +579,81 @@ app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncher
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) => {
const result = await controlPlaneStore.createClient(req.body, req.nodedcSession.user);
res.status(201).json(result);
@@ -1289,7 +1365,7 @@ function normalizeOptionalText(value) {
}
function normalizeTaskManagerRole(value) {
return value === "admin" || value === "member" ? value : null;
return value === "guest" || value === "admin" || value === "member" ? value : null;
}
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) {
if (!isUploadPayload(payload)) {
throw new Error("Некорректный payload загрузки");