improvement(admin): filter out banned users (#5659)

* improvement(admin): filter out banned users

* fix timestamp glitch
This commit is contained in:
Vikhyath Mondreti
2026-07-13 18:27:36 -07:00
committed by GitHub
parent 3d67b5045b
commit c7151a1348
2 changed files with 100 additions and 8 deletions
@@ -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<Record<string, any>>,
}))
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<Record<string, any>>,
}))
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,
+13 -1
View File
@@ -229,9 +229,21 @@ 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<boolean>`NOT (
coalesce(${user.banned}, false)
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))
: undefined
const where = searchMatch ? and(visibleUser, searchMatch) : visibleUser
const [totalRow, rows] = await Promise.all([
db.select({ total: count() }).from(user).where(where),
db