diff --git a/server/control-plane-store.mjs b/server/control-plane-store.mjs index c92f6e2..beccee3 100644 --- a/server/control-plane-store.mjs +++ b/server/control-plane-store.mjs @@ -892,6 +892,8 @@ export function createControlPlaneStore({ projectRoot }) { const workspaceName = optionalString(payload?.workspaceName, workspaceSlug); const inviteeEmail = requireString(payload?.inviteeEmail, "inviteeEmail").toLowerCase(); 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( (request) => request.taskerInviteId === taskerInviteId || @@ -904,6 +906,18 @@ export function createControlPlaneStore({ projectRoot }) { taskerInviteId, 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, { taskerInviteId, @@ -916,11 +930,13 @@ export function createControlPlaneStore({ projectRoot }) { inviterPlaneUserId: nullableStringWithFallback(payload?.inviterPlaneUserId, request.inviterPlaneUserId ?? null), inviterEmail: requireString(payload?.inviterEmail, "inviterEmail").toLowerCase(), inviterName: optionalString(payload?.inviterName, payload?.inviterEmail ?? "Operational Core user"), - status: existingRequest?.status && existingRequest.status !== "rejected" ? existingRequest.status : "new", + status: nextStatus, taskerInviteLink: existingRequest?.taskerInviteLink ?? null, - reviewedByUserId: existingRequest?.reviewedByUserId ?? null, - reviewedAt: existingRequest?.reviewedAt ?? null, - comment: nullableStringWithFallback(payload?.comment, existingRequest?.comment ?? null), + reviewedByUserId: autoApproveExistingUser ? actor.id : existingRequest?.reviewedByUserId ?? null, + reviewedAt: autoApproveExistingUser ? now : existingRequest?.reviewedAt ?? null, + comment: autoApproveExistingUser + ? "Автоподтверждено: пользователь уже активен в NODE.DC." + : nullableStringWithFallback(payload?.comment, existingRequest?.comment ?? null), updatedAt: now, }); @@ -928,8 +944,20 @@ export function createControlPlaneStore({ projectRoot }) { data.taskerInviteRequests.push(request); } + if (autoApproveExistingUser && inviteeUser) { + ensureTaskerInviteServiceAccess( + data, + { + source: "tasker_workspace_invite", + sourceTaskerInviteRequestId: request.id, + }, + inviteeUser, + now + ); + } + addAuditEvent(data, actor, { - action: existingRequest ? "Обновлена заявка workspace-инвайта" : "Создана заявка workspace-инвайта", + action: auditAction, objectType: "tasker_invite_request", objectName: `${workspaceSlug}:${inviteeEmail}`, result: "success", @@ -937,7 +965,12 @@ export function createControlPlaneStore({ projectRoot }) { }); 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) { @@ -2295,6 +2328,17 @@ function ensureTaskerInviteServiceAccess(data, invite, user, now) { 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) { const requestId = nullableString(payload?.requestId); const taskerInviteId = nullableString(payload?.taskerInviteId); diff --git a/server/dev-server.mjs b/server/dev-server.mjs index daea2ce..a37168c 100644 --- a/server/dev-server.mjs +++ b/server/dev-server.mjs @@ -501,8 +501,11 @@ app.post("/api/internal/tasker/invite-requests", asyncRoute(async (req, res) => inviterName: inviter.name, }, inviter); - publishControlPlaneEvent("tasker.invite-request.created", [inviter.id]); - res.json({ ok: true, taskerInviteRequest: result.taskerInviteRequest }); + publishControlPlaneEvent( + "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) => {