FIX - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: автоподтверждение повторных workspace-инвайтов

This commit is contained in:
DCCONSTRUCTIONS 2026-05-13 01:38:54 +03:00
parent 0ba6dc7115
commit 784195f747
2 changed files with 55 additions and 8 deletions

View File

@ -892,6 +892,8 @@ export function createControlPlaneStore({ projectRoot }) {
const workspaceName = optionalString(payload?.workspaceName, workspaceSlug); const workspaceName = optionalString(payload?.workspaceName, workspaceSlug);
const inviteeEmail = requireString(payload?.inviteeEmail, "inviteeEmail").toLowerCase(); const inviteeEmail = requireString(payload?.inviteeEmail, "inviteeEmail").toLowerCase();
const role = normalizeTaskManagerInviteRole(payload?.role); const role = normalizeTaskManagerInviteRole(payload?.role);
const inviteeUser = data.users.find((user) => user.email.toLowerCase() === inviteeEmail && user.globalStatus === "active");
const autoApproveExistingUser = Boolean(inviteeUser && !hasTaskManagerDenyException(data, inviteeUser.id));
const existingRequest = data.taskerInviteRequests.find( const existingRequest = data.taskerInviteRequests.find(
(request) => (request) =>
request.taskerInviteId === taskerInviteId || request.taskerInviteId === taskerInviteId ||
@ -904,6 +906,18 @@ export function createControlPlaneStore({ projectRoot }) {
taskerInviteId, taskerInviteId,
createdAt: now, createdAt: now,
}; };
let nextStatus = "new";
if (existingRequest?.status && existingRequest.status !== "rejected") {
nextStatus = existingRequest.status;
}
if (autoApproveExistingUser) {
nextStatus = "approved";
}
let auditAction = existingRequest ? "Обновлена заявка workspace-инвайта" : "Создана заявка workspace-инвайта";
if (autoApproveExistingUser) {
auditAction = "Автоподтверждена заявка workspace-инвайта";
}
Object.assign(request, { Object.assign(request, {
taskerInviteId, taskerInviteId,
@ -916,11 +930,13 @@ export function createControlPlaneStore({ projectRoot }) {
inviterPlaneUserId: nullableStringWithFallback(payload?.inviterPlaneUserId, request.inviterPlaneUserId ?? null), inviterPlaneUserId: nullableStringWithFallback(payload?.inviterPlaneUserId, request.inviterPlaneUserId ?? null),
inviterEmail: requireString(payload?.inviterEmail, "inviterEmail").toLowerCase(), inviterEmail: requireString(payload?.inviterEmail, "inviterEmail").toLowerCase(),
inviterName: optionalString(payload?.inviterName, payload?.inviterEmail ?? "Operational Core user"), inviterName: optionalString(payload?.inviterName, payload?.inviterEmail ?? "Operational Core user"),
status: existingRequest?.status && existingRequest.status !== "rejected" ? existingRequest.status : "new", status: nextStatus,
taskerInviteLink: existingRequest?.taskerInviteLink ?? null, taskerInviteLink: existingRequest?.taskerInviteLink ?? null,
reviewedByUserId: existingRequest?.reviewedByUserId ?? null, reviewedByUserId: autoApproveExistingUser ? actor.id : existingRequest?.reviewedByUserId ?? null,
reviewedAt: existingRequest?.reviewedAt ?? null, reviewedAt: autoApproveExistingUser ? now : existingRequest?.reviewedAt ?? null,
comment: nullableStringWithFallback(payload?.comment, existingRequest?.comment ?? null), comment: autoApproveExistingUser
? "Автоподтверждено: пользователь уже активен в NODE.DC."
: nullableStringWithFallback(payload?.comment, existingRequest?.comment ?? null),
updatedAt: now, updatedAt: now,
}); });
@ -928,8 +944,20 @@ export function createControlPlaneStore({ projectRoot }) {
data.taskerInviteRequests.push(request); data.taskerInviteRequests.push(request);
} }
if (autoApproveExistingUser && inviteeUser) {
ensureTaskerInviteServiceAccess(
data,
{
source: "tasker_workspace_invite",
sourceTaskerInviteRequestId: request.id,
},
inviteeUser,
now
);
}
addAuditEvent(data, actor, { addAuditEvent(data, actor, {
action: existingRequest ? "Обновлена заявка workspace-инвайта" : "Создана заявка workspace-инвайта", action: auditAction,
objectType: "tasker_invite_request", objectType: "tasker_invite_request",
objectName: `${workspaceSlug}:${inviteeEmail}`, objectName: `${workspaceSlug}:${inviteeEmail}`,
result: "success", result: "success",
@ -937,7 +965,12 @@ export function createControlPlaneStore({ projectRoot }) {
}); });
await writeData(data); await writeData(data);
return { taskerInviteRequest: request, data }; return {
taskerInviteRequest: request,
autoApproved: autoApproveExistingUser,
affectedUserIds: [payload?.inviterUserId, inviteeUser?.id].filter((userId) => typeof userId === "string" && userId),
data,
};
} }
async function approveTaskerInviteRequest(taskerInviteRequestId, payload, identity) { async function approveTaskerInviteRequest(taskerInviteRequestId, payload, identity) {
@ -2295,6 +2328,17 @@ function ensureTaskerInviteServiceAccess(data, invite, user, now) {
return grant; return grant;
} }
function hasTaskManagerDenyException(data, userId) {
const service = data.services.find((candidate) => candidate.slug === "task-manager");
if (!service) {
return false;
}
return data.exceptions.some(
(exception) => exception.serviceId === service.id && exception.userId === userId && exception.type === "deny"
);
}
function findTaskerInviteRequestForCancellation(data, payload) { function findTaskerInviteRequestForCancellation(data, payload) {
const requestId = nullableString(payload?.requestId); const requestId = nullableString(payload?.requestId);
const taskerInviteId = nullableString(payload?.taskerInviteId); const taskerInviteId = nullableString(payload?.taskerInviteId);

View File

@ -501,8 +501,11 @@ app.post("/api/internal/tasker/invite-requests", asyncRoute(async (req, res) =>
inviterName: inviter.name, inviterName: inviter.name,
}, inviter); }, inviter);
publishControlPlaneEvent("tasker.invite-request.created", [inviter.id]); publishControlPlaneEvent(
res.json({ ok: true, taskerInviteRequest: result.taskerInviteRequest }); "tasker.invite-request.created",
result.affectedUserIds?.length ? result.affectedUserIds : [inviter.id]
);
res.json({ ok: true, taskerInviteRequest: result.taskerInviteRequest, autoApproved: Boolean(result.autoApproved) });
})); }));
app.post("/api/internal/tasker/invite-requests/cancel", asyncRoute(async (req, res) => { app.post("/api/internal/tasker/invite-requests/cancel", asyncRoute(async (req, res) => {