Skip to content

Commit a5e7dae

Browse files
committed
feat(access-requests): request and review permission access
1 parent 1e7c3dc commit a5e7dae

109 files changed

Lines changed: 36800 additions & 141 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/.env.example‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
214214
# FORKING_ENABLED= # Workspace forks
215215
# CREDENTIAL_GROUPS= # Enterprise managed OAuth collections
216216
# TABLE_ROW_TTL= # Table TTL columns and expired-row cleanup
217+
# PERMISSION_ACCESS_REQUESTS_ENABLED= # Global access-request rollout; organizations may opt out
217218
# KNOWLEDGE_MEMBER_ACCESS= # Per-member knowledge connectors and hybrid-by-default retrieval
218219
# ORGANIZATIONS_ENABLED= / NEXT_PUBLIC_ORGANIZATIONS_ENABLED= # Organizations only
219220

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { AccessRequestsLoading } from '@/components/access-requests/access-requests-loading'
2+
3+
export default function Loading() {
4+
return <AccessRequestsLoading />
5+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Suspense } from 'react'
2+
import { ChipLink } from '@sim/emcn'
3+
import type { Metadata } from 'next'
4+
import { redirect } from 'next/navigation'
5+
import { createSearchParamsCache } from 'nuqs/server'
6+
import { AccessRequestsLoading } from '@/components/access-requests/access-requests-loading'
7+
import { MyAccessRequests } from '@/components/access-requests/my-access-requests'
8+
import { OrganizationAccessRequests } from '@/components/access-requests/organization-access-requests'
9+
import { accessRequestEntrySearchParams } from '@/components/access-requests/search-params'
10+
import { EmptyState } from '@/components/empty-state/empty-state'
11+
import { getSession } from '@/lib/auth'
12+
import { WORKSPACES_PATH } from '@/lib/navigation/paths'
13+
import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect'
14+
15+
export const metadata: Metadata = {
16+
title: 'Access requests',
17+
robots: { index: false, follow: false },
18+
}
19+
20+
interface AccessRequestsPageProps {
21+
searchParams: Promise<Record<string, string | string[] | undefined>>
22+
}
23+
24+
const entrySearchParams = createSearchParamsCache(accessRequestEntrySearchParams)
25+
26+
/** Session-only entry so access requests remain reachable outside the organization Search rollout. */
27+
export default async function AccessRequestsPage({ searchParams }: AccessRequestsPageProps) {
28+
const [rawParams, session] = await Promise.all([searchParams, getSession()])
29+
const params = entrySearchParams.parse(rawParams)
30+
const query = new URLSearchParams()
31+
if (params.organizationId) query.set('organizationId', params.organizationId)
32+
if (params.view !== 'requests') query.set('view', params.view)
33+
if (params.requestId) query.set('requestId', params.requestId)
34+
if (!session?.user) {
35+
redirect(
36+
buildAuthCrossLink('/login', {
37+
callbackUrl: `/access-requests?${query}`,
38+
isInviteFlow: false,
39+
})
40+
)
41+
}
42+
43+
if (!params.organizationId) {
44+
return (
45+
<EmptyState
46+
title='Choose an organization'
47+
description='Open My access requests from your workspace menu.'
48+
action={<ChipLink href={WORKSPACES_PATH}>Your workspaces</ChipLink>}
49+
/>
50+
)
51+
}
52+
53+
return (
54+
<Suspense fallback={<AccessRequestsLoading />}>
55+
{params.view === 'admin' ? (
56+
<main className='min-h-screen bg-[var(--bg)] px-6 py-8'>
57+
<div className='mx-auto flex max-w-3xl flex-col gap-6'>
58+
<div className='flex items-center justify-between gap-4'>
59+
<h1 className='text-[var(--text-primary)] text-lg'>Access requests</h1>
60+
<ChipLink href={WORKSPACES_PATH}>Your workspaces</ChipLink>
61+
</div>
62+
<OrganizationAccessRequests organizationId={params.organizationId} standalone />
63+
</div>
64+
</main>
65+
) : (
66+
<MyAccessRequests scope={{ kind: 'organization', organizationId: params.organizationId }} />
67+
)}
68+
</Suspense>
69+
)
70+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { cancelAccessRequestContract } from '@/lib/api/contracts/access-requests'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
9+
import { cancelAccessRequest } from '@/lib/permission-access-requests/application/requests'
10+
11+
export const POST = defineInternalJsonRoute({
12+
contract: cancelAccessRequestContract,
13+
auth: internalSessionAuth,
14+
operation: accessRequestOperations.cancel,
15+
rateLimit: internalRateLimits.user({
16+
bucketName: 'access-requests:write',
17+
config: { maxTokens: 10, refillRate: 5, refillIntervalMs: 60000 },
18+
}),
19+
errorPolicy: internalOrchestrationErrorPolicy,
20+
mapInput: ({ params, body }) => ({ requestId: params.requestId, scope: body.scope }),
21+
useCase: cancelAccessRequest,
22+
present: ({ request }) => ({ request }),
23+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { discoverAccessRequestsContract } from '@/lib/api/contracts/access-requests'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
9+
import { discoverAccessRequests } from '@/lib/permission-access-requests/application/requests'
10+
11+
export const GET = defineInternalJsonRoute({
12+
contract: discoverAccessRequestsContract,
13+
auth: internalSessionAuth,
14+
operation: accessRequestOperations.discover,
15+
rateLimit: internalRateLimits.user({
16+
bucketName: 'access-requests:read',
17+
config: { maxTokens: 120, refillRate: 60, refillIntervalMs: 60000 },
18+
}),
19+
errorPolicy: internalOrchestrationErrorPolicy,
20+
mapInput: ({ query }) => query,
21+
useCase: discoverAccessRequests,
22+
})
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
createAccessRequestContract,
3+
listMyAccessRequestsContract,
4+
} from '@/lib/api/contracts/access-requests'
5+
import {
6+
defineInternalJsonRoute,
7+
internalOrchestrationErrorPolicy,
8+
internalRateLimits,
9+
internalSessionAuth,
10+
} from '@/lib/api/server/routes'
11+
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
12+
import {
13+
createAccessRequest,
14+
listMyAccessRequests,
15+
} from '@/lib/permission-access-requests/application/requests'
16+
17+
export const GET = defineInternalJsonRoute({
18+
contract: listMyAccessRequestsContract,
19+
auth: internalSessionAuth,
20+
operation: accessRequestOperations.listMine,
21+
rateLimit: internalRateLimits.user({
22+
bucketName: 'access-requests:read',
23+
config: { maxTokens: 120, refillRate: 60, refillIntervalMs: 60000 },
24+
}),
25+
errorPolicy: internalOrchestrationErrorPolicy,
26+
mapInput: ({ query }) => ({
27+
scope: query,
28+
limit: query.limit,
29+
offset: query.offset,
30+
requestId: query.requestId,
31+
}),
32+
useCase: listMyAccessRequests,
33+
})
34+
35+
export const POST = defineInternalJsonRoute({
36+
contract: createAccessRequestContract,
37+
auth: internalSessionAuth,
38+
operation: accessRequestOperations.create,
39+
rateLimit: internalRateLimits.user({
40+
bucketName: 'access-requests:write',
41+
config: { maxTokens: 10, refillRate: 5, refillIntervalMs: 60000 },
42+
}),
43+
errorPolicy: internalOrchestrationErrorPolicy,
44+
mapInput: ({ body }) => body,
45+
useCase: createAccessRequest,
46+
present: ({ request }) => ({ request }),
47+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { previewAccessRequestContract } from '@/lib/api/contracts/access-requests'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
9+
import { previewAccessRequest } from '@/lib/permission-access-requests/application/review'
10+
11+
export const GET = defineInternalJsonRoute({
12+
contract: previewAccessRequestContract,
13+
auth: internalSessionAuth,
14+
operation: accessRequestOperations.preview,
15+
rateLimit: internalRateLimits.user({
16+
bucketName: 'access-requests:read',
17+
config: { maxTokens: 120, refillRate: 60, refillIntervalMs: 60000 },
18+
}),
19+
errorPolicy: internalOrchestrationErrorPolicy,
20+
mapInput: ({ params }) => ({ organizationId: params.id, requestId: params.requestId }),
21+
useCase: previewAccessRequest,
22+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { resolveAccessRequestContract } from '@/lib/api/contracts/access-requests'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
9+
import { resolveAccessRequest } from '@/lib/permission-access-requests/application/review'
10+
11+
export const POST = defineInternalJsonRoute({
12+
contract: resolveAccessRequestContract,
13+
auth: internalSessionAuth,
14+
operation: accessRequestOperations.resolve,
15+
rateLimit: internalRateLimits.user({
16+
bucketName: 'access-requests:write',
17+
config: { maxTokens: 10, refillRate: 5, refillIntervalMs: 60000 },
18+
}),
19+
errorPolicy: internalOrchestrationErrorPolicy,
20+
mapInput: ({ params, body }) => ({
21+
organizationId: params.id,
22+
requestId: params.requestId,
23+
decision: body,
24+
}),
25+
useCase: resolveAccessRequest,
26+
present: ({ request }) => ({ request }),
27+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { listOrganizationAccessRequestsContract } from '@/lib/api/contracts/access-requests'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
9+
import { listOrganizationAccessRequests } from '@/lib/permission-access-requests/application/requests'
10+
11+
export const GET = defineInternalJsonRoute({
12+
contract: listOrganizationAccessRequestsContract,
13+
auth: internalSessionAuth,
14+
operation: accessRequestOperations.listOrganization,
15+
rateLimit: internalRateLimits.user({
16+
bucketName: 'access-requests:read',
17+
config: { maxTokens: 120, refillRate: 60, refillIntervalMs: 60000 },
18+
}),
19+
errorPolicy: internalOrchestrationErrorPolicy,
20+
mapInput: ({ params, query }) => ({ organizationId: params.id, ...query }),
21+
useCase: listOrganizationAccessRequests,
22+
})
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {
2+
getAccessRequestSettingsContract,
3+
updateAccessRequestSettingsContract,
4+
} from '@/lib/api/contracts/access-requests'
5+
import {
6+
defineInternalJsonRoute,
7+
internalOrchestrationErrorPolicy,
8+
internalRateLimits,
9+
internalSessionAuth,
10+
} from '@/lib/api/server/routes'
11+
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
12+
import {
13+
getAccessRequestSettings,
14+
updateAccessRequestSettings,
15+
} from '@/lib/permission-access-requests/application/requests'
16+
17+
export const GET = defineInternalJsonRoute({
18+
contract: getAccessRequestSettingsContract,
19+
auth: internalSessionAuth,
20+
operation: accessRequestOperations.getSettings,
21+
rateLimit: internalRateLimits.user({
22+
bucketName: 'access-requests:read',
23+
config: { maxTokens: 120, refillRate: 60, refillIntervalMs: 60000 },
24+
}),
25+
errorPolicy: internalOrchestrationErrorPolicy,
26+
mapInput: ({ params }) => ({ organizationId: params.id }),
27+
useCase: getAccessRequestSettings,
28+
})
29+
30+
export const PATCH = defineInternalJsonRoute({
31+
contract: updateAccessRequestSettingsContract,
32+
auth: internalSessionAuth,
33+
operation: accessRequestOperations.updateSettings,
34+
rateLimit: internalRateLimits.user({
35+
bucketName: 'access-requests:write',
36+
config: { maxTokens: 10, refillRate: 5, refillIntervalMs: 60000 },
37+
}),
38+
errorPolicy: internalOrchestrationErrorPolicy,
39+
mapInput: ({ params, body }) => ({ organizationId: params.id, ...body }),
40+
useCase: updateAccessRequestSettings,
41+
})

0 commit comments

Comments
 (0)