import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { permissions, settings, type WorkspaceMode, workflow, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { listWorkspacesQuerySchema } from '@/lib/api/contracts' import { createWorkspaceContract } from '@/lib/api/contracts/workspaces' import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import type { PlanCategory } from '@/lib/billing/plan-helpers' import { PlatformEvents } from '@/lib/core/telemetry' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' import { getRandomWorkspaceColor } from '@/lib/workspaces/colors' import { CONTACT_OWNER_TO_UPGRADE_REASON, evaluateWorkspaceInvitePolicy, getInvitePlanCategoryForOrganization, getInvitePlanCategoryForUser, getWorkspaceCreationPolicy, getWorkspaceInvitePolicy, UPGRADE_TO_INVITE_REASON, WORKSPACE_MODE, } from '@/lib/workspaces/policy' import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' const logger = createLogger('Workspaces') // Get all workspaces for the current user export const GET = withRouteHandler(async (request: Request) => { const session = await getSession() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const activeOrganizationId = (session.session as { activeOrganizationId?: string } | null)?.activeOrganizationId ?? null const creationPolicy = await getWorkspaceCreationPolicy({ userId: session.user.id, activeOrganizationId, }) const scopeResult = listWorkspacesQuerySchema.safeParse( Object.fromEntries(new URL(request.url).searchParams.entries()) ) if (!scopeResult.success) { return NextResponse.json( { error: 'Invalid query parameters', details: scopeResult.error.issues }, { status: 400 } ) } const { scope } = scopeResult.data const settingsQuery = db .select({ lastActiveWorkspaceId: settings.lastActiveWorkspaceId }) .from(settings) .where(eq(settings.userId, session.user.id)) .limit(1) const [userWorkspaces, userSettings] = await Promise.all([ listAccessibleWorkspaceRowsForUser(session.user.id, scope), settingsQuery, ]) const lastActiveWorkspaceId = userSettings[0]?.lastActiveWorkspaceId ?? null if (scope === 'active' && userWorkspaces.length === 0) { if (!creationPolicy.canCreate) { return NextResponse.json({ workspaces: [], lastActiveWorkspaceId, creationPolicy }) } const defaultWorkspace = await createDefaultWorkspace( session.user.id, session.user.name, creationPolicy ) await migrateExistingWorkflows(session.user.id, defaultWorkspace.id) const refreshedCreationPolicy = await getWorkspaceCreationPolicy({ userId: session.user.id, activeOrganizationId, }) return NextResponse.json({ workspaces: [defaultWorkspace], lastActiveWorkspaceId, creationPolicy: refreshedCreationPolicy, }) } if (scope === 'active') { await ensureWorkflowsHaveWorkspace(session.user.id, userWorkspaces[0].workspace.id) } const nonOrgBilledUserIds = [ ...new Set( userWorkspaces .filter(({ workspace: ws }) => ws.workspaceMode !== WORKSPACE_MODE.ORGANIZATION) .map(({ workspace: ws }) => ws.billedAccountUserId) ), ] const orgIds = [ ...new Set( userWorkspaces .filter( ({ workspace: ws }) => ws.workspaceMode === WORKSPACE_MODE.ORGANIZATION && ws.organizationId ) .map(({ workspace: ws }) => ws.organizationId as string) ), ] const planCategoryByBilledUser = new Map() const planCategoryByOrg = new Map() await Promise.all([ ...nonOrgBilledUserIds.map(async (userId) => { planCategoryByBilledUser.set(userId, await getInvitePlanCategoryForUser(userId)) }), ...orgIds.map(async (orgId) => { planCategoryByOrg.set(orgId, await getInvitePlanCategoryForOrganization(orgId)) }), ]) const workspacesWithPermissions = userWorkspaces.map( ({ workspace: workspaceDetails, permissionType }) => { const billedPlanCategory: PlanCategory = workspaceDetails.workspaceMode === WORKSPACE_MODE.ORGANIZATION ? workspaceDetails.organizationId ? (planCategoryByOrg.get(workspaceDetails.organizationId) ?? 'free') : 'free' : (planCategoryByBilledUser.get(workspaceDetails.billedAccountUserId) ?? 'free') const invitePolicy = evaluateWorkspaceInvitePolicy(workspaceDetails, { billedPlanCategory }) const callerIsBilledUser = workspaceDetails.billedAccountUserId === session.user.id const canActOnUpgrade = invitePolicy.upgradeRequired && callerIsBilledUser const inviteDisabledReason = invitePolicy.allowed ? null : callerIsBilledUser ? (invitePolicy.reason ?? UPGRADE_TO_INVITE_REASON) : CONTACT_OWNER_TO_UPGRADE_REASON return { ...workspaceDetails, role: workspaceDetails.ownerId === session.user.id ? 'owner' : permissionType === 'admin' ? 'admin' : 'member', permissions: permissionType, inviteMembersEnabled: invitePolicy.allowed, inviteDisabledReason, inviteUpgradeRequired: canActOnUpgrade, } } ) return NextResponse.json({ workspaces: workspacesWithPermissions, lastActiveWorkspaceId, creationPolicy, }) }) // POST /api/workspaces - Create a new workspace export const POST = withRouteHandler(async (req: NextRequest) => { const session = await getSession() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } try { const parsed = await parseRequest(createWorkspaceContract, req, {}) if (!parsed.success) return parsed.response const { name, color, skipDefaultWorkflow } = parsed.data.body const activeOrganizationId = (session.session as { activeOrganizationId?: string } | null)?.activeOrganizationId ?? null const creationPolicy = await getWorkspaceCreationPolicy({ userId: session.user.id, activeOrganizationId, }) if (!creationPolicy.canCreate) { return NextResponse.json( { error: creationPolicy.reason || 'Workspace creation is not available.' }, { status: creationPolicy.status } ) } const newWorkspace = await createWorkspace({ userId: session.user.id, name, skipDefaultWorkflow, explicitColor: color, organizationId: creationPolicy.organizationId, workspaceMode: creationPolicy.workspaceMode, billedAccountUserId: creationPolicy.billedAccountUserId, }) captureServerEvent( session.user.id, 'workspace_created', { workspace_id: newWorkspace.id, name: newWorkspace.name, workspace_mode: newWorkspace.workspaceMode, organization_id: newWorkspace.organizationId, }, { groups: { workspace: newWorkspace.id }, setOnce: { first_workspace_created_at: new Date().toISOString() }, } ) recordAudit({ workspaceId: newWorkspace.id, actorId: session.user.id, actorName: session.user.name, actorEmail: session.user.email, action: AuditAction.WORKSPACE_CREATED, resourceType: AuditResourceType.WORKSPACE, resourceId: newWorkspace.id, resourceName: newWorkspace.name, description: `Created workspace "${newWorkspace.name}"`, metadata: { name: newWorkspace.name, color: newWorkspace.color, workspaceMode: newWorkspace.workspaceMode, organizationId: newWorkspace.organizationId, }, request: req, }) return NextResponse.json({ workspace: newWorkspace }) } catch (error) { logger.error('Error creating workspace:', error) return NextResponse.json({ error: 'Failed to create workspace' }, { status: 500 }) } }) async function createDefaultWorkspace( userId: string, userName: string | null | undefined, creationPolicy: { organizationId: string | null workspaceMode: WorkspaceMode billedAccountUserId: string } ) { const firstName = userName?.split(' ')[0] || null const workspaceName = firstName ? `${firstName}'s Workspace` : 'My Workspace' return createWorkspace({ userId, name: workspaceName, organizationId: creationPolicy.organizationId, workspaceMode: creationPolicy.workspaceMode, billedAccountUserId: creationPolicy.billedAccountUserId, }) } interface CreateWorkspaceParams { userId: string name: string skipDefaultWorkflow?: boolean explicitColor?: string organizationId: string | null workspaceMode: WorkspaceMode billedAccountUserId: string } async function createWorkspace({ userId, name, skipDefaultWorkflow = false, explicitColor, organizationId, workspaceMode, billedAccountUserId, }: CreateWorkspaceParams) { const workspaceId = generateId() const workflowId = generateId() const now = new Date() const color = explicitColor || getRandomWorkspaceColor() try { await db.transaction(async (tx) => { await tx.insert(workspace).values({ id: workspaceId, name, color, ownerId: userId, organizationId, workspaceMode, billedAccountUserId, allowPersonalApiKeys: true, createdAt: now, updatedAt: now, }) const permissionRows = [ { id: generateId(), entityType: 'workspace' as const, entityId: workspaceId, userId, permissionType: 'admin' as const, createdAt: now, updatedAt: now, }, ] if ( workspaceMode === WORKSPACE_MODE.ORGANIZATION && billedAccountUserId && billedAccountUserId !== userId ) { permissionRows.push({ id: generateId(), entityType: 'workspace' as const, entityId: workspaceId, userId: billedAccountUserId, permissionType: 'admin' as const, createdAt: now, updatedAt: now, }) } await tx.insert(permissions).values(permissionRows) if (!skipDefaultWorkflow) { await tx.insert(workflow).values({ id: workflowId, userId, workspaceId, folderId: null, name: 'default-agent', description: 'Your first workflow - start building here!', lastSynced: now, createdAt: now, updatedAt: now, isDeployed: false, runCount: 0, variables: {}, }) const { workflowState } = buildDefaultWorkflowArtifacts() await saveWorkflowToNormalizedTables(workflowId, workflowState, tx) } logger.info( skipDefaultWorkflow ? `Created ${workspaceMode} workspace ${workspaceId} for user ${userId}` : `Created ${workspaceMode} workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}` ) }) } catch (error) { logger.error(`Failed to create workspace ${workspaceId}:`, error) throw error } try { PlatformEvents.workspaceCreated({ workspaceId, userId, name, }) } catch { // Telemetry should not fail the operation } const invitePolicy = await getWorkspaceInvitePolicy({ organizationId, workspaceMode, billedAccountUserId, ownerId: userId, }) const callerIsBilledUser = billedAccountUserId === userId const canActOnUpgrade = invitePolicy.upgradeRequired && callerIsBilledUser const inviteDisabledReason = invitePolicy.allowed ? null : callerIsBilledUser ? (invitePolicy.reason ?? UPGRADE_TO_INVITE_REASON) : CONTACT_OWNER_TO_UPGRADE_REASON return { id: workspaceId, name, color, ownerId: userId, organizationId, workspaceMode, billedAccountUserId, allowPersonalApiKeys: true, createdAt: now, updatedAt: now, role: 'owner', permissions: 'admin', inviteMembersEnabled: invitePolicy.allowed, inviteDisabledReason, inviteUpgradeRequired: canActOnUpgrade, } } async function migrateExistingWorkflows(userId: string, workspaceId: string) { const orphanedWorkflows = await db .select({ id: workflow.id }) .from(workflow) .where(and(eq(workflow.userId, userId), isNull(workflow.workspaceId))) if (orphanedWorkflows.length === 0) { return // No orphaned workflows to migrate } logger.info( `Migrating ${orphanedWorkflows.length} workflows to workspace ${workspaceId} for user ${userId}` ) await db .update(workflow) .set({ workspaceId: workspaceId, updatedAt: new Date(), }) .where(and(eq(workflow.userId, userId), isNull(workflow.workspaceId))) } async function ensureWorkflowsHaveWorkspace(userId: string, defaultWorkspaceId: string) { const orphanedWorkflows = await db .select() .from(workflow) .where(and(eq(workflow.userId, userId), isNull(workflow.workspaceId))) if (orphanedWorkflows.length > 0) { await db .update(workflow) .set({ workspaceId: defaultWorkspaceId, updatedAt: new Date(), }) .where(and(eq(workflow.userId, userId), isNull(workflow.workspaceId))) logger.info(`Fixed ${orphanedWorkflows.length} orphaned workflows for user ${userId}`) } }