Skip to content

Commit 00a83d0

Browse files
committed
feat(projects): add project identity and lifecycle foundation
1 parent 70412cc commit 00a83d0

57 files changed

Lines changed: 33404 additions & 341 deletions

Some content is hidden

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

‎apps/docs/openapi-v2-resources.json‎

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15996,6 +15996,16 @@
1599615996
"disableKnowledgeBaseExport": {
1599715997
"type": "boolean",
1599815998
"description": "Prevent downloading a whole knowledge base as an archive."
15999+
},
16000+
"deniedPartialAccessProjectIssues": {
16001+
"default": [],
16002+
"type": "array",
16003+
"items": {
16004+
"type": "string",
16005+
"minLength": 1,
16006+
"maxLength": 255
16007+
},
16008+
"description": "Issues in the listed Projects are unavailable to teammates without access to every active environment."
1599916009
}
1600016010
},
1600116011
"required": [
@@ -16040,7 +16050,8 @@
1604016050
"disableToolAutoApproval",
1604116051
"hideSandboxesTab",
1604216052
"disableOAuthAppAccess",
16043-
"disableKnowledgeBaseExport"
16053+
"disableKnowledgeBaseExport",
16054+
"deniedPartialAccessProjectIssues"
1604416055
],
1604516056
"additionalProperties": false,
1604616057
"description": "Resolved restrictions. True disables a boolean capability; null allowlists permit every value and empty allowlists permit none."
@@ -16170,7 +16181,8 @@
1617016181
"disableToolAutoApproval": false,
1617116182
"hideSandboxesTab": false,
1617216183
"disableOAuthAppAccess": false,
16173-
"disableKnowledgeBaseExport": false
16184+
"disableKnowledgeBaseExport": false,
16185+
"deniedPartialAccessProjectIssues": []
1617416186
},
1617516187
"isDefault": false,
1617616188
"membershipMode": "inherit",
@@ -16245,7 +16257,8 @@
1624516257
"disableToolAutoApproval": false,
1624616258
"hideSandboxesTab": false,
1624716259
"disableOAuthAppAccess": false,
16248-
"disableKnowledgeBaseExport": false
16260+
"disableKnowledgeBaseExport": false,
16261+
"deniedPartialAccessProjectIssues": []
1624916262
},
1625016263
"isDefault": false,
1625116264
"membershipMode": "inherit",
@@ -16499,6 +16512,15 @@
1649916512
"disableKnowledgeBaseExport": {
1650016513
"type": "boolean",
1650116514
"description": "Prevent downloading a whole knowledge base as an archive."
16515+
},
16516+
"deniedPartialAccessProjectIssues": {
16517+
"type": "array",
16518+
"items": {
16519+
"type": "string",
16520+
"minLength": 1,
16521+
"maxLength": 255
16522+
},
16523+
"description": "Issues in the listed Projects are unavailable to teammates without access to every active environment."
1650216524
}
1650316525
},
1650416526
"additionalProperties": false,
@@ -16590,7 +16612,8 @@
1659016612
"disableToolAutoApproval": false,
1659116613
"hideSandboxesTab": false,
1659216614
"disableOAuthAppAccess": false,
16593-
"disableKnowledgeBaseExport": false
16615+
"disableKnowledgeBaseExport": false,
16616+
"deniedPartialAccessProjectIssues": []
1659416617
},
1659516618
"isDefault": false,
1659616619
"membershipMode": "inherit",
@@ -16663,7 +16686,8 @@
1666316686
"disableToolAutoApproval": false,
1666416687
"hideSandboxesTab": false,
1666516688
"disableOAuthAppAccess": false,
16666-
"disableKnowledgeBaseExport": false
16689+
"disableKnowledgeBaseExport": false,
16690+
"deniedPartialAccessProjectIssues": []
1666716691
},
1666816692
"isDefault": false,
1666916693
"membershipMode": "inherit",
@@ -16924,6 +16948,15 @@
1692416948
"disableKnowledgeBaseExport": {
1692516949
"type": "boolean",
1692616950
"description": "Prevent downloading a whole knowledge base as an archive."
16951+
},
16952+
"deniedPartialAccessProjectIssues": {
16953+
"type": "array",
16954+
"items": {
16955+
"type": "string",
16956+
"minLength": 1,
16957+
"maxLength": 255
16958+
},
16959+
"description": "Issues in the listed Projects are unavailable to teammates without access to every active environment."
1692716960
}
1692816961
},
1692916962
"additionalProperties": false,
@@ -18200,6 +18233,16 @@
1820018233
"disableKnowledgeBaseExport": {
1820118234
"type": "boolean",
1820218235
"description": "Prevent downloading a whole knowledge base as an archive."
18236+
},
18237+
"deniedPartialAccessProjectIssues": {
18238+
"default": [],
18239+
"type": "array",
18240+
"items": {
18241+
"type": "string",
18242+
"minLength": 1,
18243+
"maxLength": 255
18244+
},
18245+
"description": "Issues in the listed Projects are unavailable to teammates without access to every active environment."
1820318246
}
1820418247
},
1820518248
"required": [
@@ -18244,7 +18287,8 @@
1824418287
"disableToolAutoApproval",
1824518288
"hideSandboxesTab",
1824618289
"disableOAuthAppAccess",
18247-
"disableKnowledgeBaseExport"
18290+
"disableKnowledgeBaseExport",
18291+
"deniedPartialAccessProjectIssues"
1824818292
],
1824918293
"additionalProperties": false
1825018294
},
@@ -20729,7 +20773,7 @@
2072920773
"description": "Access request being reviewed."
2073020774
},
2073120775
"changes": {
20732-
"maxItems": 42,
20776+
"maxItems": 43,
2073320777
"type": "array",
2073420778
"items": {
2073520779
"type": "object",
@@ -20778,7 +20822,8 @@
2077820822
"disableToolAutoApproval",
2077920823
"hideSandboxesTab",
2078020824
"disableOAuthAppAccess",
20781-
"disableKnowledgeBaseExport"
20825+
"disableKnowledgeBaseExport",
20826+
"deniedPartialAccessProjectIssues"
2078220827
],
2078320828
"description": "Permission restriction changed by approval."
2078420829
},
@@ -21290,7 +21335,7 @@
2129021335
"description": "Access request being reviewed."
2129121336
},
2129221337
"changes": {
21293-
"maxItems": 42,
21338+
"maxItems": 43,
2129421339
"type": "array",
2129521340
"items": {
2129621341
"type": "object",
@@ -21339,7 +21384,8 @@
2133921384
"disableToolAutoApproval",
2134021385
"hideSandboxesTab",
2134121386
"disableOAuthAppAccess",
21342-
"disableKnowledgeBaseExport"
21387+
"disableKnowledgeBaseExport",
21388+
"deniedPartialAccessProjectIssues"
2134321389
],
2134421390
"description": "Permission restriction changed by approval."
2134521391
},
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
archiveProjectContract,
3+
getProjectContract,
4+
renameProjectContract,
5+
} from '@/lib/api/contracts/projects'
6+
import {
7+
defineInternalJsonRoute,
8+
internalOrchestrationErrorPolicy,
9+
internalRateLimits,
10+
internalSessionAuth,
11+
} from '@/lib/api/server/routes'
12+
import {
13+
archiveProject,
14+
getProject,
15+
projectOperations,
16+
renameProject,
17+
} from '@/lib/projects/application'
18+
19+
export const GET = defineInternalJsonRoute({
20+
contract: getProjectContract,
21+
auth: internalSessionAuth,
22+
operation: projectOperations.get,
23+
rateLimit: internalRateLimits.user({ bucketName: 'projects.read' }),
24+
errorPolicy: internalOrchestrationErrorPolicy,
25+
mapInput: ({ params, query }) => ({ projectId: params.id, ...query }),
26+
useCase: getProject,
27+
})
28+
export const PATCH = defineInternalJsonRoute({
29+
contract: renameProjectContract,
30+
auth: internalSessionAuth,
31+
operation: projectOperations.rename,
32+
rateLimit: internalRateLimits.user({ bucketName: 'projects.write' }),
33+
errorPolicy: internalOrchestrationErrorPolicy,
34+
mapInput: ({ params, body }) => ({ projectId: params.id, name: body.name }),
35+
useCase: renameProject,
36+
})
37+
export const DELETE = defineInternalJsonRoute({
38+
contract: archiveProjectContract,
39+
auth: internalSessionAuth,
40+
operation: projectOperations.archive,
41+
rateLimit: internalRateLimits.user({ bucketName: 'projects.write' }),
42+
errorPolicy: internalOrchestrationErrorPolicy,
43+
mapInput: ({ params }) => ({ projectId: params.id }),
44+
useCase: archiveProject,
45+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { getWorkspaceProjectContract } from '@/lib/api/contracts/projects'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { getWorkspaceProject, projectOperations } from '@/lib/projects/application'
9+
10+
export const GET = defineInternalJsonRoute({
11+
contract: getWorkspaceProjectContract,
12+
auth: internalSessionAuth,
13+
operation: projectOperations.get,
14+
rateLimit: internalRateLimits.user({ bucketName: 'projects.read' }),
15+
errorPolicy: internalOrchestrationErrorPolicy,
16+
mapInput: ({ params }) => params,
17+
useCase: getWorkspaceProject,
18+
})

‎apps/sim/app/api/projects/route.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { listProjectsContract } from '@/lib/api/contracts/projects'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { listProjects, projectOperations } from '@/lib/projects/application'
9+
10+
export const GET = defineInternalJsonRoute({
11+
contract: listProjectsContract,
12+
auth: internalSessionAuth,
13+
operation: projectOperations.list,
14+
rateLimit: internalRateLimits.user({ bucketName: 'projects.read' }),
15+
errorPolicy: internalOrchestrationErrorPolicy,
16+
mapInput: ({ query }) => query,
17+
useCase: listProjects,
18+
})

‎apps/sim/app/api/workflows/route.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ describe('Workflows API Route - POST ordering', () => {
4545

4646
beforeEach(() => {
4747
resetDbChainMock()
48+
queueTableRows(schemaMock.workspace, [
49+
{ archivedAt: null, forkSyncNewWorkflowsExcluded: false },
50+
])
4851

4952
vi.stubGlobal('crypto', {
5053
randomUUID: vi.fn().mockReturnValue('workflow-new-id'),

‎apps/sim/ee/access-control/components/group-detail.tsx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import { getAllBlocks } from '@/blocks'
6161
import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay'
6262
import type { BlockConfig } from '@/blocks/types'
6363
import { CONNECTOR_META_REGISTRY } from '@/connectors/registry'
64+
import { ProjectIssueRestrictions } from '@/ee/access-control/components/project-issue-restrictions'
6465
import { WorkspaceSelect } from '@/ee/access-control/components/workspace-select'
6566
import {
6667
type PermissionGroup,
@@ -1783,6 +1784,16 @@ export function GroupDetail({
17831784

17841785
{configTab === 'platform' && (
17851786
<div className='flex flex-col gap-7'>
1787+
<ProjectIssueRestrictions
1788+
organizationId={organizationId}
1789+
value={editingConfig.deniedPartialAccessProjectIssues}
1790+
onChange={(value) =>
1791+
setEditingConfig((previous) => ({
1792+
...previous,
1793+
deniedPartialAccessProjectIssues: value,
1794+
}))
1795+
}
1796+
/>
17861797
<div className='flex items-center gap-2'>
17871798
<ChipInput
17881799
icon={Search}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use client'
2+
3+
import { Checkbox, Chip } from '@sim/emcn'
4+
import { useProjects } from '@/hooks/queries/projects'
5+
6+
interface ProjectIssueRestrictionsProps {
7+
organizationId: string
8+
value: string[]
9+
onChange: (value: string[]) => void
10+
}
11+
12+
/** Project choices use the authorized inventory; policy remains enforced at the application boundary. */
13+
export function ProjectIssueRestrictions({
14+
organizationId,
15+
value,
16+
onChange,
17+
}: ProjectIssueRestrictionsProps) {
18+
const projects = useProjects(organizationId)
19+
const selected = new Set(value)
20+
return (
21+
<div className='flex flex-col gap-2'>
22+
<p className='text-small'>Restrict Issues for partial-access teammates</p>
23+
<p className='text-[var(--text-muted)] text-small'>
24+
For selected Projects, teammates governed by this group need access to every active
25+
environment to use Issues.
26+
</p>
27+
{projects.isPending && <p className='text-small'>Loading Projects…</p>}
28+
{projects.error && (
29+
<p className='text-[var(--text-error)] text-small'>{projects.error.message}</p>
30+
)}
31+
{projects.data?.pages
32+
.flatMap((page) => page.projects)
33+
.map((project) => (
34+
<label
35+
htmlFor={`project-issues-${project.id}`}
36+
key={project.id}
37+
className='flex items-center gap-2'
38+
>
39+
<Checkbox
40+
id={`project-issues-${project.id}`}
41+
checked={selected.has(project.id)}
42+
onCheckedChange={(checked) =>
43+
onChange(
44+
checked === true
45+
? [...new Set([...value, project.id])]
46+
: value.filter((id) => id !== project.id)
47+
)
48+
}
49+
/>
50+
<span className='text-small'>{project.name}</span>
51+
</label>
52+
))}
53+
{projects.hasNextPage && (
54+
<Chip disabled={projects.isFetchingNextPage} onClick={() => void projects.fetchNextPage()}>
55+
Load more
56+
</Chip>
57+
)}
58+
</div>
59+
)
60+
}

0 commit comments

Comments
 (0)