mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-12 23:45:07 -05:00
copilot + oauth name comflict
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { createElement, useEffect, useMemo, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Plus, Search, Share2, Trash2 } from 'lucide-react'
|
||||
import { AlertTriangle, Plus, Search, Share2, Trash2 } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
Badge,
|
||||
@@ -297,7 +297,7 @@ export function CredentialsManager() {
|
||||
[createSecretScope]
|
||||
)
|
||||
const selectedExistingEnvCredential = useMemo(() => {
|
||||
if (createType !== 'secret') return null
|
||||
if (createType !== 'secret' || createSecretInputMode !== 'single') return null
|
||||
const envKey = normalizeEnvKeyInput(createEnvKey)
|
||||
if (!envKey) return null
|
||||
return (
|
||||
@@ -306,7 +306,31 @@ export function CredentialsManager() {
|
||||
row.type === createSecretType && (row.envKey || '').toLowerCase() === envKey.toLowerCase()
|
||||
) ?? null
|
||||
)
|
||||
}, [credentials, createEnvKey, createSecretType, createType])
|
||||
}, [credentials, createEnvKey, createSecretType, createType, createSecretInputMode])
|
||||
|
||||
const crossScopeEnvConflict = useMemo(() => {
|
||||
if (createType !== 'secret' || createSecretInputMode !== 'single') return null
|
||||
if (createSecretScope !== 'personal') return null
|
||||
const envKey = normalizeEnvKeyInput(createEnvKey)
|
||||
if (!envKey) return null
|
||||
return (
|
||||
credentials.find(
|
||||
(row) =>
|
||||
row.type === 'env_workspace' && (row.envKey || '').toLowerCase() === envKey.toLowerCase()
|
||||
) ?? null
|
||||
)
|
||||
}, [credentials, createEnvKey, createSecretScope, createType, createSecretInputMode])
|
||||
|
||||
const existingOAuthDisplayName = useMemo(() => {
|
||||
if (createType !== 'oauth') return null
|
||||
const name = createDisplayName.trim()
|
||||
if (!name) return null
|
||||
return (
|
||||
credentials.find(
|
||||
(row) => row.type === 'oauth' && row.displayName.toLowerCase() === name.toLowerCase()
|
||||
) ?? null
|
||||
)
|
||||
}, [credentials, createDisplayName, createType])
|
||||
const selectedEnvCurrentValue = useMemo(() => {
|
||||
if (!selectedCredential || selectedCredential.type === 'oauth') return ''
|
||||
const envKey = selectedCredential.envKey || ''
|
||||
@@ -1309,6 +1333,20 @@ export function CredentialsManager() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{existingOAuthDisplayName && (
|
||||
<div className='rounded-[8px] border border-red-500/50 bg-red-50 p-[12px] dark:bg-red-950/30'>
|
||||
<div className='flex items-start gap-[10px]'>
|
||||
<AlertTriangle className='mt-[1px] h-4 w-4 flex-shrink-0 text-red-600 dark:text-red-400' />
|
||||
<p className='text-[12px] text-red-700 dark:text-red-300'>
|
||||
A credential named{' '}
|
||||
<span className='font-medium'>
|
||||
{existingOAuthDisplayName.displayName}
|
||||
</span>{' '}
|
||||
already exists.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-col gap-[10px]'>
|
||||
@@ -1411,28 +1449,29 @@ export function CredentialsManager() {
|
||||
</div>
|
||||
|
||||
{selectedExistingEnvCredential && (
|
||||
<div className='rounded-[8px] border border-[var(--brand-9)]/40 bg-[var(--surface-3)] px-[10px] py-[8px]'>
|
||||
<p className='text-[12px] text-[var(--text-primary)]'>
|
||||
This secret key already maps to credential{' '}
|
||||
<span className='font-medium'>
|
||||
{selectedExistingEnvCredential.displayName}
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
<p className='mt-[4px] text-[11px] text-[var(--text-tertiary)]'>
|
||||
Create will update the secret value and reuse the existing credential.
|
||||
</p>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='mt-[6px]'
|
||||
onClick={() => {
|
||||
setSelectedCredentialId(selectedExistingEnvCredential.id)
|
||||
setShowCreateModal(false)
|
||||
resetCreateForm()
|
||||
}}
|
||||
>
|
||||
Open existing credential
|
||||
</Button>
|
||||
<div className='rounded-[8px] border border-red-500/50 bg-red-50 p-[12px] dark:bg-red-950/30'>
|
||||
<div className='flex items-start gap-[10px]'>
|
||||
<AlertTriangle className='mt-[1px] h-4 w-4 flex-shrink-0 text-red-600 dark:text-red-400' />
|
||||
<p className='text-[12px] text-red-700 dark:text-red-300'>
|
||||
A secret with key{' '}
|
||||
<span className='font-medium'>
|
||||
{selectedExistingEnvCredential.displayName}
|
||||
</span>{' '}
|
||||
already exists.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!selectedExistingEnvCredential && crossScopeEnvConflict && (
|
||||
<div className='rounded-[8px] border border-amber-500/50 bg-amber-50 p-[12px] dark:bg-amber-950/30'>
|
||||
<div className='flex items-start gap-[10px]'>
|
||||
<AlertTriangle className='mt-[1px] h-4 w-4 flex-shrink-0 text-amber-600 dark:text-amber-400' />
|
||||
<p className='text-[12px] text-amber-700 dark:text-amber-300'>
|
||||
A workspace secret with key{' '}
|
||||
<span className='font-medium'>{crossScopeEnvConflict.envKey}</span>{' '}
|
||||
already exists. Workspace secrets take precedence at runtime.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -1471,8 +1510,13 @@ export function CredentialsManager() {
|
||||
)}
|
||||
|
||||
{createError && (
|
||||
<div className='whitespace-pre-wrap rounded-[8px] border border-[var(--status-red)]/40 bg-[var(--status-red)]/10 px-[10px] py-[8px] text-[12px] text-[var(--status-red)]'>
|
||||
{createError}
|
||||
<div className='rounded-[8px] border border-red-500/50 bg-red-50 p-[12px] dark:bg-red-950/30'>
|
||||
<div className='flex items-start gap-[10px]'>
|
||||
<AlertTriangle className='mt-[1px] h-4 w-4 flex-shrink-0 text-red-600 dark:text-red-400' />
|
||||
<p className='whitespace-pre-wrap text-[12px] text-red-700 dark:text-red-300'>
|
||||
{createError}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1488,10 +1532,13 @@ export function CredentialsManager() {
|
||||
(createType === 'oauth'
|
||||
? !createOAuthProviderId ||
|
||||
!createDisplayName.trim() ||
|
||||
connectOAuthService.isPending
|
||||
connectOAuthService.isPending ||
|
||||
Boolean(existingOAuthDisplayName)
|
||||
: createSecretInputMode === 'bulk'
|
||||
? !createBulkText.trim()
|
||||
: !createEnvKey.trim() || !createEnvValue.trim()) ||
|
||||
: !createEnvKey.trim() ||
|
||||
!createEnvValue.trim() ||
|
||||
Boolean(selectedExistingEnvCredential)) ||
|
||||
createCredential.isPending ||
|
||||
savePersonalEnvironment.isPending ||
|
||||
upsertWorkspaceEnvironment.isPending ||
|
||||
@@ -1508,9 +1555,7 @@ export function CredentialsManager() {
|
||||
upsertWorkspaceEnvironment.isPending
|
||||
? 'Importing...'
|
||||
: 'Import all'
|
||||
: selectedExistingEnvCredential
|
||||
? 'Update and use existing'
|
||||
: 'Create'}
|
||||
: 'Create'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { db } from '@sim/db'
|
||||
import { environment } from '@sim/db/schema'
|
||||
import { credential, environment, workflow, workspaceEnvironment } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { and, eq, inArray } from 'drizzle-orm'
|
||||
import { z } from 'zod'
|
||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
||||
import { syncPersonalEnvCredentialsForUser } from '@/lib/credentials/environment'
|
||||
import {
|
||||
syncPersonalEnvCredentialsForUser,
|
||||
syncWorkspaceEnvCredentials,
|
||||
} from '@/lib/credentials/environment'
|
||||
|
||||
interface SetEnvironmentVariablesParams {
|
||||
variables: Record<string, any> | Array<{ name: string; value: string }>
|
||||
@@ -55,79 +58,179 @@ export const setEnvironmentVariablesServerTool: BaseServerTool<SetEnvironmentVar
|
||||
const normalized = normalizeVariables(variables || {})
|
||||
const { variables: validatedVariables } = EnvVarSchema.parse({ variables: normalized })
|
||||
|
||||
// Fetch existing personal environment variables
|
||||
const existingData = await db
|
||||
.select()
|
||||
.from(environment)
|
||||
.where(eq(environment.userId, authenticatedUserId))
|
||||
.limit(1)
|
||||
const existingEncrypted = (existingData[0]?.variables as Record<string, string>) || {}
|
||||
const requestedKeys = Object.keys(validatedVariables)
|
||||
const workflowId = params.workflowId
|
||||
|
||||
const toEncrypt: Record<string, string> = {}
|
||||
const added: string[] = []
|
||||
const updated: string[] = []
|
||||
for (const [key, newVal] of Object.entries(validatedVariables)) {
|
||||
if (!(key in existingEncrypted)) {
|
||||
toEncrypt[key] = newVal
|
||||
added.push(key)
|
||||
} else {
|
||||
try {
|
||||
const { decrypted } = await decryptSecret(existingEncrypted[key])
|
||||
if (decrypted !== newVal) {
|
||||
toEncrypt[key] = newVal
|
||||
updated.push(key)
|
||||
}
|
||||
} catch {
|
||||
toEncrypt[key] = newVal
|
||||
updated.push(key)
|
||||
const workspaceKeySet = new Set<string>()
|
||||
let resolvedWorkspaceId: string | null = null
|
||||
|
||||
if (requestedKeys.length > 0 && workflowId) {
|
||||
const [wf] = await db
|
||||
.select({ workspaceId: workflow.workspaceId })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (wf?.workspaceId) {
|
||||
resolvedWorkspaceId = wf.workspaceId
|
||||
const existingWorkspaceCredentials = await db
|
||||
.select({ envKey: credential.envKey })
|
||||
.from(credential)
|
||||
.where(
|
||||
and(
|
||||
eq(credential.workspaceId, wf.workspaceId),
|
||||
eq(credential.type, 'env_workspace'),
|
||||
inArray(credential.envKey, requestedKeys)
|
||||
)
|
||||
)
|
||||
|
||||
for (const row of existingWorkspaceCredentials) {
|
||||
if (row.envKey) workspaceKeySet.add(row.envKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newlyEncrypted = await Object.entries(toEncrypt).reduce(
|
||||
async (accP, [key, val]) => {
|
||||
const acc = await accP
|
||||
const { encrypted } = await encryptSecret(val)
|
||||
return { ...acc, [key]: encrypted }
|
||||
},
|
||||
Promise.resolve({} as Record<string, string>)
|
||||
)
|
||||
const personalVars: Record<string, string> = {}
|
||||
const workspaceVars: Record<string, string> = {}
|
||||
|
||||
const finalEncrypted = { ...existingEncrypted, ...newlyEncrypted }
|
||||
for (const [key, value] of Object.entries(validatedVariables)) {
|
||||
if (workspaceKeySet.has(key)) {
|
||||
workspaceVars[key] = value
|
||||
} else {
|
||||
personalVars[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Save to personal environment variables (keyed by userId)
|
||||
await db
|
||||
.insert(environment)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
const added: string[] = []
|
||||
const updated: string[] = []
|
||||
const workspaceUpdated: string[] = []
|
||||
|
||||
if (Object.keys(personalVars).length > 0) {
|
||||
const existingData = await db
|
||||
.select()
|
||||
.from(environment)
|
||||
.where(eq(environment.userId, authenticatedUserId))
|
||||
.limit(1)
|
||||
const existingEncrypted = (existingData[0]?.variables as Record<string, string>) || {}
|
||||
|
||||
const toEncrypt: Record<string, string> = {}
|
||||
for (const [key, newVal] of Object.entries(personalVars)) {
|
||||
if (!(key in existingEncrypted)) {
|
||||
toEncrypt[key] = newVal
|
||||
added.push(key)
|
||||
} else {
|
||||
try {
|
||||
const { decrypted } = await decryptSecret(existingEncrypted[key])
|
||||
if (decrypted !== newVal) {
|
||||
toEncrypt[key] = newVal
|
||||
updated.push(key)
|
||||
}
|
||||
} catch {
|
||||
toEncrypt[key] = newVal
|
||||
updated.push(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newlyEncrypted = await Object.entries(toEncrypt).reduce(
|
||||
async (accP, [key, val]) => {
|
||||
const acc = await accP
|
||||
const { encrypted } = await encryptSecret(val)
|
||||
return { ...acc, [key]: encrypted }
|
||||
},
|
||||
Promise.resolve({} as Record<string, string>)
|
||||
)
|
||||
|
||||
const finalEncrypted = { ...existingEncrypted, ...newlyEncrypted }
|
||||
|
||||
await db
|
||||
.insert(environment)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
userId: authenticatedUserId,
|
||||
variables: finalEncrypted,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [environment.userId],
|
||||
set: { variables: finalEncrypted, updatedAt: new Date() },
|
||||
})
|
||||
|
||||
await syncPersonalEnvCredentialsForUser({
|
||||
userId: authenticatedUserId,
|
||||
variables: finalEncrypted,
|
||||
updatedAt: new Date(),
|
||||
envKeys: Object.keys(finalEncrypted),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [environment.userId],
|
||||
set: { variables: finalEncrypted, updatedAt: new Date() },
|
||||
}
|
||||
|
||||
if (Object.keys(workspaceVars).length > 0 && resolvedWorkspaceId) {
|
||||
const wsRows = await db
|
||||
.select()
|
||||
.from(workspaceEnvironment)
|
||||
.where(eq(workspaceEnvironment.workspaceId, resolvedWorkspaceId))
|
||||
.limit(1)
|
||||
|
||||
const existingWsEncrypted = (wsRows[0]?.variables as Record<string, string>) || {}
|
||||
|
||||
const toEncryptWs: Record<string, string> = {}
|
||||
for (const [key, newVal] of Object.entries(workspaceVars)) {
|
||||
toEncryptWs[key] = newVal
|
||||
workspaceUpdated.push(key)
|
||||
}
|
||||
|
||||
const newlyEncryptedWs = await Object.entries(toEncryptWs).reduce(
|
||||
async (accP, [key, val]) => {
|
||||
const acc = await accP
|
||||
const { encrypted } = await encryptSecret(val)
|
||||
return { ...acc, [key]: encrypted }
|
||||
},
|
||||
Promise.resolve({} as Record<string, string>)
|
||||
)
|
||||
|
||||
const mergedWs = { ...existingWsEncrypted, ...newlyEncryptedWs }
|
||||
|
||||
await db
|
||||
.insert(workspaceEnvironment)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
workspaceId: resolvedWorkspaceId,
|
||||
variables: mergedWs,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [workspaceEnvironment.workspaceId],
|
||||
set: { variables: mergedWs, updatedAt: new Date() },
|
||||
})
|
||||
|
||||
await syncWorkspaceEnvCredentials({
|
||||
workspaceId: resolvedWorkspaceId,
|
||||
envKeys: Object.keys(workspaceVars),
|
||||
actingUserId: authenticatedUserId,
|
||||
})
|
||||
}
|
||||
|
||||
await syncPersonalEnvCredentialsForUser({
|
||||
userId: authenticatedUserId,
|
||||
envKeys: Object.keys(finalEncrypted),
|
||||
})
|
||||
const totalProcessed = added.length + updated.length + workspaceUpdated.length
|
||||
|
||||
logger.info('Saved personal environment variables', {
|
||||
logger.info('Saved environment variables', {
|
||||
userId: authenticatedUserId,
|
||||
addedCount: added.length,
|
||||
updatedCount: updated.length,
|
||||
totalCount: Object.keys(finalEncrypted).length,
|
||||
workspaceUpdatedCount: workspaceUpdated.length,
|
||||
})
|
||||
|
||||
const parts: string[] = []
|
||||
if (added.length > 0) parts.push(`${added.length} personal secret(s) added`)
|
||||
if (updated.length > 0) parts.push(`${updated.length} personal secret(s) updated`)
|
||||
if (workspaceUpdated.length > 0)
|
||||
parts.push(`${workspaceUpdated.length} workspace secret(s) updated`)
|
||||
|
||||
return {
|
||||
message: `Successfully processed ${Object.keys(validatedVariables).length} personal environment variable(s): ${added.length} added, ${updated.length} updated`,
|
||||
message: `Successfully processed ${totalProcessed} secret(s): ${parts.join(', ')}`,
|
||||
variableCount: Object.keys(validatedVariables).length,
|
||||
variableNames: Object.keys(validatedVariables),
|
||||
totalVariableCount: Object.keys(finalEncrypted).length,
|
||||
addedVariables: added,
|
||||
updatedVariables: updated,
|
||||
workspaceUpdatedVariables: workspaceUpdated,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user