import { and, eq } from "@openstatus/db"; import { usersToWorkspaces } from "@openstatus/db/src/schema"; import { requireScope } from "../auth"; import { type ServiceContext, withTransaction } from "../context"; import { NotFoundError, PreconditionFailedError } from "../errors"; import { removeMemberInWorkspace } from "./internal"; import { DeleteMemberInput } from "./schemas"; /** * Delete a member's workspace association. Idempotent on the target row — * if the target user has no membership in the caller's workspace (wrong id, * already removed, or scoped to a different workspace), the call succeeds * silently and no audit row is emitted. This matches the workspace-filtered * DELETE semantics inherited from the legacy router. * * Authorization rules preserved from the legacy router: * - only the caller's own membership row is consulted; the `owner` role is * required to remove anyone else; * - an owner cannot remove themselves (would orphan the workspace). * * Both role-failure and self-removal surface as `PRECONDITION_FAILED` to * match the existing tRPC contract — these are state-based rejections, not * authz failures in the sense of "you lack any access to this workspace." * * Only fires when the actor is an openstatus user: removing another user is * not something a system / apiKey / webhook actor should do today. */ // The delete and its `member.delete` audit row live in // `removeMemberInWorkspace`; this verb only adds the owner / self-removal guards. // oxlint-disable-next-line openstatus/services-mutation-guards export async function deleteMember(args: { ctx: ServiceContext; input: DeleteMemberInput; }): Promise { const { ctx } = args; requireScope(ctx, "write"); const input = DeleteMemberInput.parse(args.input); if (ctx.actor.type !== "user") { throw new PreconditionFailedError( "Only user actors can remove workspace members", ); } const callerId = ctx.actor.userId; await withTransaction(ctx, async (tx) => { const caller = await tx.query.usersToWorkspaces.findFirst({ where: and( eq(usersToWorkspaces.userId, callerId), eq(usersToWorkspaces.workspaceId, ctx.workspace.id), ), }); if (!caller) { throw new NotFoundError("membership", callerId); } if (caller.role !== "owner") { throw new PreconditionFailedError( "Not authorized to remove user from workspace", ); } if (input.userId === callerId) { throw new PreconditionFailedError( "Cannot remove yourself from workspace", ); } await removeMemberInWorkspace({ tx, ctx, userId: input.userId }); }); }