diff --git a/apps/sim/lib/workspaces/policy.test.ts b/apps/sim/lib/workspaces/policy.test.ts index 22ed9cb08f0..ae2b2cbde96 100644 --- a/apps/sim/lib/workspaces/policy.test.ts +++ b/apps/sim/lib/workspaces/policy.test.ts @@ -82,7 +82,7 @@ describe('getWorkspaceCreationPolicy', () => { }) it('blocks free users once they already own one non-organization workspace', async () => { - mockDbResults.value = [[{ value: 1 }]] + mockDbResults.value = [[], [{ value: 1 }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1' }) @@ -98,7 +98,7 @@ describe('getWorkspaceCreationPolicy', () => { plan: 'pro_6000', status: 'active', }) - mockDbResults.value = [[{ value: 2 }]] + mockDbResults.value = [[], [{ value: 2 }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1' }) @@ -114,7 +114,7 @@ describe('getWorkspaceCreationPolicy', () => { plan: 'pro_25000', status: 'active', }) - mockDbResults.value = [[{ value: 5 }]] + mockDbResults.value = [[], [{ value: 5 }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1' }) @@ -130,7 +130,7 @@ describe('getWorkspaceCreationPolicy', () => { plan: 'pro_25000', status: 'active', }) - mockDbResults.value = [[{ value: 10 }]] + mockDbResults.value = [[], [{ value: 10 }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1' }) @@ -220,6 +220,68 @@ describe('getWorkspaceCreationPolicy', () => { expect(result.reason).toContain('owners and admins') }) + it('grants platform admins unlimited personal workspaces regardless of plan', async () => { + mockDbResults.value = [[{ role: 'admin' }], [{ value: 25 }]] + + const result = await getWorkspaceCreationPolicy({ userId: 'admin-user' }) + + expect(result.canCreate).toBe(true) + expect(result.workspaceMode).toBe(WORKSPACE_MODE.PERSONAL) + expect(result.maxWorkspaces).toBeNull() + expect(result.currentWorkspaceCount).toBe(25) + expect(mockGetHighestPrioritySubscription).not.toHaveBeenCalled() + }) + + it('keeps platform admins in org context when the org lacks a team plan', async () => { + mockGetUserOrganization.mockResolvedValueOnce({ + organizationId: 'org-1', + role: 'admin', + memberId: 'member-1', + }) + mockGetOrganizationSubscription.mockResolvedValueOnce(null) + mockDbResults.value = [[{ role: 'admin' }], [{ userId: 'owner-1' }]] + + const result = await getWorkspaceCreationPolicy({ + userId: 'admin-user', + activeOrganizationId: 'org-1', + }) + + expect(result.canCreate).toBe(true) + expect(result.workspaceMode).toBe(WORKSPACE_MODE.ORGANIZATION) + expect(result.organizationId).toBe('org-1') + expect(result.billedAccountUserId).toBe('owner-1') + expect(mockGetHighestPrioritySubscription).not.toHaveBeenCalled() + }) + + it('blocks platform admins who are only org members from creating org workspaces', async () => { + mockGetUserOrganization.mockResolvedValueOnce({ + organizationId: 'org-1', + role: 'member', + memberId: 'member-1', + }) + mockGetOrganizationSubscription.mockResolvedValueOnce(null) + mockDbResults.value = [[{ role: 'admin' }], [{ value: 0 }]] + + const result = await getWorkspaceCreationPolicy({ + userId: 'admin-user', + activeOrganizationId: 'org-1', + }) + + expect(result.canCreate).toBe(true) + expect(result.workspaceMode).toBe(WORKSPACE_MODE.PERSONAL) + expect(result.organizationId).toBeNull() + }) + + it('still enforces plan limits for non-admin users', async () => { + mockDbResults.value = [[{ role: 'user' }], [{ value: 1 }]] + + const result = await getWorkspaceCreationPolicy({ userId: 'regular-user' }) + + expect(result.canCreate).toBe(false) + expect(result.maxWorkspaces).toBe(1) + expect(result.currentWorkspaceCount).toBe(1) + }) + it('blocks users without org membership from creating workspaces in the active org context', async () => { mockDbResults.value = [[], [{ userId: 'owner-1' }]] diff --git a/apps/sim/lib/workspaces/policy.ts b/apps/sim/lib/workspaces/policy.ts index 100ac51035f..184fbb82ca1 100644 --- a/apps/sim/lib/workspaces/policy.ts +++ b/apps/sim/lib/workspaces/policy.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { member, type WorkspaceMode, workspace } from '@sim/db/schema' +import { member, user, type WorkspaceMode, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, count, eq, isNull } from 'drizzle-orm' import { getOrganizationSubscription } from '@/lib/billing/core/billing' @@ -276,6 +276,32 @@ export async function getWorkspaceCreationPolicy({ } } + if (await isPlatformAdmin(userId)) { + if (organizationId && orgRole && ['owner', 'admin'].includes(orgRole)) { + return { + canCreate: true, + workspaceMode: WORKSPACE_MODE.ORGANIZATION, + organizationId, + billedAccountUserId: await requireOrganizationOwnerId(organizationId), + maxWorkspaces: null, + currentWorkspaceCount: 0, + reason: null, + status: 200, + } + } + + return { + canCreate: true, + workspaceMode: WORKSPACE_MODE.PERSONAL, + organizationId: null, + billedAccountUserId: userId, + maxWorkspaces: null, + currentWorkspaceCount: await countNonOrganizationOwnedWorkspaces(userId), + reason: null, + status: 200, + } + } + const highestPrioritySubscription = await getHighestPrioritySubscription(userId) const plan = highestPrioritySubscription?.plan const maxWorkspaces = isMax(plan) ? 10 : isPro(plan) ? 3 : 1 @@ -331,6 +357,12 @@ export async function getOrganizationOwnerId(organizationId: string): Promise { + const [row] = await db.select({ role: user.role }).from(user).where(eq(user.id, userId)).limit(1) + + return row?.role === 'admin' +} + /** * Like `getOrganizationOwnerId` but throws when no owner row exists. * Use when the caller needs a guaranteed billed-account userId — every