Skip to content

Commit 587657b

Browse files
committed
feat(mothership): manage settings and workflow file inputs
1 parent b7616b3 commit 587657b

243 files changed

Lines changed: 19494 additions & 8292 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/sim/app/api/custom-blocks/[id]/authorize-manage.ts‎

Lines changed: 0 additions & 41 deletions
This file was deleted.
Lines changed: 28 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,36 @@
1-
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
2-
import { createLogger } from '@sim/logger'
3-
import { getErrorMessage } from '@sim/utils/errors'
4-
import type { NextRequest } from 'next/server'
5-
import { NextResponse } from 'next/server'
61
import {
72
deleteCustomBlockContract,
83
updateCustomBlockContract,
94
} from '@/lib/api/contracts/custom-blocks'
10-
import { parseRequest } from '@/lib/api/server'
11-
import { getSession } from '@/lib/auth'
12-
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
135
import {
14-
CustomBlockValidationError,
15-
deleteCustomBlock,
16-
getCustomBlockUsageCounts,
17-
updateCustomBlock,
18-
} from '@/lib/workflows/custom-blocks/operations'
19-
import { authorizeManage } from '@/app/api/custom-blocks/[id]/authorize-manage'
20-
21-
const logger = createLogger('CustomBlockAPI')
22-
23-
type RouteContext = { params: Promise<{ id: string }> }
24-
25-
export const PATCH = withRouteHandler(async (request: NextRequest, context: RouteContext) => {
26-
const session = await getSession()
27-
if (!session?.user?.id) {
28-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
29-
}
30-
31-
const parsed = await parseRequest(updateCustomBlockContract, request, context)
32-
if (!parsed.success) return parsed.response
33-
34-
const { id } = parsed.data.params
35-
const authz = await authorizeManage(session.user.id, id)
36-
if (authz.error) return authz.error
37-
const { ctx } = authz
38-
39-
const { name, description, enabled, iconUrl, inputs, exposedOutputs, traceChildRuns } =
40-
parsed.data.body
41-
try {
42-
await updateCustomBlock(id, {
43-
name,
44-
description,
45-
enabled,
46-
inputs,
47-
iconUrl,
48-
exposedOutputs,
49-
traceChildRuns,
50-
})
51-
recordAudit({
52-
workspaceId: ctx.sourceWorkspaceId,
53-
actorId: session.user.id,
54-
actorName: session.user.name,
55-
actorEmail: session.user.email,
56-
action: AuditAction.CUSTOM_BLOCK_UPDATED,
57-
resourceType: AuditResourceType.CUSTOM_BLOCK,
58-
resourceId: id,
59-
resourceName: name ?? ctx.name,
60-
description: `Updated custom block "${name ?? ctx.name}"`,
61-
metadata: { organizationId: ctx.organizationId, type: ctx.type },
62-
request,
63-
})
64-
return NextResponse.json({ success: true as const })
65-
} catch (error) {
66-
if (error instanceof CustomBlockValidationError) {
67-
return NextResponse.json({ error: error.message }, { status: 400 })
68-
}
69-
logger.error('Failed to update custom block', { id, error: getErrorMessage(error) })
70-
throw error
71-
}
6+
defineInternalJsonRoute,
7+
internalRateLimits,
8+
internalSessionAuth,
9+
} from '@/lib/api/server/routes'
10+
import {
11+
customBlockSettingsOperations,
12+
deleteCustomBlockSettings,
13+
updateCustomBlockSettings,
14+
} from '@/lib/workflows/custom-blocks/application/settings'
15+
import { customBlockError } from '@/app/api/custom-blocks/errors'
16+
17+
export const PATCH = defineInternalJsonRoute({
18+
contract: updateCustomBlockContract,
19+
auth: internalSessionAuth,
20+
operation: customBlockSettingsOperations.update,
21+
rateLimit: internalRateLimits.none({ reason: 'Preserve existing custom block update policy.' }),
22+
errorPolicy: { project: customBlockError },
23+
mapInput: ({ params, body }) => ({ id: params.id, patch: body }),
24+
useCase: updateCustomBlockSettings,
7225
})
7326

74-
export const DELETE = withRouteHandler(async (request: NextRequest, context: RouteContext) => {
75-
const session = await getSession()
76-
if (!session?.user?.id) {
77-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
78-
}
79-
80-
const parsed = await parseRequest(deleteCustomBlockContract, request, context)
81-
if (!parsed.success) return parsed.response
82-
83-
const { id } = parsed.data.params
84-
const authz = await authorizeManage(session.user.id, id)
85-
if (authz.error) return authz.error
86-
const { ctx } = authz
87-
88-
const usageCounts = await getCustomBlockUsageCounts(ctx.organizationId, ctx.type)
89-
await deleteCustomBlock(id)
90-
recordAudit({
91-
workspaceId: ctx.sourceWorkspaceId,
92-
actorId: session.user.id,
93-
actorName: session.user.name,
94-
actorEmail: session.user.email,
95-
action: AuditAction.CUSTOM_BLOCK_DELETED,
96-
resourceType: AuditResourceType.CUSTOM_BLOCK,
97-
resourceId: id,
98-
resourceName: ctx.name,
99-
description: `Unpublished custom block "${ctx.name}"`,
100-
metadata: {
101-
organizationId: ctx.organizationId,
102-
type: ctx.type,
103-
usageCount: usageCounts.usageCount,
104-
deployedUsageCount: usageCounts.deployedUsageCount,
105-
},
106-
request,
107-
})
108-
return NextResponse.json({ success: true as const })
27+
export const DELETE = defineInternalJsonRoute({
28+
contract: deleteCustomBlockContract,
29+
auth: internalSessionAuth,
30+
operation: customBlockSettingsOperations.delete,
31+
rateLimit: internalRateLimits.none({ reason: 'Preserve existing custom block deletion policy.' }),
32+
errorPolicy: { project: customBlockError },
33+
mapInput: ({ params }) => params,
34+
useCase: deleteCustomBlockSettings,
35+
present: () => ({ success: true as const }),
10936
})

‎apps/sim/app/api/custom-blocks/[id]/usages/route.test.ts‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,21 @@ const { mockHasWorkspaceAdminAccess, mockOperations } = vi.hoisted(() => ({
1010
getCustomBlockManageContext: vi.fn(),
1111
getCustomBlockUsageCounts: vi.fn(),
1212
isCustomBlocksDeploymentEnabled: vi.fn(),
13+
CustomBlockValidationError: class extends Error {},
1314
},
1415
}))
1516

16-
vi.mock('@/lib/workspaces/permissions/utils', () => ({
17-
hasWorkspaceAdminAccess: mockHasWorkspaceAdminAccess,
17+
vi.mock('@sim/platform-authz/workspace', async (importOriginal) => ({
18+
...(await importOriginal<typeof import('@sim/platform-authz/workspace')>()),
19+
resolveEffectiveWorkspacePermission: async (...args: unknown[]) =>
20+
(await mockHasWorkspaceAdminAccess(...args)) ? 'admin' : 'read',
21+
}))
22+
vi.mock('@/lib/workspaces/application/workspace-context', () => ({
23+
resolveActiveWorkspaceApplicationContext: async (workspaceId: string) => ({
24+
workspaceId,
25+
workspaceOrganizationId: 'org-1',
26+
allowPersonalApiKeys: true,
27+
}),
1828
}))
1929

2030
vi.mock('@/lib/workflows/custom-blocks/operations', () => mockOperations)
@@ -39,7 +49,7 @@ function callRoute(id = 'cb-1') {
3949
describe('GET /api/custom-blocks/[id]/usages', () => {
4050
beforeEach(() => {
4151
vi.clearAllMocks()
42-
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
52+
mockGetSession.mockResolvedValue({ user: { id: 'user-1' }, session: { id: 'session-1' } })
4353
mockHasWorkspaceAdminAccess.mockResolvedValue(true)
4454
mockOperations.getCustomBlockManageContext.mockResolvedValue(MANAGE_CONTEXT)
4555
mockOperations.getCustomBlockUsageCounts.mockResolvedValue(USAGE_COUNTS)
Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
1-
import type { NextRequest } from 'next/server'
2-
import { NextResponse } from 'next/server'
31
import { getCustomBlockUsageCountsContract } from '@/lib/api/contracts/custom-blocks'
4-
import { parseRequest } from '@/lib/api/server'
5-
import { getSession } from '@/lib/auth'
6-
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
7-
import { getCustomBlockUsageCounts } from '@/lib/workflows/custom-blocks/operations'
8-
import { authorizeManage } from '@/app/api/custom-blocks/[id]/authorize-manage'
2+
import {
3+
defineInternalJsonRoute,
4+
internalRateLimits,
5+
internalSessionAuth,
6+
} from '@/lib/api/server/routes'
7+
import {
8+
customBlockSettingsOperations,
9+
readCustomBlockUsages,
10+
} from '@/lib/workflows/custom-blocks/application/settings'
11+
import { customBlockError } from '@/app/api/custom-blocks/errors'
912

10-
type RouteContext = { params: Promise<{ id: string }> }
11-
12-
export const GET = withRouteHandler(async (request: NextRequest, context: RouteContext) => {
13-
const session = await getSession()
14-
if (!session?.user?.id) {
15-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
16-
}
17-
18-
const parsed = await parseRequest(getCustomBlockUsageCountsContract, request, context)
19-
if (!parsed.success) return parsed.response
20-
21-
const authz = await authorizeManage(session.user.id, parsed.data.params.id)
22-
if (authz.error) return authz.error
23-
24-
const counts = await getCustomBlockUsageCounts(authz.ctx.organizationId, authz.ctx.type)
25-
return NextResponse.json(counts)
13+
export const GET = defineInternalJsonRoute({
14+
contract: getCustomBlockUsageCountsContract,
15+
auth: internalSessionAuth,
16+
operation: customBlockSettingsOperations.usages,
17+
rateLimit: internalRateLimits.none({
18+
reason: 'Preserve existing custom block usage read policy.',
19+
}),
20+
errorPolicy: { project: customBlockError },
21+
mapInput: ({ params }) => params,
22+
useCase: readCustomBlockUsages,
2623
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { internalErrorResponse } from '@/lib/api/server/routes'
2+
import { InternalUnauthenticatedError } from '@/lib/api/server/routes/internal-json-route'
3+
import {
4+
InsufficientWorkspacePermissionsError,
5+
NoWorkspaceAccessError,
6+
} from '@/lib/core/application/workspace-authorization'
7+
import { OrchestrationError, statusForOrchestrationError } from '@/lib/core/orchestration/types'
8+
import { CustomBlockValidationError } from '@/lib/workflows/custom-blocks/operations'
9+
10+
export function customBlockError(error: unknown, read = false) {
11+
if (error instanceof InternalUnauthenticatedError)
12+
return internalErrorResponse(401, { error: 'Unauthorized' })
13+
if (
14+
error instanceof InsufficientWorkspacePermissionsError ||
15+
error instanceof NoWorkspaceAccessError
16+
)
17+
return internalErrorResponse(403, {
18+
error: read ? 'Access denied' : 'Admin permissions required',
19+
})
20+
if (error instanceof CustomBlockValidationError)
21+
return internalErrorResponse(400, { error: error.message })
22+
if (error instanceof OrchestrationError)
23+
return internalErrorResponse(statusForOrchestrationError(error.code), { error: error.message })
24+
return null
25+
}

0 commit comments

Comments
 (0)