diff --git a/backend/src/lib/fn/object.ts b/backend/src/lib/fn/object.ts index 9f44e9da8..6ff7278aa 100644 --- a/backend/src/lib/fn/object.ts +++ b/backend/src/lib/fn/object.ts @@ -84,3 +84,22 @@ export const deepEqual = (obj1: unknown, obj2: unknown): boolean => { deepEqual((obj1 as Record)[key], (obj2 as Record)[key]) ); }; + +export const deepEqualSkipFields = (obj1: unknown, obj2: unknown, skipFields: string[] = []): boolean => { + if (skipFields.length === 0) { + return deepEqual(obj1, obj2); + } + + if (typeof obj1 !== "object" || typeof obj2 !== "object" || obj1 === null || obj2 === null) { + return deepEqual(obj1, obj2); + } + + const filtered1 = Object.fromEntries( + Object.entries(obj1 as Record).filter(([key]) => !skipFields.includes(key)) + ); + const filtered2 = Object.fromEntries( + Object.entries(obj2 as Record).filter(([key]) => !skipFields.includes(key)) + ); + + return deepEqual(filtered1, filtered2); +}; diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts index a86c07cf9..d649b3ba5 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts @@ -449,7 +449,7 @@ export const registerSyncSecretsEndpoints = , destination, excludeSyncId, projectId diff --git a/backend/src/services/secret-sync/secret-sync-dal.ts b/backend/src/services/secret-sync/secret-sync-dal.ts index 57c6581ce..64d2c0bdb 100644 --- a/backend/src/services/secret-sync/secret-sync-dal.ts +++ b/backend/src/services/secret-sync/secret-sync-dal.ts @@ -204,5 +204,19 @@ export const secretSyncDALFactory = ( } }; - return { ...secretSyncOrm, findById, findOne, find, create, updateById }; + const findByDestinationAndOrgId = async (destination: string, orgId: string, tx?: Knex) => { + try { + const response = await (tx || db.replicaNode())(TableName.SecretSync) + .join(TableName.Project, `${TableName.SecretSync}.projectId`, `${TableName.Project}.id`) + .where(`${TableName.SecretSync}.destination`, destination) + .where(`${TableName.Project}.orgId`, orgId) + .select(selectAllTableCols(TableName.SecretSync)); + + return response; + } catch (error) { + throw new DatabaseError({ error, name: "Find By Destination And Org ID - Secret Sync" }); + } + }; + + return { ...secretSyncOrm, findById, findOne, find, create, updateById, findByDestinationAndOrgId }; }; diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index 739b7c62c..83751f1b4 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -12,7 +12,7 @@ import { import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { DatabaseErrorCode } from "@app/lib/error-codes"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; -import { deepEqual } from "@app/lib/fn/object"; +import { deepEqualSkipFields } from "@app/lib/fn/object"; import { OrgServiceActor } from "@app/lib/types"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; @@ -698,10 +698,46 @@ export const secretSyncServiceFactory = ({ return updatedSecretSync as TSecretSync; }; + const getSkipFieldsForDestination = (destination: SecretSync): string[] => { + switch (destination) { + case SecretSync.AWSSecretsManager: + return ["mappingBehavior", "secretName"]; + case SecretSync.OnePass: + return ["valueLabel"]; + case SecretSync.AzureAppConfiguration: + return ["label"]; + case SecretSync.AzureDevOps: + return ["devopsProjectName"]; + case SecretSync.Checkly: + return ["groupName", "accountName"]; + case SecretSync.DigitalOceanAppPlatform: + return ["appName"]; + case SecretSync.GitLab: + return ["projectName", "shouldProtectSecrets", "shouldMaskSecrets", "shouldHideSecrets"]; + case SecretSync.Heroku: + return ["appName"]; + case SecretSync.Netlify: + return ["accountName", "siteName"]; + case SecretSync.Railway: + return ["projectName", "environmentName", "serviceName"]; + case SecretSync.Supabase: + return ["projectName"]; + case SecretSync.TerraformCloud: + return ["variableSetName", "workspaceName"]; + case SecretSync.Vercel: + return ["appName"]; + case SecretSync.Zabbix: + return ["hostName", "macroType"]; + default: + return []; + } + }; + const checkDuplicateDestination = async ( { destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO, actor: OrgServiceActor ) => { + const skipFields = getSkipFieldsForDestination(destination); const { permission } = await permissionService.getProjectPermission({ actor: actor.type, actorId: actor.id, @@ -716,14 +752,12 @@ export const secretSyncServiceFactory = ({ ProjectPermissionSub.SecretSyncs ); - if (!destinationConfig || typeof destinationConfig !== "object") { + if (!destinationConfig || Object.keys(destinationConfig).length === 0) { return { hasDuplicate: false }; } try { - const existingSyncs = await secretSyncDAL.find({ - destination - }); + const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId); const duplicates = existingSyncs.filter((sync) => { if (sync.id === excludeSyncId) { @@ -731,7 +765,7 @@ export const secretSyncServiceFactory = ({ } try { - return deepEqual(sync.destinationConfig, destinationConfig); + return deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields); } catch { return false; } diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 92384ff46..e742f4763 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -326,7 +326,7 @@ export type TDeleteSecretSyncDTO = { export type TCheckDuplicateDestinationDTO = { destination: SecretSync; - destinationConfig: unknown; + destinationConfig: Record; excludeSyncId?: string; projectId: string; }; diff --git a/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx b/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx index 97c8d7acb..2b313b3ff 100644 --- a/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx +++ b/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect, useState } from "react"; +import { ReactNode, useCallback, useEffect, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -53,31 +53,34 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) => { enabled: checkDuplicateEnabled && Boolean(destinationConfigToCheck) } ); - const performUpdate = async (formData: TSecretSyncForm) => { - try { - const { environment, connection, ...updateData } = formData; - const updatedSecretSync = await updateSecretSync.mutateAsync({ - syncId: secretSync.id, - ...updateData, - environment: environment?.slug, - connectionId: connection.id, - projectId: secretSync.projectId - }); + const performUpdate = useCallback( + async (formData: TSecretSyncForm) => { + try { + const { environment, connection, ...updateData } = formData; + const updatedSecretSync = await updateSecretSync.mutateAsync({ + syncId: secretSync.id, + ...updateData, + environment: environment?.slug, + connectionId: connection.id, + projectId: secretSync.projectId + }); - createNotification({ - text: `Successfully updated ${destinationName} Sync`, - type: "success" - }); - onComplete(updatedSecretSync); - } catch (err: any) { - console.error(err); - createNotification({ - title: `Failed to update ${destinationName} Sync`, - text: err.message, - type: "error" - }); - } - }; + createNotification({ + text: `Successfully updated ${destinationName} Sync`, + type: "success" + }); + onComplete(updatedSecretSync); + } catch (err: any) { + console.error(err); + createNotification({ + title: `Failed to update ${destinationName} Sync`, + text: err.message, + type: "error" + }); + } + }, + [updateSecretSync, secretSync.id, secretSync.projectId, destinationName, onComplete] + ); useEffect(() => { if (checkDuplicateEnabled && !isCheckingDuplicate && destinationConfigToCheck) {