Skip to content

Commit a0b120c

Browse files
fix(credentials): scope personal connections to organizations (#7598)
* fix(credentials): use organization groups for personal connections * fix(credentials): scope GitLab personal tokens to organizations * fix(credentials): polish connection hint and provider picker sizing
1 parent 6322ac4 commit a0b120c

33 files changed

Lines changed: 1294 additions & 195 deletions

apps/sim/app/api/credentials/[id]/route.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export const GET = defineInternalJsonRoute({
3030
rateLimit,
3131
errorPolicy: internalCredentialDetailErrorPolicy,
3232
parseOptions: credentialValidationParseOptions,
33-
mapInput: ({ params }) => ({ credentialId: params.id }),
33+
mapInput: ({ params, query }) => ({
34+
credentialId: params.id,
35+
...(query.workspaceId ? { assertedWorkspaceId: query.workspaceId } : {}),
36+
}),
3437
useCase: getWorkspaceCredentialUseCase,
3538
present: ({ credential, access }) => ({
3639
credential: toWorkspaceCredential(credential, access),
@@ -44,7 +47,11 @@ export const PUT = defineInternalJsonRoute({
4447
rateLimit,
4548
errorPolicy: internalCredentialErrorPolicy,
4649
parseOptions: credentialValidationParseOptions,
47-
mapInput: ({ params, body }) => ({ credentialId: params.id, ...body }),
50+
mapInput: ({ params, body, query }) => ({
51+
credentialId: params.id,
52+
...body,
53+
...(query.workspaceId ? { assertedWorkspaceId: query.workspaceId } : {}),
54+
}),
4855
useCase: updateWorkspaceCredentialUseCase,
4956
present: ({ credential, access }) => ({
5057
credential: toWorkspaceCredential(credential, access),
@@ -58,7 +65,10 @@ export const DELETE = defineInternalJsonRoute({
5865
rateLimit,
5966
errorPolicy: internalCredentialErrorPolicy,
6067
parseOptions: credentialValidationParseOptions,
61-
mapInput: ({ params }) => ({ credentialId: params.id }),
68+
mapInput: ({ params, query }) => ({
69+
credentialId: params.id,
70+
...(query.workspaceId ? { workspaceId: query.workspaceId } : {}),
71+
}),
6272
useCase: deleteCredentialUseCase,
6373
present: () => ({ success: true as const }),
6474
})

apps/sim/app/workspace/[workspaceId]/components/credential-detail/hooks/use-credential-detail-form.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { useCallback, useState } from 'react'
44
import { toast } from '@sim/emcn'
55
import { createLogger } from '@sim/logger'
66
import { getErrorMessage } from '@sim/utils/errors'
7+
import { useUnsavedChangesGuard } from '@/app/workspace/[workspaceId]/components/credential-detail/hooks/use-unsaved-changes-guard'
78
import { useUpdateWorkspaceCredential, type WorkspaceCredential } from '@/hooks/queries/credentials'
8-
import { useUnsavedChangesGuard } from './use-unsaved-changes-guard'
99

1010
const logger = createLogger('CredentialDetailForm')
1111

@@ -26,6 +26,7 @@ export interface CredentialDetailFormSection {
2626
}
2727

2828
interface UseCredentialDetailFormParams {
29+
workspaceId?: string
2930
credential: WorkspaceCredential | null
3031
isAdmin: boolean
3132
/** Where the back link / discard navigates to. */
@@ -47,12 +48,13 @@ interface UseCredentialDetailFormParams {
4748
* into that one save and one guard.
4849
*/
4950
export function useCredentialDetailForm({
51+
workspaceId,
5052
credential,
5153
isAdmin,
5254
backHref,
5355
section,
5456
}: UseCredentialDetailFormParams) {
55-
const updateCredential = useUpdateWorkspaceCredential()
57+
const updateCredential = useUpdateWorkspaceCredential(workspaceId)
5658

5759
const [displayNameDraft, setDisplayNameDraft] = useState('')
5860
const [descriptionDraft, setDescriptionDraft] = useState('')

apps/sim/app/workspace/[workspaceId]/integrations/components/connect-personal-token-modal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function PersonalTokenForm({
4444
const [host, setHost] = useState(instanceUrl ? new URL(instanceUrl).host : 'gitlab.com')
4545
const [token, setToken] = useState('')
4646
const create = useCreateWorkspaceCredential()
47-
const update = useUpdateWorkspaceCredential()
47+
const update = useUpdateWorkspaceCredential(workspaceId)
4848
const pending = create.isPending || update.isPending
4949
const error = (credentialId ? update.error : create.error)?.message
5050
function submit() {
@@ -88,7 +88,7 @@ function PersonalTokenForm({
8888
type='custom'
8989
title='Personal access token'
9090
required
91-
hint='Use a token with the api scope. Only you can use this connection.'
91+
hint='Only you can use this connection.'
9292
>
9393
<SecretInput
9494
value={token}

apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export function ConnectedCredentialDetail({
8282
const { data: oauthConnections = [] } = useOAuthConnections()
8383
const connectOAuthService = useConnectOAuthService()
8484
const createDraft = useCreateCredentialDraft()
85-
const deleteCredential = useDeleteWorkspaceCredential()
85+
const deleteCredential = useDeleteWorkspaceCredential(workspaceId)
8686

8787
const credential = useMemo<WorkspaceCredential | null>(
8888
() => credentials.find((c) => c.id === credentialId) ?? null,
@@ -107,7 +107,12 @@ export function ConnectedCredentialDetail({
107107
const [isShareModalOpen, setIsShareModalOpen] = useState(false)
108108
const [reconnectOpen, setReconnectOpen] = useState(false)
109109

110-
const form = useCredentialDetailForm({ credential, isAdmin, backHref: integrationsHref })
110+
const form = useCredentialDetailForm({
111+
credential,
112+
isAdmin,
113+
backHref: integrationsHref,
114+
workspaceId,
115+
})
111116

112117
const oauthServiceNameByProviderId = useMemo(
113118
() => new Map(oauthConnections.map((service) => [service.providerId, service.name])),

apps/sim/ee/credential-groups/components/organization-account-provider-catalog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export function OrganizationAccountProviderCatalog({
8484
<ChipModalHeader onClose={onClose} closeDisabled={pending}>
8585
Add provider
8686
</ChipModalHeader>
87-
<ChipModalBody className='h-[480px] max-h-[70dvh] overflow-hidden'>
87+
<ChipModalBody className='h-[480px] max-h-[70dvh] flex-none overflow-hidden'>
8888
<ChipModalField
8989
type='custom'
9090
title='Find a provider'

apps/sim/hooks/queries/credentials.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from '@/lib/api/contracts/organization-credentials'
2727
import { environmentKeys } from '@/hooks/queries/environment'
2828
import { oauthConnectionsKeys } from '@/hooks/queries/oauth/oauth-connections'
29+
import { personalCredentialKeys } from '@/hooks/queries/personal-credentials'
2930
import { workspaceCredentialKeys } from '@/hooks/queries/utils/credential-keys'
3031
import { workspaceCredentialListQueryOptions } from '@/hooks/queries/utils/fetch-workspace-credentials'
3132
import { invalidateSelectorQueries } from '@/hooks/queries/utils/selector-keys'
@@ -60,13 +61,18 @@ export function useWorkspaceCredentials(params: {
6061
})
6162
}
6263

63-
export function useWorkspaceCredential(credentialId?: string, enabled = true) {
64+
export function useWorkspaceCredential(
65+
credentialId?: string,
66+
enabled = true,
67+
workspaceId?: string
68+
) {
6469
return useQuery<WorkspaceCredential | null>({
65-
queryKey: workspaceCredentialKeys.detail(credentialId),
70+
queryKey: workspaceCredentialKeys.detailForWorkspace(credentialId, workspaceId),
6671
queryFn: async ({ signal }) => {
6772
if (!credentialId) return null
6873
const data = await requestJson(getWorkspaceCredentialContract, {
6974
params: { id: credentialId },
75+
query: { workspaceId },
7076
signal,
7177
})
7278
return data.credential ?? null
@@ -103,6 +109,7 @@ export function useCreateWorkspaceCredential() {
103109
},
104110
onSettled: () =>
105111
Promise.all([
112+
queryClient.invalidateQueries({ queryKey: personalCredentialKeys.lists() }),
106113
queryClient.invalidateQueries({
107114
queryKey: workspaceCredentialKeys.lists(),
108115
}),
@@ -114,7 +121,7 @@ export function useCreateWorkspaceCredential() {
114121
})
115122
}
116123

117-
export function useUpdateWorkspaceCredential() {
124+
export function useUpdateWorkspaceCredential(workspaceId?: string) {
118125
const queryClient = useQueryClient()
119126

120127
return useMutation({
@@ -130,6 +137,7 @@ export function useUpdateWorkspaceCredential() {
130137
return requestJson(updateWorkspaceCredentialContract, {
131138
params: { id: credentialId },
132139
body,
140+
query: { workspaceId },
133141
})
134142
},
135143
onMutate: async (variables) => {
@@ -142,7 +150,7 @@ export function useUpdateWorkspaceCredential() {
142150
queryKey: workspaceCredentialKeys.lists(),
143151
})
144152
const previousDetail = queryClient.getQueryData<WorkspaceCredential | null>(
145-
workspaceCredentialKeys.detail(variables.credentialId)
153+
workspaceCredentialKeys.detailForWorkspace(variables.credentialId, workspaceId)
146154
)
147155

148156
/** Applies the in-flight edit to one cached credential. */
@@ -163,7 +171,7 @@ export function useUpdateWorkspaceCredential() {
163171
* Discard to restore the pre-save value over the committed one.
164172
*/
165173
queryClient.setQueryData<WorkspaceCredential | null>(
166-
workspaceCredentialKeys.detail(variables.credentialId),
174+
workspaceCredentialKeys.detailForWorkspace(variables.credentialId, workspaceId),
167175
(old) => (old ? withEdit(old) : old)
168176
)
169177

@@ -185,13 +193,14 @@ export function useUpdateWorkspaceCredential() {
185193
}
186194
if (context?.previousDetail !== undefined) {
187195
queryClient.setQueryData(
188-
workspaceCredentialKeys.detail(variables.credentialId),
196+
workspaceCredentialKeys.detailForWorkspace(variables.credentialId, workspaceId),
189197
context.previousDetail
190198
)
191199
}
192200
},
193201
onSettled: (_data, _error, variables) =>
194202
Promise.all([
203+
queryClient.invalidateQueries({ queryKey: personalCredentialKeys.lists() }),
195204
queryClient.invalidateQueries({
196205
queryKey: workspaceCredentialKeys.detail(variables.credentialId),
197206
}),
@@ -206,15 +215,19 @@ export function useUpdateWorkspaceCredential() {
206215
})
207216
}
208217

209-
export function useDeleteWorkspaceCredential() {
218+
export function useDeleteWorkspaceCredential(workspaceId?: string) {
210219
const queryClient = useQueryClient()
211220

212221
return useMutation({
213222
mutationFn: async (credentialId: string) => {
214-
return requestJson(deleteWorkspaceCredentialContract, { params: { id: credentialId } })
223+
return requestJson(deleteWorkspaceCredentialContract, {
224+
params: { id: credentialId },
225+
query: { workspaceId },
226+
})
215227
},
216228
onSettled: (_data, _error, credentialId) =>
217229
Promise.all([
230+
queryClient.invalidateQueries({ queryKey: personalCredentialKeys.lists() }),
218231
queryClient.invalidateQueries({ queryKey: workspaceCredentialKeys.detail(credentialId) }),
219232
queryClient.invalidateQueries({ queryKey: workspaceCredentialKeys.lists() }),
220233
queryClient.invalidateQueries({ queryKey: OAUTH_CREDENTIALS_KEY }),

apps/sim/hooks/queries/scoped-credentials.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ export function useUpdateScopedCredential() {
105105
params: { id: credentialId },
106106
body: { ...body, organizationId: body.organizationId },
107107
})
108-
return requestJson(updateWorkspaceCredentialContract, { params: { id: credentialId }, body })
108+
return requestJson(updateWorkspaceCredentialContract, {
109+
params: { id: credentialId },
110+
body,
111+
query: { workspaceId: 'workspaceId' in input ? input.workspaceId : undefined },
112+
})
109113
},
110114
onSuccess: reconcile,
111115
})

apps/sim/hooks/queries/utils/credential-keys.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export const workspaceCredentialKeys = {
1919
details: () => [...workspaceCredentialKeys.all, 'detail'] as const,
2020
detail: (credentialId?: string) =>
2121
[...workspaceCredentialKeys.details(), credentialId ?? 'none'] as const,
22+
detailForWorkspace: (credentialId?: string, workspaceId?: string) =>
23+
workspaceId
24+
? ([...workspaceCredentialKeys.detail(credentialId), 'workspace', workspaceId] as const)
25+
: workspaceCredentialKeys.detail(credentialId),
2226
members: (credentialId?: string) =>
2327
[...workspaceCredentialKeys.detail(credentialId), 'members'] as const,
2428
/**

apps/sim/lib/api/contracts/credentials.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from 'zod'
2-
import { workspaceIdSchema } from '@/lib/api/contracts/primitives'
2+
import { organizationIdSchema, workspaceIdSchema } from '@/lib/api/contracts/primitives'
33
import { defineRouteContract } from '@/lib/api/contracts/types'
44
import {
55
ATLASSIAN_PRODUCTS,
@@ -36,7 +36,8 @@ export const workspaceCredentialRoleSchema = z.enum(['admin', 'member'])
3636
export const workspaceCredentialMemberStatusSchema = z.enum(['active', 'pending', 'revoked'])
3737
export const workspaceCredentialSchema = z.object({
3838
id: z.string(),
39-
workspaceId: z.string(),
39+
workspaceId: z.string().nullable(),
40+
organizationId: organizationIdSchema.optional(),
4041
type: workspaceCredentialTypeSchema,
4142
displayName: z.string(),
4243
description: z.string().nullable(),
@@ -466,6 +467,7 @@ export const getWorkspaceCredentialContract = defineRouteContract({
466467
method: 'GET',
467468
path: '/api/credentials/[id]',
468469
params: credentialIdParamsSchema,
470+
query: z.object({ workspaceId: workspaceIdSchema.optional() }),
469471
response: {
470472
mode: 'json',
471473
schema: z.object({
@@ -528,6 +530,7 @@ export const updateWorkspaceCredentialContract = defineRouteContract({
528530
method: 'PUT',
529531
path: '/api/credentials/[id]',
530532
params: credentialIdParamsSchema,
533+
query: z.object({ workspaceId: workspaceIdSchema.optional() }),
531534
body: updateCredentialByIdBodySchema,
532535
response: {
533536
mode: 'json',
@@ -541,6 +544,7 @@ export const deleteWorkspaceCredentialContract = defineRouteContract({
541544
method: 'DELETE',
542545
path: '/api/credentials/[id]',
543546
params: credentialIdParamsSchema,
547+
query: z.object({ workspaceId: workspaceIdSchema.optional() }),
544548
response: {
545549
mode: 'json',
546550
schema: z.object({

0 commit comments

Comments
 (0)