mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(admin): filter out banned users (#5659)
* improvement(admin): filter out banned users * fix timestamp glitch
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user