diff --git a/backend/src/ee/routes/v1/pit-router.ts b/backend/src/ee/routes/v1/pit-router.ts index 27d17fc25..6688fb266 100644 --- a/backend/src/ee/routes/v1/pit-router.ts +++ b/backend/src/ee/routes/v1/pit-router.ts @@ -432,11 +432,17 @@ export const registerPITRouter = async (server: FastifyZodProvider) => { } ], body: z.object({ - projectSlug: z.string().trim().optional(), - workspaceId: z.string().trim().optional(), + projectId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - message: z.string().trim(), + message: z + .string() + .trim() + .min(1) + .max(255) + .refine((message) => message.trim() !== "", { + message: "Commit message cannot be empty" + }), changes: z.object({ secrets: z.object({ create: z @@ -500,7 +506,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => { .refine((name) => isValidFolderName(name), { message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." }), - description: z.string().optional(), + description: z.string().nullable().optional(), id: z.string() }) ) @@ -534,7 +540,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, - projectId: req.body.workspaceId, + projectId: req.body.projectId, environment: req.body.environment, secretPath: req.body.secretPath, message: req.body.message, diff --git a/backend/src/ee/services/pit/pit-service.ts b/backend/src/ee/services/pit/pit-service.ts index c31f9184e..e31ebc638 100644 --- a/backend/src/ee/services/pit/pit-service.ts +++ b/backend/src/ee/services/pit/pit-service.ts @@ -8,7 +8,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { TFolderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal"; import { ResourceType, - TCreateCommitChangeDTO, + TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service"; import { @@ -500,8 +500,7 @@ export const pitServiceFactory = ({ const processNewCommitRaw = async ({ actorId, - projectSlug, - projectId: optionalProjectId, + projectId, environment, actor, actorOrgId, @@ -522,8 +521,7 @@ export const pitServiceFactory = ({ } }: { actorId: string; - projectSlug?: string; - projectId?: string; + projectId: string; environment: string; actor: ActorType; actorOrgId: string; @@ -532,16 +530,6 @@ export const pitServiceFactory = ({ message: string; changes: TProcessNewCommitRawDTO; }) => { - if (!projectSlug && !optionalProjectId) - throw new BadRequestError({ message: "Must provide either project slug or projectId" }); - - let projectId = optionalProjectId as string; - if (!optionalProjectId && projectSlug) { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - projectId = project.id; - } - const policy = actor === ActorType.USER ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) @@ -581,7 +569,7 @@ export const pitServiceFactory = ({ message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`, name: "CreateManySecret" }); - const commitChanges: TCreateCommitChangeDTO[] = []; + const commitChanges: TCommitResourceChangeDTO[] = []; if ((changes.folders?.create?.length ?? 0) > 0) { await folderService.createManyFolders({ @@ -605,7 +593,6 @@ export const pitServiceFactory = ({ if ((changes.folders?.update?.length ?? 0) > 0) { await folderService.updateManyFolders({ projectId, - projectSlug: projectSlug || "", actor, actorId, actorOrgId, diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index c0242f8e7..c079df8f0 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -1,5 +1,6 @@ /* eslint-disable no-nested-ternary */ import { ForbiddenError, subject } from "@casl/ability"; +import { Knex } from "knex"; import { ProjectMembershipRole, @@ -1260,8 +1261,9 @@ export const secretApprovalRequestServiceFactory = ({ policy, projectId, secretPath, - environment - }: TGenerateSecretApprovalRequestV2BridgeDTO) => { + environment, + trx: providedTx + }: TGenerateSecretApprovalRequestV2BridgeDTO & { trx?: Knex }) => { if (actor === ActorType.SERVICE || actor === ActorType.Machine) throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" }); @@ -1487,7 +1489,7 @@ export const secretApprovalRequestServiceFactory = ({ ); }); - const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => { + const executeApprovalRequestCreation = async (tx: Knex) => { const doc = await secretApprovalRequestDAL.create( { folderId, @@ -1549,7 +1551,11 @@ export const secretApprovalRequestServiceFactory = ({ } return { ...doc, commits: approvalCommits }; - }); + }; + + const secretApprovalRequest = providedTx + ? await executeApprovalRequestCreation(providedTx) + : await secretApprovalRequestDAL.transaction(executeApprovalRequestCreation); const user = await userDAL.findById(secretApprovalRequest.committerUserId); const env = await projectEnvDAL.findOne({ id: policy.envId }); diff --git a/backend/src/services/folder-commit/folder-commit-service.ts b/backend/src/services/folder-commit/folder-commit-service.ts index 6d78ef50d..3612cc005 100644 --- a/backend/src/services/folder-commit/folder-commit-service.ts +++ b/backend/src/services/folder-commit/folder-commit-service.ts @@ -47,7 +47,7 @@ export enum ResourceType { FOLDER = "folder" } -export type TCreateCommitChangeDTO = { +export type TCommitResourceChangeDTO = { type: string; secretVersionId?: string; folderVersionId?: string; @@ -65,7 +65,7 @@ type TCreateCommitDTO = { }; message?: string; folderId: string; - changes: TCreateCommitChangeDTO[]; + changes: TCommitResourceChangeDTO[]; omitIgnoreFilter?: boolean; }; @@ -897,16 +897,16 @@ export const folderCommitServiceFactory = ({ } } - const newCommit = await folderCommitDAL.create( - { - actorMetadata: metadata, - actorType: data.actor.type, - message: data.message, - folderId: data.folderId, - envId: folder.envId - }, - tx - ); + const newCommit = await folderCommitDAL.create( + { + actorMetadata: metadata, + actorType: data.actor.type, + message: data.message, + folderId: data.folderId, + envId: folder.envId + }, + tx + ); const batchSize = 500; const chunks = chunkArray(changes, batchSize); diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index 06b7dc5dc..67688106b 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -17,7 +17,7 @@ import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns"; import { ChangeType, CommitType, - TCreateCommitChangeDTO, + TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; import { TProjectDALFactory } from "../project/project-dal"; @@ -251,15 +251,18 @@ export const secretFolderServiceFactory = ({ folders, tx: providedTx, commitChanges - }: TUpdateManyFoldersDTO & { tx?: Knex; commitChanges?: TCreateCommitChangeDTO[]; projectId?: string }) => { + }: TUpdateManyFoldersDTO & { tx?: Knex; commitChanges?: TCommitResourceChangeDTO[]; projectId?: string }) => { let projectId = providedProjectId; - if (!projectId) { + if (!projectId && projectSlug) { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) { throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); } projectId = project.id; } + if (!projectId) { + throw new BadRequestError({ message: "Must provide either project slug or projectId" }); + } const { permission } = await permissionService.getProjectPermission({ actor, @@ -1010,7 +1013,7 @@ export const secretFolderServiceFactory = ({ folders, tx: providedTx, commitChanges - }: TCreateManyFoldersDTO & { tx?: Knex; commitChanges?: TCreateCommitChangeDTO[] }) => { + }: TCreateManyFoldersDTO & { tx?: Knex; commitChanges?: TCommitResourceChangeDTO[] }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -1221,7 +1224,7 @@ export const secretFolderServiceFactory = ({ folders, tx: providedTx, commitChanges - }: TDeleteManyFoldersDTO & { tx?: Knex; commitChanges?: TCreateCommitChangeDTO[] }) => { + }: TDeleteManyFoldersDTO & { tx?: Knex; commitChanges?: TCommitResourceChangeDTO[] }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, diff --git a/backend/src/services/secret-folder/secret-folder-types.ts b/backend/src/services/secret-folder/secret-folder-types.ts index b57e4073f..ae8e2c5dc 100644 --- a/backend/src/services/secret-folder/secret-folder-types.ts +++ b/backend/src/services/secret-folder/secret-folder-types.ts @@ -23,7 +23,7 @@ export type TUpdateFolderDTO = { } & TProjectPermission; export type TUpdateManyFoldersDTO = { - projectSlug: string; + projectSlug?: string; folders: { environment: string; path: string; @@ -75,7 +75,7 @@ export type TCreateManyFoldersDTO = { name: string; environment: string; path: string; - description?: string; + description?: string | null; }>; }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 8db15a7e6..5baed1d32 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -28,7 +28,7 @@ import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { ActorType } from "../auth/auth-type"; -import { TCreateCommitChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; +import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; @@ -1477,7 +1477,7 @@ export const secretV2BridgeServiceFactory = ({ secrets: inputSecrets, tx: providedTx, commitChanges - }: TCreateManySecretDTO & { tx?: Knex; commitChanges?: TCreateCommitChangeDTO[] }) => { + }: TCreateManySecretDTO & { tx?: Knex; commitChanges?: TCommitResourceChangeDTO[] }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -1652,7 +1652,7 @@ export const secretV2BridgeServiceFactory = ({ mode: updateMode, tx: providedTx, commitChanges - }: TUpdateManySecretDTO & { tx?: Knex; commitChanges?: TCreateCommitChangeDTO[] }) => { + }: TUpdateManySecretDTO & { tx?: Knex; commitChanges?: TCommitResourceChangeDTO[] }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -2013,7 +2013,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, tx: providedTx, commitChanges - }: TDeleteManySecretDTO & { tx?: Knex; commitChanges?: TCreateCommitChangeDTO[] }) => { + }: TDeleteManySecretDTO & { tx?: Knex; commitChanges?: TCommitResourceChangeDTO[] }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 974d7bcba..825e7c805 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -8,7 +8,7 @@ import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; -import { TCreateCommitChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; +import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; @@ -167,7 +167,7 @@ export type TFnSecretBulkInsert = { folderId: string; orgId: string; tx?: Knex; - commitChanges?: TCreateCommitChangeDTO[]; + commitChanges?: TCommitResourceChangeDTO[]; inputSecrets: Array< Omit & { tagIds?: string[]; @@ -215,7 +215,7 @@ export type TFnSecretBulkUpdate = { actorId?: string; }; tx?: Knex; - commitChanges?: TCreateCommitChangeDTO[]; + commitChanges?: TCommitResourceChangeDTO[]; }; export type TFnSecretBulkDelete = { @@ -225,7 +225,7 @@ export type TFnSecretBulkDelete = { actorId: string; actorType?: string; tx?: Knex; - commitChanges?: TCreateCommitChangeDTO[]; + commitChanges?: TCommitResourceChangeDTO[]; secretDAL: Pick; secretQueueService: { removeSecretReminder: (data: TRemoveSecretReminderDTO, tx?: Knex) => Promise; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 9a9d0e9c2..963801e50 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -570,7 +570,7 @@ export type TProcessNewCommitRawDTO = { }; folders: { create?: { folderName: string; description?: string }[]; - update?: { folderName: string; description?: string; id: string }[]; + update?: { folderName: string; description?: string | null; id: string }[]; delete?: { folderName: string; id: string }[]; }; }; diff --git a/frontend/src/hooks/api/secretFolders/types.ts b/frontend/src/hooks/api/secretFolders/types.ts index f41267cc8..dc9bce70f 100644 --- a/frontend/src/hooks/api/secretFolders/types.ts +++ b/frontend/src/hooks/api/secretFolders/types.ts @@ -4,13 +4,19 @@ export enum ReservedFolders { SecretReplication = "__reserve_replication_" } +export enum PendingAction { + Create = "create", + Update = "update", + Delete = "delete" +} + export type TSecretFolder = { id: string; name: string; description?: string; parentId?: string | null; isPending?: boolean; - pendingAction?: string; + pendingAction?: PendingAction; }; export type TSecretFolderWithPath = TSecretFolder & { path: string }; diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx index 30c2051a7..804c044db 100644 --- a/frontend/src/hooks/api/secrets/mutations.tsx +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -9,6 +9,7 @@ import { import { commitKeys } from "../folderCommits/queries"; import { secretApprovalRequestKeys } from "../secretApprovalRequest/queries"; +import { PendingAction } from "../secretFolders/types"; import { secretSnapshotKeys } from "../secretSnapshots/queries"; import { secretKeys } from "./queries"; import { @@ -440,9 +441,20 @@ export const useCreateCommit = () => { >({ mutationFn: async ({ workspaceId, environment, secretPath, pendingChanges, message }) => { const transformedSecretUpdates = pendingChanges.secrets - .filter((change) => change.type === "update") + .filter((change) => change.type === PendingAction.Update) .map((change: PendingSecretUpdate) => { - const updatePayload: any = { + const updatePayload: { + secretKey: string; + newSecretName?: string; + secretValue?: string; + secretComment?: string; + skipMultilineEncoding?: boolean; + tagIds?: string[]; + secretMetadata?: { + key: string; + value: string; + }[]; + } = { secretKey: change.secretKey }; @@ -469,14 +481,14 @@ export const useCreateCommit = () => { return updatePayload; }); const { data } = await apiRequest.post("/api/v1/pit/batch/commit", { - workspaceId, + projectId: workspaceId, environment, secretPath, changes: { secrets: { create: pendingChanges.secrets - .filter((change) => change.type === "create") + .filter((change) => change.type === PendingAction.Create) .map((change) => ({ secretKey: change.secretKey, secretValue: change.secretValue, @@ -485,13 +497,22 @@ export const useCreateCommit = () => { tagIds: change.tags?.map((tag) => tag.id), secretMetadata: change.secretMetadata })) || [], - update: transformedSecretUpdates, - delete: pendingChanges.secrets.filter((change) => change.type === "delete") || [] + update: transformedSecretUpdates || [], + delete: + pendingChanges.secrets.filter((change) => change.type === PendingAction.Delete) || [] }, folders: { - create: pendingChanges.folders.filter((change) => change.type === "create") || [], - update: pendingChanges.folders.filter((change) => change.type === "update") || [], - delete: pendingChanges.folders.filter((change) => change.type === "delete") || [] + create: + pendingChanges.folders.filter((change) => change.type === PendingAction.Create) || [], + update: + pendingChanges.folders + .filter((change) => change.type === PendingAction.Update) + .map((change) => ({ + ...change, + description: change.description || null + })) || [], + delete: + pendingChanges.folders.filter((change) => change.type === PendingAction.Delete) || [] } }, message diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 33776a629..29da8e578 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -1,5 +1,6 @@ import { ProjectPermissionActions } from "@app/context"; +import { PendingAction } from "../secretFolders/types"; import type { WsTag } from "../tags/types"; export enum SecretType { @@ -67,7 +68,7 @@ export type SecretV3RawSanitized = { secretReminderRecipients?: SecretReminderRecipient[]; rotationId?: string; isPending?: boolean; - pendingAction?: "create" | "update" | "delete"; + pendingAction?: PendingAction; }; export type SecretV3Raw = { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index b056c22cd..0a66bdfc4 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -47,7 +47,9 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard"; import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits"; import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { useCreateCommit } from "@app/hooks/api/secrets/mutations"; +import { SecretV3RawSanitized } from "@app/hooks/api/types"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView"; @@ -136,14 +138,22 @@ const Page = () => { } }, [isBatchMode, workspaceId, environment, secretPath, loadPendingChanges]); - const handleCreateCommit = (changes: PendingChanges, message: string) => { - createCommit({ - workspaceId, - environment, - secretPath, - pendingChanges: changes, - message - }); + const handleCreateCommit = async (changes: PendingChanges, message: string) => { + try { + await createCommit({ + workspaceId, + environment, + secretPath, + pendingChanges: changes, + message + }); + } catch (error) { + createNotification({ + text: "Failed to commit changes", + type: "error" + }); + console.error(error); + } }; const canReadSecret = hasSecretReadValueOrDescribePermission( @@ -521,7 +531,7 @@ const Page = () => { pendingChanges.secrets.forEach((change) => { switch (change.type) { - case "create": + case PendingAction.Create: mergedSecrets.unshift({ id: change.id, key: change.secretKey, @@ -534,11 +544,11 @@ const Page = () => { updatedAt: new Date().toISOString(), version: 1, isPending: true, - pendingAction: "create" - } as any); + pendingAction: PendingAction.Create + } as unknown as SecretV3RawSanitized); break; - case "update": + case PendingAction.Update: const updateIndex = mergedSecrets.findIndex((s) => s.key === change.secretKey); if (updateIndex >= 0) { mergedSecrets[updateIndex] = { @@ -558,18 +568,18 @@ const Page = () => { : mergedSecrets[updateIndex].skipMultilineEncoding, secretMetadata: change.secretMetadata || mergedSecrets[updateIndex].secretMetadata, isPending: true, - pendingAction: "update" + pendingAction: PendingAction.Update }; } break; - case "delete": + case PendingAction.Delete: const deleteIndex = mergedSecrets.findIndex((s) => s.key === change.secretKey); if (deleteIndex >= 0) { mergedSecrets[deleteIndex] = { ...mergedSecrets[deleteIndex], isPending: true, - pendingAction: "delete" + pendingAction: PendingAction.Delete }; } break; @@ -591,18 +601,18 @@ const Page = () => { pendingChanges.folders.forEach((change) => { switch (change.type) { - case "create": + case PendingAction.Create: mergedFolders.unshift({ id: change.id, name: change.folderName, description: change.description, parentId: null, isPending: true, - pendingAction: "create" + pendingAction: PendingAction.Create } as any); break; - case "update": + case PendingAction.Update: const updateIndex = mergedFolders.findIndex((f) => f.id === change.id); if (updateIndex >= 0) { mergedFolders[updateIndex] = { @@ -613,18 +623,18 @@ const Page = () => { ? change.description : mergedFolders[updateIndex].description, isPending: true, - pendingAction: "update" + pendingAction: PendingAction.Update }; } break; - case "delete": + case PendingAction.Delete: const deleteIndex = mergedFolders.findIndex((f) => f.id === change.id); if (deleteIndex >= 0) { mergedFolders[deleteIndex] = { ...mergedFolders[deleteIndex], isPending: true, - pendingAction: "delete" + pendingAction: PendingAction.Delete }; } break; @@ -758,9 +768,7 @@ const Page = () => { )} {(pendingChanges.secrets.length > 0 || pendingChanges.folders.length > 0) && ( { - await handleCreateCommit(changes, commitMessage); - }} + onCommit={handleCreateCommit} environment={environment} workspaceId={workspaceId} secretPath={secretPath} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx index 858ac9680..a13f3d9b1 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx @@ -4,6 +4,7 @@ import { useRouter } from "@tanstack/react-router"; import { createStore, StateCreator, StoreApi, useStore } from "zustand"; import { useShallow } from "zustand/react/shallow"; +import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; // akhilmhdh: Don't remove this file if ur thinking why use zustand just for selected selects state @@ -19,7 +20,7 @@ export interface BasePendingChange { // Secret-related change types export interface PendingSecretCreate extends BasePendingChange { resourceType: "secret"; - type: "create"; + type: PendingAction.Create; secretKey: string; secretValue: string; secretComment?: string; @@ -31,7 +32,7 @@ export interface PendingSecretCreate extends BasePendingChange { export interface PendingSecretUpdate extends BasePendingChange { resourceType: "secret"; - type: "update"; + type: PendingAction.Update; secretKey: string; newSecretName?: string; originalValue?: string; @@ -48,7 +49,7 @@ export interface PendingSecretUpdate extends BasePendingChange { export interface PendingSecretDelete extends BasePendingChange { resourceType: "secret"; - type: "delete"; + type: PendingAction.Delete; secretKey: string; secretValue: string; } @@ -56,7 +57,7 @@ export interface PendingSecretDelete extends BasePendingChange { // Folder-related change types export interface PendingFolderCreate extends BasePendingChange { resourceType: "folder"; - type: "create"; + type: PendingAction.Create; id: string; folderName: string; description?: string; @@ -65,7 +66,7 @@ export interface PendingFolderCreate extends BasePendingChange { export interface PendingFolderUpdate extends BasePendingChange { resourceType: "folder"; - type: "update"; + type: PendingAction.Update; originalFolderName: string; folderName: string; id: string; @@ -75,7 +76,7 @@ export interface PendingFolderUpdate extends BasePendingChange { export interface PendingFolderDelete extends BasePendingChange { resourceType: "folder"; - type: "delete"; + type: PendingAction.Delete; id: string; folderName: string; folderPath: string; @@ -248,43 +249,45 @@ const createBatchModeStore: StateCreator const newChanges = { ...state.pendingChanges }; if (change.resourceType === "folder") { - if ( - change.type === "create" && - (state.existingFolderNames.has(change.folderName) || - newChanges.folders.some((f) => f.folderName === change.folderName)) - ) { + const existingFolder = + state.existingFolderNames.has(change.folderName) || + newChanges.folders.some((f) => f.folderName === change.folderName); + + if (change.type === PendingAction.Create && existingFolder) { return { pendingChanges: newChanges }; } if ( - change.type === "update" && + change.type === PendingAction.Update && change.folderName !== change.originalFolderName && - (state.existingFolderNames.has(change.folderName) || - newChanges.folders.some((f) => f.folderName === change.folderName)) + existingFolder ) { return { pendingChanges: newChanges }; } } if (change.resourceType === "secret") { - if ( - change.type === "create" && - (state.existingSecretKeys.has(change.secretKey) || - newChanges.secrets.some((s) => s.secretKey === change.secretKey)) - ) { + const existingSecret = + state.existingSecretKeys.has(change.secretKey) || + newChanges.secrets.some((s) => s.secretKey === change.secretKey); + + if (change.type === PendingAction.Create && existingSecret) { return { pendingChanges: newChanges }; } - if ( - change.type === "update" && + + const existingNewSecretName = + change.type === PendingAction.Update && change.newSecretName && change.newSecretName !== change.secretKey && (state.existingSecretKeys.has(change.newSecretName) || newChanges.secrets.some( (s) => (s.secretKey === change.newSecretName || - (s.type === "update" && s.newSecretName === change.newSecretName)) && + (s.type === PendingAction.Update && + s.newSecretName === change.newSecretName)) && s.id !== change.id - )) - ) { + )); + + if (existingNewSecretName) { return { pendingChanges: newChanges }; } } @@ -292,10 +295,10 @@ const createBatchModeStore: StateCreator if (change.resourceType === "secret") { const secretChanges = [...newChanges.secrets]; - if (change.type === "create") { + if (change.type === PendingAction.Create) { const existingCreateIndex = secretChanges.findIndex( (c) => - c.type === "create" && + c.type === PendingAction.Create && (c.secretKey === change.secretKey || c.secretKey === change.originalKey) ); @@ -308,9 +311,9 @@ const createBatchModeStore: StateCreator } else { secretChanges.push(change); } - } else if (change.type === "update") { + } else if (change.type === PendingAction.Update) { const existingCreateIndex = secretChanges.findIndex( - (c) => c.type === "create" && c.id === change.id + (c) => c.type === PendingAction.Create && c.id === change.id ); if (existingCreateIndex >= 0) { @@ -339,7 +342,7 @@ const createBatchModeStore: StateCreator }; } else { const existingUpdateIndex = secretChanges.findIndex( - (c) => c.type === "update" && c.id === change.id + (c) => c.type === PendingAction.Update && c.id === change.id ); if (existingUpdateIndex >= 0) { @@ -394,9 +397,9 @@ const createBatchModeStore: StateCreator } else if (change.resourceType === "folder") { const folderChanges = [...newChanges.folders]; - if (change.type === "create") { + if (change.type === PendingAction.Create) { const existingCreateIndex = folderChanges.findIndex( - (c) => c.type === "create" && c.folderName === change.folderName + (c) => c.type === PendingAction.Create && c.folderName === change.folderName ); if (existingCreateIndex >= 0) { @@ -408,9 +411,9 @@ const createBatchModeStore: StateCreator } else { folderChanges.push(change); } - } else if (change.type === "update") { + } else if (change.type === PendingAction.Update) { const existingCreateIndex = folderChanges.findIndex( - (c) => c.type === "create" && c.folderName === change.originalFolderName + (c) => c.type === PendingAction.Create && c.folderName === change.originalFolderName ); if (existingCreateIndex >= 0) { @@ -426,7 +429,7 @@ const createBatchModeStore: StateCreator }; } else { const existingUpdateIndex = folderChanges.findIndex( - (c) => c.type === "update" && c.id === change.id + (c) => c.type === PendingAction.Update && c.id === change.id ); if (existingUpdateIndex >= 0) { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 48db3177b..5b8d7b247 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -74,6 +74,7 @@ import { } from "@app/hooks/api/dashboard/queries"; import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types"; import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries"; +import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries"; import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types"; import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput"; @@ -193,7 +194,7 @@ export const ActionBar = ({ const pendingFolderCreate: PendingFolderCreate = { id: folderId, resourceType: "folder", - type: "create", + type: PendingAction.Create, folderName, description: description || undefined, parentPath: secretPath, diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx index 240d6db13..40b3ca3a0 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx @@ -12,6 +12,7 @@ import { Modal, ModalContent } from "@app/components/v2"; +import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { PendingChange, @@ -106,11 +107,11 @@ const ChangeTable: React.FC = ({ }) => { const getChangeBadge = (type: PendingChange["type"]) => { switch (type) { - case "create": + case PendingAction.Create: return Created; - case "update": + case PendingAction.Update: return Updated; - case "delete": + case PendingAction.Delete: return Deleted; default: return null; @@ -120,7 +121,7 @@ const ChangeTable: React.FC = ({ const renderSecretChanges = () => { if (change.resourceType !== "secret") return null; - if (change.type === "create") { + if (change.type === PendingAction.Create) { return (
@@ -171,7 +172,7 @@ const ChangeTable: React.FC = ({ ); } - if (change.type === "update") { + if (change.type === PendingAction.Update) { const hasKeyChange = change.newSecretName && change.secretKey !== change.newSecretName; const hasValueChange = change.secretValue !== change.originalValue; const hasCommentChange = change.secretComment !== change.originalComment; @@ -252,7 +253,7 @@ const ChangeTable: React.FC = ({ ); } - if (change.type === "delete") { + if (change.type === PendingAction.Delete) { return (
@@ -275,7 +276,7 @@ const ChangeTable: React.FC = ({ const renderFolderChanges = () => { if (change.resourceType !== "folder") return null; - if (change.type === "create") { + if (change.type === PendingAction.Create) { return (
@@ -300,7 +301,7 @@ const ChangeTable: React.FC = ({ ); } - if (change.type === "update") { + if (change.type === PendingAction.Update) { const hasNameChange = change.folderName !== change.originalFolderName; const hasDescriptionChange = change.description !== change.originalDescription; @@ -334,7 +335,7 @@ const ChangeTable: React.FC = ({ ); } - if (change.type === "delete") { + if (change.type === PendingAction.Delete) { return (
@@ -356,10 +357,12 @@ const ChangeTable: React.FC = ({ const getChangeName = () => { if (change.resourceType === "secret") { - return change.type === "update" ? change.newSecretName || change.secretKey : change.secretKey; + return change.type === PendingAction.Update + ? change.newSecretName || change.secretKey + : change.secretKey; } if (change.resourceType === "folder") { - return change.type === "update" ? change.originalFolderName : change.folderName; + return change.type === PendingAction.Update ? change.originalFolderName : change.folderName; } return "Unknown"; }; @@ -417,19 +420,14 @@ export const CommitForm: React.FC = ({ if (!commitMessage.trim()) { return; } - - try { - await onCommit(pendingChanges, commitMessage); - clearAllPendingChanges({ - workspaceId, - environment, - secretPath - }); - setIsModalOpen(false); - setCommitMessage(""); - } catch (error) { - console.error("Failed to commit changes:", error); - } + await onCommit(pendingChanges, commitMessage); + clearAllPendingChanges({ + workspaceId, + environment, + secretPath + }); + setIsModalOpen(false); + setCommitMessage(""); }; return ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx index 03f5c639a..9b43d820d 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -12,6 +12,7 @@ import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { getKeyValue } from "@app/helpers/parseEnvVar"; import { useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api"; +import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { SecretType } from "@app/hooks/api/types"; import { @@ -96,7 +97,7 @@ export const CreateSecretForm = ({ if (isBatchMode) { const pendingSecretCreate: PendingSecretCreate = { id: key, - type: "create", + type: PendingAction.Create, secretKey: key, secretValue: value || "", secretComment: "", diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx index 1ba1e9809..8859a95fc 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx @@ -12,7 +12,7 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api"; -import { TSecretFolder } from "@app/hooks/api/secretFolders/types"; +import { PendingAction, TSecretFolder } from "@app/hooks/api/secretFolders/types"; import { PendingFolderCreate, @@ -60,15 +60,17 @@ export const FolderListView = ({ oldFolderDescription?: string ) => { try { - const { id: folderId, pendingAction, isPending } = popUp.updateFolder.data as TSecretFolder; + const updateFolderData = popUp.updateFolder.data; + if (!updateFolderData) throw new Error("Update folder data is required"); + const { id: folderId, pendingAction, isPending } = updateFolderData as TSecretFolder; if (isBatchMode) { - const isEditingPendingCreation = isPending && pendingAction === "create"; + const isEditingPendingCreation = isPending && pendingAction === PendingAction.Create; if (isEditingPendingCreation) { const updatedCreate: PendingFolderCreate = { id: folderId, - type: "create", + type: PendingAction.Create, folderName: newFolderName, description: newFolderDescription || undefined, parentPath: secretPath, @@ -84,7 +86,7 @@ export const FolderListView = ({ } else { const updateChange: PendingFolderUpdate = { id: folderId, - type: "update", + type: PendingAction.Update, originalFolderName: oldFolderName || "", folderName: newFolderName, originalDescription: oldFolderDescription, @@ -144,7 +146,7 @@ export const FolderListView = ({ folderName: folderData.name, folderPath: secretPath, resourceType: "folder", - type: "delete", + type: PendingAction.Delete, timestamp: Date.now() }; @@ -197,9 +199,9 @@ export const FolderListView = ({ className={twMerge( "group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700", isPending && "bg-mineshaft-700/60", - pendingAction === "delete" && "border-l-2 border-l-red-600/75", - pendingAction === "update" && "border-l-2 border-l-yellow-600/75", - pendingAction === "create" && "border-l-2 border-l-green-600/75" + pendingAction === PendingAction.Delete && "border-l-2 border-l-red-600/75", + pendingAction === PendingAction.Update && "border-l-2 border-l-yellow-600/75", + pendingAction === PendingAction.Create && "border-l-2 border-l-green-600/75" )} >
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx index 5c90412f6..62b684882 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -50,6 +50,7 @@ import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionCo import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons"; +import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { FontAwesomeSpriteName, formSchema, @@ -88,7 +89,7 @@ type Props = { }[]; }[]; isPending?: boolean; - pendingAction?: "create" | "update" | "delete"; + pendingAction?: PendingAction; }; export const SecretItem = memo( @@ -201,6 +202,8 @@ export const SecretItem = memo( secretTags: selectedTagSlugs }) ); + const isReadOnlySecret = + isReadOnly || isRotatedSecret || (isPending && pendingAction !== PendingAction.Update); const { secretValueHidden } = secret; @@ -289,9 +292,9 @@ export const SecretItem = memo( isDirty && "border-primary-400/50", isRotatedSecret && "bg-mineshaft-700/60", isPending && "bg-mineshaft-700/60", - pendingAction === "delete" && "border-l-2 border-l-red-600/75", - pendingAction === "update" && "border-l-2 border-l-yellow-600/75", - pendingAction === "create" && "border-l-2 border-l-green-600/75" + pendingAction === PendingAction.Delete && "border-l-2 border-l-red-600/75", + pendingAction === PendingAction.Update && "border-l-2 border-l-yellow-600/75", + pendingAction === PendingAction.Create && "border-l-2 border-l-green-600/75" )} >
@@ -383,9 +386,7 @@ export const SecretItem = memo( control={control} render={({ field }) => ( )} - {pendingAction !== "create" && pendingAction !== "delete" && ( + {/* Only allow to open the side panel if the secret is not in a pending create or delete state */} + {pendingAction !== PendingAction.Create && pendingAction !== PendingAction.Delete && (