From ef3df5c2cd32852b1fbb642ec93bafeed0be7830 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 13 Jul 2026 18:13:31 -0700 Subject: [PATCH 1/2] improvement(admin): filter out banned users --- .../workflow/workflow-handler.test.ts | 94 +++++++++++++++++-- apps/sim/lib/admin/dashboard.ts | 11 ++- 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index e6906a687f9..db21354265f 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -9,13 +9,21 @@ import { import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' -const { mockExecutorExecute, mockCreateSnapshot, mockResolveBillingAttribution, executorOptions } = - vi.hoisted(() => ({ - mockExecutorExecute: vi.fn(), - mockCreateSnapshot: vi.fn(), - mockResolveBillingAttribution: vi.fn(), - executorOptions: [] as Array>, - })) +const { + mockExecutorExecute, + mockCreateSnapshot, + mockResolveBillingAttribution, + mockGetCustomBlockAuthority, + mockGetPersonalAndWorkspaceEnv, + executorOptions, +} = vi.hoisted(() => ({ + mockExecutorExecute: vi.fn(), + mockCreateSnapshot: vi.fn(), + mockResolveBillingAttribution: vi.fn(), + mockGetCustomBlockAuthority: vi.fn(), + mockGetPersonalAndWorkspaceEnv: vi.fn(), + executorOptions: [] as Array>, +})) vi.mock('@/executor', () => ({ Executor: class { @@ -30,6 +38,14 @@ vi.mock('@/lib/billing/core/billing-attribution', () => ({ resolveBillingAttribution: mockResolveBillingAttribution, })) +vi.mock('@/lib/environment/utils', () => ({ + getPersonalAndWorkspaceEnv: mockGetPersonalAndWorkspaceEnv, +})) + +vi.mock('@/lib/workflows/custom-blocks/operations', () => ({ + getCustomBlockAuthority: mockGetCustomBlockAuthority, +})) + vi.mock('@/lib/logs/execution/snapshot/service', () => ({ snapshotService: { createSnapshotWithDeduplication: mockCreateSnapshot }, })) @@ -321,6 +337,70 @@ describe('WorkflowBlockHandler', () => { expect(mockResolveBillingAttribution).not.toHaveBeenCalled() }) + it('resolves a source-scoped billing attribution for custom block children', async () => { + const consumerAttribution = { actorUserId: 'consumer-1', workspaceId: 'workspace-consumer' } + const sourceAttribution = { actorUserId: 'owner-9', workspaceId: 'workspace-source' } + const customBlock = { + ...mockBlock, + metadata: { id: 'custom_block_abc', name: 'Published Block' }, + } + const ctx = { + ...mockContext, + workspaceId: 'workspace-consumer', + metadata: { ...mockContext.metadata, billingAttribution: consumerAttribution }, + } as unknown as ExecutionContext + + mockGetCustomBlockAuthority.mockResolvedValue({ + workflowId: 'source-workflow-id', + organizationId: 'org-1', + ownerUserId: 'owner-9', + exposedOutputs: [], + requiredInputIds: [], + }) + mockGetPersonalAndWorkspaceEnv.mockResolvedValue({ + personalDecrypted: {}, + workspaceDecrypted: {}, + }) + mockResolveBillingAttribution.mockResolvedValue(sourceAttribution) + mockFetch.mockImplementation(async (url: unknown) => { + if (String(url).includes('/deployed')) { + return { + ok: true, + json: () => + Promise.resolve({ + data: { + deployedState: { blocks: {}, edges: [], loops: {}, parallels: {} }, + }, + }), + } + } + return { + ok: true, + json: () => + Promise.resolve({ + data: { + name: 'Source Workflow', + workspaceId: 'workspace-source', + variables: {}, + }, + }), + } + }) + mockCreateSnapshot.mockResolvedValue({ snapshot: { id: 'snapshot-1' } }) + mockExecutorExecute.mockResolvedValue({ success: true, output: { data: 'ok' } }) + + await handler.execute(ctx, customBlock, {}) + + expect(mockResolveBillingAttribution).toHaveBeenCalledWith({ + actorUserId: 'owner-9', + workspaceId: 'workspace-source', + }) + expect(executorOptions).toHaveLength(1) + expect(executorOptions[0].contextExtensions.billingAttribution).toBe(sourceAttribution) + expect(executorOptions[0].contextExtensions.userId).toBe('owner-9') + expect(executorOptions[0].contextExtensions.workspaceId).toBe('workspace-source') + }) + it('should fail closed when the executing context has no workspace', async () => { mockFetch.mockResolvedValueOnce({ ok: true, diff --git a/apps/sim/lib/admin/dashboard.ts b/apps/sim/lib/admin/dashboard.ts index 9fccdec4892..472394e4c5c 100644 --- a/apps/sim/lib/admin/dashboard.ts +++ b/apps/sim/lib/admin/dashboard.ts @@ -229,9 +229,18 @@ function buildDashboardOrganizationSummary({ export async function listDashboardUsers({ search, limit, offset }: PaginationInput) { const trimmed = search.trim() - const where = trimmed + // Mirror Better Auth's active-ban semantics: permanent bans and temporary + // bans whose expiry is still in the future stay out of the Users dashboard, + // while an expired temporary ban is treated as lifted. Keep this predicate + // in the database query so pagination totals cannot leak or count hidden rows. + const visibleUser = sql`NOT ( + coalesce(${user.banned}, false) + AND (${user.banExpires} IS NULL OR ${user.banExpires} > now()) + )` + const searchMatch = trimmed ? or(ilike(user.name, `%${trimmed}%`), ilike(user.email, `%${trimmed}%`), eq(user.id, trimmed)) : undefined + const where = searchMatch ? and(visibleUser, searchMatch) : visibleUser const [totalRow, rows] = await Promise.all([ db.select({ total: count() }).from(user).where(where), db From 88459f5a2c4a51525865e6d576a15cc8f08ac7ad Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 13 Jul 2026 18:21:06 -0700 Subject: [PATCH 2/2] fix timestamp glitch --- apps/sim/lib/admin/dashboard.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/admin/dashboard.ts b/apps/sim/lib/admin/dashboard.ts index 472394e4c5c..d29bf4379fb 100644 --- a/apps/sim/lib/admin/dashboard.ts +++ b/apps/sim/lib/admin/dashboard.ts @@ -235,7 +235,10 @@ export async function listDashboardUsers({ search, limit, offset }: PaginationIn // in the database query so pagination totals cannot leak or count hidden rows. const visibleUser = sql`NOT ( coalesce(${user.banned}, false) - AND (${user.banExpires} IS NULL OR ${user.banExpires} > now()) + AND ( + ${user.banExpires} IS NULL + OR ${user.banExpires} > (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') + ) )` const searchMatch = trimmed ? or(ilike(user.name, `%${trimmed}%`), ilike(user.email, `%${trimmed}%`), eq(user.id, trimmed))