1- import { AuditAction , AuditResourceType , recordAudit } from '@sim/audit'
21import { db } from '@sim/db'
3- import { credential , credentialMember , environment , workspaceEnvironment } from '@sim/db/schema'
2+ import { credential , credentialMember } from '@sim/db/schema'
43import { createLogger } from '@sim/logger'
5- import { generateId } from '@sim/utils/id'
64import { and , eq } from 'drizzle-orm'
75import { type NextRequest , NextResponse } from 'next/server'
86import { updateWorkspaceCredentialContract } from '@/lib/api/contracts/credentials'
97import { getValidationErrorMessage , parseRequest } from '@/lib/api/server'
108import { getSession } from '@/lib/auth'
11- import { encryptSecret } from '@/lib/core/security/encryption'
129import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1310import { getCredentialActorContext } from '@/lib/credentials/access'
14- import { deleteCredential } from '@/lib/credentials/deletion'
15- import {
16- deleteWorkspaceEnvCredentials ,
17- syncPersonalEnvCredentialsForUser ,
18- } from '@/lib/credentials/environment'
19- import { captureServerEvent } from '@/lib/posthog/server'
11+ import { performDeleteCredential , performUpdateCredential } from '@/lib/credentials/orchestration'
2012
2113const logger = createLogger ( 'CredentialByIdAPI' )
2214
@@ -93,93 +85,33 @@ export const PUT = withRouteHandler(
9385 const { id } = parsed . data . params
9486 const body = parsed . data . body
9587
96- const access = await getCredentialActorContext ( id , session . user . id )
97- if ( ! access . credential ) {
98- return NextResponse . json ( { error : 'Credential not found' } , { status : 404 } )
99- }
100- if ( ! access . hasWorkspaceAccess || ! access . isAdmin ) {
101- return NextResponse . json ( { error : 'Credential admin permission required' } , { status : 403 } )
102- }
103-
104- const updates : Record < string , unknown > = { }
105-
106- if ( body . description !== undefined ) {
107- updates . description = body . description ?? null
108- }
109-
110- if (
111- body . displayName !== undefined &&
112- ( access . credential . type === 'oauth' || access . credential . type === 'service_account' )
113- ) {
114- updates . displayName = body . displayName
115- }
116-
117- if ( body . serviceAccountJson !== undefined && access . credential . type === 'service_account' ) {
118- let parsedJson : Record < string , unknown >
119- try {
120- parsedJson = JSON . parse ( body . serviceAccountJson )
121- } catch {
122- return NextResponse . json ( { error : 'Invalid JSON format' } , { status : 400 } )
123- }
124- if (
125- parsedJson . type !== 'service_account' ||
126- typeof parsedJson . client_email !== 'string' ||
127- typeof parsedJson . private_key !== 'string' ||
128- typeof parsedJson . project_id !== 'string'
129- ) {
130- return NextResponse . json ( { error : 'Invalid service account JSON key' } , { status : 400 } )
131- }
132- const { encrypted } = await encryptSecret ( body . serviceAccountJson )
133- updates . encryptedServiceAccountKey = encrypted
134- }
135-
136- if ( Object . keys ( updates ) . length === 0 ) {
137- if ( access . credential . type === 'oauth' || access . credential . type === 'service_account' ) {
138- return NextResponse . json (
139- {
140- error : 'No updatable fields provided.' ,
141- } ,
142- { status : 400 }
143- )
144- }
145- return NextResponse . json (
146- {
147- error :
148- 'Environment credentials cannot be updated via this endpoint. Use the environment value editor in credentials settings.' ,
149- } ,
150- { status : 400 }
151- )
152- }
153-
154- updates . updatedAt = new Date ( )
155- await db . update ( credential ) . set ( updates ) . where ( eq ( credential . id , id ) )
156-
157- recordAudit ( {
158- workspaceId : access . credential . workspaceId ,
159- actorId : session . user . id ,
88+ const result = await performUpdateCredential ( {
89+ credentialId : id ,
90+ userId : session . user . id ,
16091 actorName : session . user . name ,
16192 actorEmail : session . user . email ,
162- action : AuditAction . CREDENTIAL_UPDATED ,
163- resourceType : AuditResourceType . CREDENTIAL ,
164- resourceId : id ,
165- resourceName : access . credential . displayName ,
166- description : `Updated ${ access . credential . type } credential "${ access . credential . displayName } "` ,
167- metadata : {
168- credentialType : access . credential . type ,
169- updatedFields : Object . keys ( updates ) . filter ( ( k ) => k !== 'updatedAt' ) ,
170- } ,
93+ displayName : body . displayName ,
94+ description : body . description ,
95+ serviceAccountJson : body . serviceAccountJson ,
17196 request,
17297 } )
98+ if ( ! result . success ) {
99+ const status =
100+ result . errorCode === 'not_found'
101+ ? 404
102+ : result . errorCode === 'forbidden'
103+ ? 403
104+ : result . errorCode === 'conflict'
105+ ? 409
106+ : result . errorCode === 'validation'
107+ ? 400
108+ : 500
109+ return NextResponse . json ( { error : result . error } , { status } )
110+ }
173111
174112 const row = await getCredentialResponse ( id , session . user . id )
175113 return NextResponse . json ( { credential : row } , { status : 200 } )
176114 } catch ( error ) {
177- if ( error instanceof Error && error . message . includes ( 'unique' ) ) {
178- return NextResponse . json (
179- { error : 'A service account credential with this name already exists in the workspace' } ,
180- { status : 409 }
181- )
182- }
183115 logger . error ( 'Failed to update credential' , error )
184116 return NextResponse . json ( { error : 'Internal server error' } , { status : 500 } )
185117 }
@@ -196,163 +128,24 @@ export const DELETE = withRouteHandler(
196128 const { id } = await params
197129
198130 try {
199- const access = await getCredentialActorContext ( id , session . user . id )
200- if ( ! access . credential ) {
201- return NextResponse . json ( { error : 'Credential not found' } , { status : 404 } )
202- }
203- if ( ! access . hasWorkspaceAccess || ! access . isAdmin ) {
204- return NextResponse . json ( { error : 'Credential admin permission required' } , { status : 403 } )
205- }
206-
207- if ( access . credential . type === 'env_personal' && access . credential . envKey ) {
208- const ownerUserId = access . credential . envOwnerUserId
209- if ( ! ownerUserId ) {
210- return NextResponse . json ( { error : 'Invalid personal secret owner' } , { status : 400 } )
211- }
212-
213- const [ personalRow ] = await db
214- . select ( { variables : environment . variables } )
215- . from ( environment )
216- . where ( eq ( environment . userId , ownerUserId ) )
217- . limit ( 1 )
218-
219- const current = ( ( personalRow ?. variables as Record < string , string > | null ) ?? { } ) as Record <
220- string ,
221- string
222- >
223- if ( access . credential . envKey in current ) {
224- delete current [ access . credential . envKey ]
225- }
226-
227- await db
228- . insert ( environment )
229- . values ( {
230- id : ownerUserId ,
231- userId : ownerUserId ,
232- variables : current ,
233- updatedAt : new Date ( ) ,
234- } )
235- . onConflictDoUpdate ( {
236- target : [ environment . userId ] ,
237- set : { variables : current , updatedAt : new Date ( ) } ,
238- } )
239-
240- await syncPersonalEnvCredentialsForUser ( {
241- userId : ownerUserId ,
242- envKeys : Object . keys ( current ) ,
243- } )
244-
245- captureServerEvent (
246- session . user . id ,
247- 'credential_deleted' ,
248- {
249- credential_type : 'env_personal' ,
250- provider_id : access . credential . envKey ,
251- workspace_id : access . credential . workspaceId ,
252- } ,
253- { groups : { workspace : access . credential . workspaceId } }
254- )
255-
256- recordAudit ( {
257- workspaceId : access . credential . workspaceId ,
258- actorId : session . user . id ,
259- actorName : session . user . name ,
260- actorEmail : session . user . email ,
261- action : AuditAction . CREDENTIAL_DELETED ,
262- resourceType : AuditResourceType . CREDENTIAL ,
263- resourceId : id ,
264- resourceName : access . credential . displayName ,
265- description : `Deleted personal env credential "${ access . credential . envKey } "` ,
266- metadata : { credentialType : 'env_personal' , envKey : access . credential . envKey } ,
267- request,
268- } )
269-
270- return NextResponse . json ( { success : true } , { status : 200 } )
271- }
272-
273- if ( access . credential . type === 'env_workspace' && access . credential . envKey ) {
274- const [ workspaceRow ] = await db
275- . select ( {
276- id : workspaceEnvironment . id ,
277- createdAt : workspaceEnvironment . createdAt ,
278- variables : workspaceEnvironment . variables ,
279- } )
280- . from ( workspaceEnvironment )
281- . where ( eq ( workspaceEnvironment . workspaceId , access . credential . workspaceId ) )
282- . limit ( 1 )
283-
284- const current = ( ( workspaceRow ?. variables as Record < string , string > | null ) ??
285- { } ) as Record < string , string >
286- if ( access . credential . envKey in current ) {
287- delete current [ access . credential . envKey ]
288- }
289-
290- await db
291- . insert ( workspaceEnvironment )
292- . values ( {
293- id : workspaceRow ?. id || generateId ( ) ,
294- workspaceId : access . credential . workspaceId ,
295- variables : current ,
296- createdAt : workspaceRow ?. createdAt || new Date ( ) ,
297- updatedAt : new Date ( ) ,
298- } )
299- . onConflictDoUpdate ( {
300- target : [ workspaceEnvironment . workspaceId ] ,
301- set : { variables : current , updatedAt : new Date ( ) } ,
302- } )
303-
304- await deleteWorkspaceEnvCredentials ( {
305- workspaceId : access . credential . workspaceId ,
306- removedKeys : [ access . credential . envKey ] ,
307- } )
308-
309- captureServerEvent (
310- session . user . id ,
311- 'credential_deleted' ,
312- {
313- credential_type : 'env_workspace' ,
314- provider_id : access . credential . envKey ,
315- workspace_id : access . credential . workspaceId ,
316- } ,
317- { groups : { workspace : access . credential . workspaceId } }
318- )
319-
320- recordAudit ( {
321- workspaceId : access . credential . workspaceId ,
322- actorId : session . user . id ,
323- actorName : session . user . name ,
324- actorEmail : session . user . email ,
325- action : AuditAction . CREDENTIAL_DELETED ,
326- resourceType : AuditResourceType . CREDENTIAL ,
327- resourceId : id ,
328- resourceName : access . credential . displayName ,
329- description : `Deleted workspace env credential "${ access . credential . envKey } "` ,
330- metadata : { credentialType : 'env_workspace' , envKey : access . credential . envKey } ,
331- request,
332- } )
333-
334- return NextResponse . json ( { success : true } , { status : 200 } )
335- }
336-
337- await deleteCredential ( {
131+ const result = await performDeleteCredential ( {
338132 credentialId : id ,
339- actorId : session . user . id ,
133+ userId : session . user . id ,
340134 actorName : session . user . name ,
341135 actorEmail : session . user . email ,
342- reason : 'user_delete' ,
343136 request,
344137 } )
345-
346- captureServerEvent (
347- session . user . id ,
348- 'credential_deleted' ,
349- {
350- credential_type : access . credential . type as 'oauth' | 'service_account' ,
351- provider_id : access . credential . providerId ?? id ,
352- workspace_id : access . credential . workspaceId ,
353- } ,
354- { groups : { workspace : access . credential . workspaceId } }
355- )
138+ if ( ! result . success ) {
139+ const status =
140+ result . errorCode === 'not_found'
141+ ? 404
142+ : result . errorCode === 'forbidden'
143+ ? 403
144+ : result . errorCode === 'validation'
145+ ? 400
146+ : 500
147+ return NextResponse . json ( { error : result . error } , { status } )
148+ }
356149
357150 return NextResponse . json ( { success : true } , { status : 200 } )
358151 } catch ( error ) {
0 commit comments