Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
# FORKING_ENABLED= # Workspace forks
# CREDENTIAL_GROUPS= # Enterprise managed OAuth collections
# TABLE_ROW_TTL= # Table TTL columns and expired-row cleanup
# PERMISSION_ACCESS_REQUESTS_ENABLED= # Global access-request rollout; organizations may opt out
# KNOWLEDGE_MEMBER_ACCESS= # Per-member knowledge connectors and hybrid-by-default retrieval
# ORGANIZATIONS_ENABLED= / NEXT_PUBLIC_ORGANIZATIONS_ENABLED= # Organizations only

Expand Down
15 changes: 15 additions & 0 deletions apps/sim/app/access-requests/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ReactNode } from 'react'
import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar'

interface AccessRequestsLayoutProps {
children: ReactNode
}

export default function AccessRequestsLayout({ children }: AccessRequestsLayoutProps) {
return (
<div className='desktop-title-bar-page flex flex-col bg-[var(--bg)]'>
<DesktopTitleBarLane />
{children}
</div>
)
}
5 changes: 5 additions & 0 deletions apps/sim/app/access-requests/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AccessRequestsLoading } from '@/components/access-requests/access-requests-loading'

export default function Loading() {
return <AccessRequestsLoading />
}
70 changes: 70 additions & 0 deletions apps/sim/app/access-requests/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Suspense } from 'react'
import { ChipLink } from '@sim/emcn'
import type { Metadata } from 'next'
import { redirect } from 'next/navigation'
import { createSearchParamsCache } from 'nuqs/server'
import { AccessRequestsLoading } from '@/components/access-requests/access-requests-loading'
import { MyAccessRequests } from '@/components/access-requests/my-access-requests'
import { OrganizationAccessRequests } from '@/components/access-requests/organization-access-requests'
import { accessRequestEntrySearchParams } from '@/components/access-requests/search-params'
import { EmptyState } from '@/components/empty-state/empty-state'
import { getSession } from '@/lib/auth'
import { WORKSPACES_PATH } from '@/lib/navigation/paths'
import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect'

export const metadata: Metadata = {
title: 'Access requests',
robots: { index: false, follow: false },
}

interface AccessRequestsPageProps {
searchParams: Promise<Record<string, string | string[] | undefined>>
}

const entrySearchParams = createSearchParamsCache(accessRequestEntrySearchParams)

/** Session-only entry so access requests remain reachable outside the organization Search rollout. */
export default async function AccessRequestsPage({ searchParams }: AccessRequestsPageProps) {
const [rawParams, session] = await Promise.all([searchParams, getSession()])
const params = entrySearchParams.parse(rawParams)
const query = new URLSearchParams()
if (params.organizationId) query.set('organizationId', params.organizationId)
if (params.view !== 'requests') query.set('view', params.view)
if (params.requestId) query.set('requestId', params.requestId)
if (!session?.user) {
redirect(
buildAuthCrossLink('/login', {
callbackUrl: `/access-requests?${query}`,
isInviteFlow: false,
})
)
}

if (!params.organizationId) {
return (
<EmptyState
title='Choose an organization'
description='Open My access requests from your workspace menu.'
action={<ChipLink href={WORKSPACES_PATH}>Your workspaces</ChipLink>}
/>
)
}

return (
<Suspense fallback={<AccessRequestsLoading />}>
{params.view === 'admin' ? (
<main className='flex-1 px-6 py-8'>
<div className='mx-auto flex max-w-3xl flex-col gap-6'>
<div className='flex items-center justify-between gap-4'>
<h1 className='text-[var(--text-primary)] text-lg'>Access requests</h1>
<ChipLink href={WORKSPACES_PATH}>Your workspaces</ChipLink>
</div>
<OrganizationAccessRequests organizationId={params.organizationId} standalone />
</div>
</main>
) : (
<MyAccessRequests scope={{ kind: 'organization', organizationId: params.organizationId }} />
)}
</Suspense>
)
}
23 changes: 23 additions & 0 deletions apps/sim/app/api/access-requests/[requestId]/cancel/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { cancelAccessRequestContract } from '@/lib/api/contracts/access-requests'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
import { cancelAccessRequest } from '@/lib/permission-access-requests/application/requests'

export const POST = defineInternalJsonRoute({
contract: cancelAccessRequestContract,
auth: internalSessionAuth,
operation: accessRequestOperations.cancel,
rateLimit: internalRateLimits.user({
bucketName: 'access-requests:write',
config: { maxTokens: 10, refillRate: 5, refillIntervalMs: 60000 },
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ params, body }) => ({ requestId: params.requestId, scope: body.scope }),
useCase: cancelAccessRequest,
present: ({ request }) => ({ request }),
})
22 changes: 22 additions & 0 deletions apps/sim/app/api/access-requests/discovery/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { discoverAccessRequestsContract } from '@/lib/api/contracts/access-requests'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
import { discoverAccessRequests } from '@/lib/permission-access-requests/application/requests'

export const GET = defineInternalJsonRoute({
contract: discoverAccessRequestsContract,
auth: internalSessionAuth,
operation: accessRequestOperations.discover,
rateLimit: internalRateLimits.user({
bucketName: 'access-requests:read',
config: { maxTokens: 120, refillRate: 60, refillIntervalMs: 60000 },
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ query }) => query,
useCase: discoverAccessRequests,
})
47 changes: 47 additions & 0 deletions apps/sim/app/api/access-requests/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
createAccessRequestContract,
listMyAccessRequestsContract,
} from '@/lib/api/contracts/access-requests'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
import {
createAccessRequest,
listMyAccessRequests,
} from '@/lib/permission-access-requests/application/requests'

export const GET = defineInternalJsonRoute({
contract: listMyAccessRequestsContract,
auth: internalSessionAuth,
operation: accessRequestOperations.listMine,
rateLimit: internalRateLimits.user({
bucketName: 'access-requests:read',
config: { maxTokens: 120, refillRate: 60, refillIntervalMs: 60000 },
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ query }) => ({
scope: query,
limit: query.limit,
offset: query.offset,
requestId: query.requestId,
}),
useCase: listMyAccessRequests,
})

export const POST = defineInternalJsonRoute({
contract: createAccessRequestContract,
auth: internalSessionAuth,
operation: accessRequestOperations.create,
rateLimit: internalRateLimits.user({
bucketName: 'access-requests:write',
config: { maxTokens: 10, refillRate: 5, refillIntervalMs: 60000 },
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ body }) => body,
useCase: createAccessRequest,
present: ({ request }) => ({ request }),
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { previewAccessRequestContract } from '@/lib/api/contracts/access-requests'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
import { previewAccessRequest } from '@/lib/permission-access-requests/application/review'

export const GET = defineInternalJsonRoute({
contract: previewAccessRequestContract,
auth: internalSessionAuth,
operation: accessRequestOperations.preview,
rateLimit: internalRateLimits.user({
bucketName: 'access-requests:read',
config: { maxTokens: 120, refillRate: 60, refillIntervalMs: 60000 },
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ params }) => ({ organizationId: params.id, requestId: params.requestId }),
useCase: previewAccessRequest,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { resolveAccessRequestContract } from '@/lib/api/contracts/access-requests'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
import { resolveAccessRequest } from '@/lib/permission-access-requests/application/review'

export const POST = defineInternalJsonRoute({
contract: resolveAccessRequestContract,
auth: internalSessionAuth,
operation: accessRequestOperations.resolve,
rateLimit: internalRateLimits.user({
bucketName: 'access-requests:write',
config: { maxTokens: 10, refillRate: 5, refillIntervalMs: 60000 },
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ params, body }) => ({
organizationId: params.id,
requestId: params.requestId,
decision: body,
}),
useCase: resolveAccessRequest,
present: ({ request }) => ({ request }),
})
22 changes: 22 additions & 0 deletions apps/sim/app/api/organizations/[id]/access-requests/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { listOrganizationAccessRequestsContract } from '@/lib/api/contracts/access-requests'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
import { listOrganizationAccessRequests } from '@/lib/permission-access-requests/application/requests'

export const GET = defineInternalJsonRoute({
contract: listOrganizationAccessRequestsContract,
auth: internalSessionAuth,
operation: accessRequestOperations.listOrganization,
rateLimit: internalRateLimits.user({
bucketName: 'access-requests:read',
config: { maxTokens: 120, refillRate: 60, refillIntervalMs: 60000 },
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ params, query }) => ({ organizationId: params.id, ...query }),
useCase: listOrganizationAccessRequests,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
getAccessRequestSettingsContract,
updateAccessRequestSettingsContract,
} from '@/lib/api/contracts/access-requests'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { accessRequestOperations } from '@/lib/permission-access-requests/application/operations'
import {
getAccessRequestSettings,
updateAccessRequestSettings,
} from '@/lib/permission-access-requests/application/requests'

export const GET = defineInternalJsonRoute({
contract: getAccessRequestSettingsContract,
auth: internalSessionAuth,
operation: accessRequestOperations.getSettings,
rateLimit: internalRateLimits.user({
bucketName: 'access-requests:read',
config: { maxTokens: 120, refillRate: 60, refillIntervalMs: 60000 },
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ params }) => ({ organizationId: params.id }),
useCase: getAccessRequestSettings,
})

export const PATCH = defineInternalJsonRoute({
contract: updateAccessRequestSettingsContract,
auth: internalSessionAuth,
operation: accessRequestOperations.updateSettings,
rateLimit: internalRateLimits.user({
bucketName: 'access-requests:write',
config: { maxTokens: 10, refillRate: 5, refillIntervalMs: 60000 },
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ params, body }) => ({ organizationId: params.id, ...body }),
useCase: updateAccessRequestSettings,
})
2 changes: 2 additions & 0 deletions apps/sim/app/api/webhooks/outbox/process/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getConnectorFailureDiagnostic } from '@/lib/knowledge/connectors/connec
import { knowledgeDocumentProcessingOutboxHandlers } from '@/lib/knowledge/documents/processing-outbox-handler'
import { recoverKnowledgeDocumentProcessing } from '@/lib/knowledge/documents/processing-recovery'
import { organizationResourceCleanupOutboxHandlers } from '@/lib/organizations/resource-cleanup'
import { permissionAccessRequestOutboxHandlers } from '@/lib/permission-access-requests/notifications'
import { workspaceFileLiveDocOutboxHandlers } from '@/lib/uploads/contexts/workspace/workspace-file-live-doc-outbox'
import { workspaceFileStorageCleanupOutboxHandlers } from '@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox'
import { workflowDeploymentOutboxHandlers } from '@/lib/workflows/deployment-outbox'
Expand All @@ -44,6 +45,7 @@ const handlers = {
...directGrantOutboxHandlers,
...knowledgeDocumentProcessingOutboxHandlers,
...organizationResourceCleanupOutboxHandlers,
...permissionAccessRequestOutboxHandlers,
...workspaceFileLiveDocOutboxHandlers,
...workspaceFileStorageCleanupOutboxHandlers,
...workflowDeploymentOutboxHandlers,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AccessRequestsLoading } from '@/components/access-requests/access-requests-loading'

export default function Loading() {
return <AccessRequestsLoading />
}
19 changes: 19 additions & 0 deletions apps/sim/app/workspace/[workspaceId]/access-requests/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Suspense } from 'react'
import type { Metadata } from 'next'
import { AccessRequestsLoading } from '@/components/access-requests/access-requests-loading'
import { MyAccessRequests } from '@/components/access-requests/my-access-requests'

export const metadata: Metadata = { title: 'My access requests' }

interface AccessRequestsPageProps {
params: Promise<{ workspaceId: string }>
}

export default async function AccessRequestsPage({ params }: AccessRequestsPageProps) {
const { workspaceId } = await params
return (
<Suspense fallback={<AccessRequestsLoading />}>
<MyAccessRequests scope={{ kind: 'workspace', workspaceId }} />
</Suspense>
)
}
Loading
Loading