Skip to content

Commit 7216d96

Browse files
Merge remote-tracking branch 'origin/improvement/v2-endpoints' into codex/v2-application-knowledge
# Conflicts: # apps/sim/lib/api/server/routes/v2-json-route.ts # apps/sim/lib/folders/orchestration.ts
2 parents 8a5cea9 + d22e6bd commit 7216d96

183 files changed

Lines changed: 13762 additions & 7744 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.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { NextRequest } from 'next/server'
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
const mocks = vi.hoisted(() => ({
8+
getSession: vi.fn(),
9+
execute: vi.fn(),
10+
}))
11+
12+
vi.mock('@/lib/auth', () => ({ getSession: mocks.getSession }))
13+
14+
vi.mock('@/lib/audit-logs/application/list-audit-logs', () => ({
15+
listAuditLogs: { operation: { id: 'audit_logs.list' }, execute: mocks.execute },
16+
}))
17+
18+
import { OrchestrationError } from '@/lib/core/orchestration/types'
19+
import { GET } from '@/app/api/audit-logs/route'
20+
21+
const log = {
22+
id: 'audit-1',
23+
workspaceId: 'workspace-1',
24+
actorId: 'admin-1',
25+
actorName: 'Ada',
26+
actorEmail: 'ada@example.com',
27+
action: 'workspace.updated',
28+
resourceType: 'workspace',
29+
resourceId: 'workspace-1',
30+
resourceName: 'Engineering',
31+
description: null,
32+
metadata: {},
33+
createdAt: new Date('2026-08-01T00:00:00Z'),
34+
}
35+
36+
describe('GET /api/audit-logs', () => {
37+
beforeEach(() => {
38+
vi.clearAllMocks()
39+
mocks.getSession.mockResolvedValue({
40+
user: { id: 'admin-1' },
41+
session: { id: 'session-1' },
42+
})
43+
mocks.execute.mockResolvedValue({ data: [log], nextCursor: 'next-1' })
44+
})
45+
46+
it('authenticates before parsing the organization query', async () => {
47+
const response = await GET(new NextRequest('http://localhost:3000/api/audit-logs'))
48+
49+
expect(response.status).toBe(400)
50+
expect(mocks.getSession).toHaveBeenCalled()
51+
expect(mocks.execute).not.toHaveBeenCalled()
52+
})
53+
54+
it('keeps the internal envelope while sharing the application operation', async () => {
55+
const request = new NextRequest(
56+
'http://localhost:3000/api/audit-logs?organizationId=organization-1'
57+
)
58+
const response = await GET(request)
59+
60+
expect(response.status).toBe(200)
61+
expect(await response.json()).toMatchObject({
62+
success: true,
63+
data: [{ id: 'audit-1', actorId: 'admin-1' }],
64+
nextCursor: 'next-1',
65+
})
66+
expect(mocks.execute).toHaveBeenCalledWith({
67+
principal: { kind: 'session', userId: 'admin-1', sessionId: 'session-1' },
68+
input: expect.objectContaining({ organizationId: 'organization-1' }),
69+
request,
70+
})
71+
})
72+
73+
it('preserves internal typed error presentation', async () => {
74+
mocks.execute.mockRejectedValueOnce(new OrchestrationError('forbidden', 'Admin required'))
75+
76+
const response = await GET(
77+
new NextRequest('http://localhost:3000/api/audit-logs?organizationId=organization-1')
78+
)
79+
80+
expect(response.status).toBe(403)
81+
expect(await response.json()).toEqual({ error: 'Admin required' })
82+
})
83+
})
Lines changed: 36 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,42 @@
1-
import { createLogger } from '@sim/logger'
2-
import { getErrorMessage } from '@sim/utils/errors'
3-
import { type NextRequest, NextResponse } from 'next/server'
41
import { listAuditLogsContract } from '@/lib/api/contracts/audit-logs'
5-
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
6-
import { getSession } from '@/lib/auth'
7-
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8-
import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth'
9-
import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format'
102
import {
11-
buildFilterConditions,
12-
buildOrgScopeCondition,
13-
getOrgWorkspaceIds,
14-
queryAuditLogs,
15-
} from '@/app/api/v1/audit-logs/query'
16-
17-
const logger = createLogger('AuditLogsAPI')
3+
defineInternalJsonRoute,
4+
internalPlainOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { listAuditLogs } from '@/lib/audit-logs/application/list-audit-logs'
9+
import { auditLogOperations } from '@/lib/audit-logs/application/operations'
10+
import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format'
1811

1912
export const dynamic = 'force-dynamic'
2013

21-
export const GET = withRouteHandler(async (request: NextRequest) => {
22-
try {
23-
const session = await getSession()
24-
if (!session?.user?.id) {
25-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
26-
}
27-
28-
const parsed = await parseRequest(
29-
listAuditLogsContract,
30-
request,
31-
{},
32-
{
33-
validationErrorResponse: (error) =>
34-
NextResponse.json(
35-
{ error: getValidationErrorMessage(error, 'Invalid query parameters') },
36-
{ status: 400 }
37-
),
38-
}
39-
)
40-
if (!parsed.success) return parsed.response
41-
42-
const authResult = await validateEnterpriseAuditAccess(
43-
session.user.id,
44-
parsed.data.query.organizationId
45-
)
46-
if (!authResult.success) {
47-
return authResult.response
48-
}
49-
50-
const { organizationId, orgMemberIds } = authResult.context
51-
52-
const {
53-
organizationId: _targetOrganizationId,
54-
search,
55-
action,
56-
resourceType,
57-
actorId,
58-
startDate,
59-
endDate,
60-
includeDeparted,
61-
limit,
62-
cursor,
63-
} = parsed.data.query
64-
65-
const orgWorkspaceIds = await getOrgWorkspaceIds(organizationId)
66-
const scopeCondition = buildOrgScopeCondition({
67-
organizationId,
68-
orgWorkspaceIds,
69-
orgMemberIds,
70-
includeDeparted,
71-
})
72-
const filterConditions = buildFilterConditions({
73-
action,
74-
resourceType,
75-
actorId,
76-
search,
77-
startDate,
78-
endDate,
79-
})
80-
81-
const { data, nextCursor } = await queryAuditLogs(
82-
[scopeCondition, ...filterConditions],
83-
limit,
84-
cursor
85-
)
86-
87-
return NextResponse.json({
88-
success: true,
89-
data: data.map(formatAuditLogEntry),
90-
nextCursor,
91-
})
92-
} catch (error: unknown) {
93-
const message = getErrorMessage(error, 'Unknown error')
94-
logger.error('Audit logs fetch error', { error: message })
95-
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
96-
}
14+
export const GET = defineInternalJsonRoute({
15+
contract: listAuditLogsContract,
16+
auth: internalSessionAuth,
17+
operation: auditLogOperations.list,
18+
rateLimit: internalRateLimits.none({
19+
reason: 'Existing authenticated audit-log settings read has no request-rate policy',
20+
}),
21+
errorPolicy: internalPlainOrchestrationErrorPolicy,
22+
mapInput: ({ query }) => ({
23+
organizationId: query.organizationId,
24+
includeDeparted: query.includeDeparted,
25+
filters: {
26+
search: query.search,
27+
action: query.action,
28+
resourceType: query.resourceType,
29+
actorId: query.actorId,
30+
startDate: query.startDate,
31+
endDate: query.endDate,
32+
},
33+
limit: query.limit,
34+
cursor: query.cursor,
35+
}),
36+
useCase: listAuditLogs,
37+
present: ({ data, nextCursor }) => ({
38+
success: true,
39+
data: data.map(formatAuditLogEntry),
40+
nextCursor,
41+
}),
9742
})

0 commit comments

Comments
 (0)