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 d649b3ba5..9da2f4b69 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 @@ -440,7 +440,10 @@ export const registerSyncSecretsEndpoints = , + newConfig: Record + ): boolean => { + switch (destination) { + case SecretSync.GitLab: { + const existingTargetEnv = existingConfig.targetEnvironment as string | undefined; + const newTargetEnv = newConfig.targetEnvironment as string | undefined; + + // If either has wildcard '*', it conflicts with any targetEnvironment + if (existingTargetEnv === "*" || newTargetEnv === "*") { + return true; + } + + return ( + existingTargetEnv === newTargetEnv && + ((newConfig.scope as string) === "group" + ? existingConfig.groupId === newConfig.groupId + : existingConfig.projectId === newConfig.projectId) + ); + } + default: + // For other sync types, no special handling needed + return true; + } + }; + const checkDuplicateDestination = async ( { destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO, actor: OrgServiceActor @@ -753,7 +790,7 @@ export const secretSyncServiceFactory = ({ ); if (!destinationConfig || Object.keys(destinationConfig).length === 0) { - return { hasDuplicate: false }; + return { hasDuplicate: false, duplicateProjectId: undefined }; } try { @@ -765,15 +802,27 @@ export const secretSyncServiceFactory = ({ } try { - return deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields); + const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields); + if (baseFieldsMatch) { + return handleSpecialCaseDuplicateCheck( + destination, + sync.destinationConfig as Record, + destinationConfig + ); + } + return false; } catch { return false; } }); - return { hasDuplicate: duplicates.length > 0 }; + const hasDuplicate = duplicates.length > 0; + return { + hasDuplicate, + duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined + }; } catch (error) { - return { hasDuplicate: false }; + return { hasDuplicate: false, duplicateProjectId: undefined }; } }; diff --git a/frontend/src/components/secret-syncs/forms/DuplicateDestinationConfirmationModal.tsx b/frontend/src/components/secret-syncs/forms/DuplicateDestinationConfirmationModal.tsx index 45ec2c1be..82ab06038 100644 --- a/frontend/src/components/secret-syncs/forms/DuplicateDestinationConfirmationModal.tsx +++ b/frontend/src/components/secret-syncs/forms/DuplicateDestinationConfirmationModal.tsx @@ -5,13 +5,15 @@ type Props = { onOpenChange: (isOpen: boolean) => void; onConfirm: () => void; isLoading?: boolean; + duplicateProjectId?: string; }; export const DuplicateDestinationConfirmationModal = ({ isOpen, onOpenChange, onConfirm, - isLoading + isLoading, + duplicateProjectId }: Props) => { return ( @@ -21,6 +23,14 @@ export const DuplicateDestinationConfirmationModal = ({ Another secret sync in your organization is already configured with the same destination. Proceeding may cause conflicts or overwrite existing data.

+ {duplicateProjectId && ( +

+ Duplicate found in project ID:{" "} + + {duplicateProjectId} + +

+ )}

Are you sure you want to continue?

diff --git a/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx b/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx index 2b313b3ff..3c0e4ea17 100644 --- a/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx +++ b/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx @@ -44,8 +44,9 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) => const [destinationConfigToCheck, setDestinationConfigToCheck] = useState(null); const [checkDuplicateEnabled, setCheckDuplicateEnabled] = useState(false); + const [storedDuplicateProjectId, setStoredDuplicateProjectId] = useState(); - const { data: hasDuplicate, isLoading: isCheckingDuplicate } = useCheckDuplicateDestination( + const { data: duplicateData, isLoading: isCheckingDuplicate } = useCheckDuplicateDestination( secretSync.destination, destinationConfigToCheck, secretSync.projectId, @@ -84,7 +85,8 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) => useEffect(() => { if (checkDuplicateEnabled && !isCheckingDuplicate && destinationConfigToCheck) { - if (hasDuplicate) { + if (duplicateData?.hasDuplicate) { + setStoredDuplicateProjectId(duplicateData.duplicateProjectId); setShowDuplicateConfirmation(true); } else if (pendingFormData) { performUpdate(pendingFormData); @@ -96,7 +98,8 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) => }, [ checkDuplicateEnabled, isCheckingDuplicate, - hasDuplicate, + duplicateData?.hasDuplicate, + duplicateData?.duplicateProjectId, destinationConfigToCheck, pendingFormData, performUpdate @@ -197,9 +200,15 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) => { + setShowDuplicateConfirmation(open); + if (!open) { + setStoredDuplicateProjectId(undefined); + } + }} onConfirm={handleConfirmDuplicate} isLoading={updateSecretSync.isPending} + duplicateProjectId={storedDuplicateProjectId} /> ); diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 76a885172..84801b000 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -67,7 +67,7 @@ export const SecretSyncReviewFields = () => { const destinationName = SECRET_SYNC_MAP[destination].name; - const { hasDuplicate, isChecking } = useDuplicateDestinationCheck({ + const { hasDuplicate, duplicateProjectId, isChecking } = useDuplicateDestinationCheck({ destination, projectId: currentProject?.id || "", enabled: true, @@ -192,10 +192,17 @@ export const SecretSyncReviewFields = () => {
-

- Another secret sync in your organization is already configured with the same - destination. This may lead to conflicts or unexpected behavior. -

+
+

+ Another secret sync in your organization is already configured with the same + destination. This may lead to conflicts or unexpected behavior. +

+ {duplicateProjectId && ( +

+ Duplicate found in project ID: {duplicateProjectId} +

+ )} +
)} diff --git a/frontend/src/hooks/api/secretSyncs/queries.tsx b/frontend/src/hooks/api/secretSyncs/queries.tsx index 627ff15f3..250d02ff8 100644 --- a/frontend/src/hooks/api/secretSyncs/queries.tsx +++ b/frontend/src/hooks/api/secretSyncs/queries.tsx @@ -103,23 +103,28 @@ export const useCheckDuplicateDestination = ( projectId: string, excludeSyncId?: string, options?: Omit< - UseQueryOptions>, + UseQueryOptions< + { hasDuplicate: boolean; duplicateProjectId?: string }, + unknown, + { hasDuplicate: boolean; duplicateProjectId?: string }, + ReturnType + >, "queryKey" | "queryFn" > ) => { return useQuery({ queryKey: secretSyncKeys.duplicateCheck(destination, destinationConfig, excludeSyncId), queryFn: async () => { - const { data } = await apiRequest.post<{ hasDuplicate: boolean }>( - `/api/v1/secret-syncs/${destination}/check-destination`, - { - destinationConfig, - excludeSyncId, - projectId - } - ); + const { data } = await apiRequest.post<{ + hasDuplicate: boolean; + duplicateProjectId?: string; + }>(`/api/v1/secret-syncs/${destination}/check-destination`, { + destinationConfig, + excludeSyncId, + projectId + }); - return data.hasDuplicate; + return data; }, enabled: Boolean(destinationConfig) && Object.keys(destinationConfig || {}).length > 0, ...options diff --git a/frontend/src/hooks/api/secretSyncs/useDuplicateDestinationCheck.ts b/frontend/src/hooks/api/secretSyncs/useDuplicateDestinationCheck.ts index c570e15a6..97985fba9 100644 --- a/frontend/src/hooks/api/secretSyncs/useDuplicateDestinationCheck.ts +++ b/frontend/src/hooks/api/secretSyncs/useDuplicateDestinationCheck.ts @@ -30,7 +30,7 @@ export const useDuplicateDestinationCheck = ({ const shouldCheck = enabled && hasValidConfig; const { - data: hasDuplicate, + data: duplicateData, isLoading, error, refetch @@ -41,7 +41,8 @@ export const useDuplicateDestinationCheck = ({ }); return { - hasDuplicate: shouldCheck ? Boolean(hasDuplicate) : false, + hasDuplicate: shouldCheck ? Boolean(duplicateData?.hasDuplicate) : false, + duplicateProjectId: duplicateData?.duplicateProjectId, isChecking: shouldCheck && isLoading, hasError: Boolean(error), hasValidConfig,