Files
sim/apps/sim/app/api/workspaces/members/[id]/route.ts
Waleed ace87791d8 feat(analytics): add PostHog product analytics (#3910)
* feat(analytics): add PostHog product analytics

* fix(posthog): fix workspace group via URL params, type errors, and clean up comments

* fix(posthog): address PR review - fix pre-tx event, auth_method, paused executions, enterprise cancellation, settings double-fire

* chore(posthog): remove unused identifyServerPerson

* fix(posthog): isolate processQueuedResumes errors, simplify settings posthog deps

* fix(posthog): correctly classify SSO auth_method, fix phantom empty-string workspace groups

* fix(posthog): remove usePostHog from memo'd TemplateCard, fix copilot chat phantom workspace group

* fix(posthog): eliminate all remaining phantom empty-string workspace groups

* fix(posthog): fix cancel route phantom group, remove redundant workspaceId shadow in catch block

* fix(posthog): use ids.length for block_removed guard to handle container blocks with descendants

* chore(posthog): remove unused removedBlockTypes variable

* fix(posthog): remove phantom $set person properties from subscription events

* fix(posthog): add passedKnowledgeBaseName to knowledge_base_opened effect deps

* fix(posthog): capture currentWorkflowId synchronously before async import to avoid stale closure

* fix(posthog): add typed captureEvent wrapper for React components, deduplicate copilot_panel_opened

* feat(posthog): add task_created and task_message_sent events, remove copilot_panel_opened

* feat(posthog): track task_renamed, task_deleted, task_marked_read, task_marked_unread

* feat(analytics): expand posthog event coverage with source tracking and lifecycle events

* fix(analytics): flush posthog events on SIGTERM before ECS task termination

* fix(analytics): fix posthog in useCallback deps and fire block events for bulk operations
2026-04-03 01:00:35 -07:00

135 lines
4.4 KiB
TypeScript

import { db } from '@sim/db'
import { permissions, workspace } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { revokeWorkspaceCredentialMemberships } from '@/lib/credentials/access'
import { captureServerEvent } from '@/lib/posthog/server'
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('WorkspaceMemberAPI')
const deleteMemberSchema = z.object({
workspaceId: z.string().uuid(),
})
// DELETE /api/workspaces/members/[id] - Remove a member from a workspace
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id: userId } = await params
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
// Get the workspace ID from the request body or URL
const body = deleteMemberSchema.parse(await req.json())
const { workspaceId } = body
const workspaceRow = await db
.select({ billedAccountUserId: workspace.billedAccountUserId })
.from(workspace)
.where(eq(workspace.id, workspaceId))
.limit(1)
if (!workspaceRow.length) {
return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
}
if (workspaceRow[0].billedAccountUserId === userId) {
return NextResponse.json(
{ error: 'Cannot remove the workspace billing account. Please reassign billing first.' },
{ status: 400 }
)
}
// Check if the user to be removed actually has permissions for this workspace
const userPermission = await db
.select()
.from(permissions)
.where(
and(
eq(permissions.userId, userId),
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workspaceId)
)
)
.then((rows) => rows[0])
if (!userPermission) {
return NextResponse.json({ error: 'User not found in workspace' }, { status: 404 })
}
// Check if current user has admin access to this workspace
const hasAdminAccess = await hasWorkspaceAdminAccess(session.user.id, workspaceId)
const isSelf = userId === session.user.id
if (!hasAdminAccess && !isSelf) {
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
}
// Prevent removing yourself if you're the last admin
if (isSelf && userPermission.permissionType === 'admin') {
const otherAdmins = await db
.select()
.from(permissions)
.where(
and(
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workspaceId),
eq(permissions.permissionType, 'admin')
)
)
.then((rows) => rows.filter((row) => row.userId !== session.user.id))
if (otherAdmins.length === 0) {
return NextResponse.json(
{ error: 'Cannot remove the last admin from a workspace' },
{ status: 400 }
)
}
}
// Delete the user's permissions for this workspace
await db
.delete(permissions)
.where(
and(
eq(permissions.userId, userId),
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workspaceId)
)
)
await revokeWorkspaceCredentialMemberships(workspaceId, userId)
captureServerEvent(
session.user.id,
'workspace_member_removed',
{ workspace_id: workspaceId, is_self_removal: isSelf },
{ groups: { workspace: workspaceId } }
)
recordAudit({
workspaceId,
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.MEMBER_REMOVED,
resourceType: AuditResourceType.WORKSPACE,
resourceId: workspaceId,
description: isSelf ? 'Left the workspace' : 'Removed a member from the workspace',
metadata: { removedUserId: userId, selfRemoval: isSelf },
request: req,
})
return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error removing workspace member:', error)
return NextResponse.json({ error: 'Failed to remove workspace member' }, { status: 500 })
}
}