mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 16:27:40 +00:00
PIT: general improvements and fixes
This commit is contained in:
@@ -3,13 +3,15 @@ import { Knex } from "knex";
|
|||||||
import { inMemoryKeyStore } from "@app/keystore/memory";
|
import { inMemoryKeyStore } from "@app/keystore/memory";
|
||||||
|
|
||||||
import { ProjectType, TableName } from "../schemas";
|
import { ProjectType, TableName } from "../schemas";
|
||||||
|
import { getMigrationEnvConfig } from "./utils/env-config";
|
||||||
import { getMigrationPITServices } from "./utils/services";
|
import { getMigrationPITServices } from "./utils/services";
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
|
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
|
||||||
if (hasFolderCommitTable) {
|
if (hasFolderCommitTable) {
|
||||||
const keyStore = inMemoryKeyStore();
|
const keyStore = inMemoryKeyStore();
|
||||||
const { folderCommitService } = await getMigrationPITServices({ db: knex, keyStore });
|
const envConfig = getMigrationEnvConfig();
|
||||||
|
const { folderCommitService } = await getMigrationPITServices({ db: knex, keyStore, envConfig });
|
||||||
const projects = await knex(TableName.Project).where({ version: 3, type: ProjectType.SecretManager }).select("id");
|
const projects = await knex(TableName.Project).where({ version: 3, type: ProjectType.SecretManager }).select("id");
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
|||||||
@@ -64,7 +64,15 @@ export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }
|
|||||||
return { kmsService };
|
return { kmsService };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMigrationPITServices = async ({ db, keyStore }: { db: Knex; keyStore: TKeyStoreFactory }) => {
|
export const getMigrationPITServices = async ({
|
||||||
|
db,
|
||||||
|
keyStore,
|
||||||
|
envConfig
|
||||||
|
}: {
|
||||||
|
db: Knex;
|
||||||
|
keyStore: TKeyStoreFactory;
|
||||||
|
envConfig: TMigrationEnvConfig;
|
||||||
|
}) => {
|
||||||
const projectDAL = projectDALFactory(db);
|
const projectDAL = projectDALFactory(db);
|
||||||
const folderCommitDAL = folderCommitDALFactory(db);
|
const folderCommitDAL = folderCommitDALFactory(db);
|
||||||
const folderCommitChangesDAL = folderCommitChangesDALFactory(db);
|
const folderCommitChangesDAL = folderCommitChangesDALFactory(db);
|
||||||
@@ -79,6 +87,33 @@ export const getMigrationPITServices = async ({ db, keyStore }: { db: Knex; keyS
|
|||||||
const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore });
|
const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore });
|
||||||
const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db);
|
const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db);
|
||||||
|
|
||||||
|
const orgDAL = orgDALFactory(db);
|
||||||
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
||||||
|
const kmsDAL = kmskeyDALFactory(db);
|
||||||
|
const internalKmsDAL = internalKmsDALFactory(db);
|
||||||
|
|
||||||
|
const hsmModule = initializeHsmModule(envConfig);
|
||||||
|
hsmModule.initialize();
|
||||||
|
|
||||||
|
const hsmService = hsmServiceFactory({
|
||||||
|
hsmModule: hsmModule.getModule(),
|
||||||
|
envConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsService = kmsServiceFactory({
|
||||||
|
kmsRootConfigDAL,
|
||||||
|
keyStore,
|
||||||
|
kmsDAL,
|
||||||
|
internalKmsDAL,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
hsmService,
|
||||||
|
envConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
await hsmService.startService();
|
||||||
|
await kmsService.startService();
|
||||||
|
|
||||||
const folderCommitService = folderCommitServiceFactory({
|
const folderCommitService = folderCommitServiceFactory({
|
||||||
folderCommitDAL,
|
folderCommitDAL,
|
||||||
folderCommitChangesDAL,
|
folderCommitChangesDAL,
|
||||||
@@ -92,7 +127,8 @@ export const getMigrationPITServices = async ({ db, keyStore }: { db: Knex; keyS
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
folderCheckpointResourcesDAL,
|
folderCheckpointResourcesDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
folderTreeCheckpointResourcesDAL
|
folderTreeCheckpointResourcesDAL,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
return { folderCommitService };
|
return { folderCommitService };
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
folderId: z.string().trim(),
|
folderId: z.string().trim(),
|
||||||
deepRollback: z.boolean().default(false),
|
deepRollback: z.boolean().default(false),
|
||||||
message: z.string().trim().optional(),
|
message: z.string().max(256).trim().optional(),
|
||||||
envId: z.string().trim(),
|
envId: z.string().trim(),
|
||||||
projectId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
isSecretCommitChange
|
isSecretCommitChange
|
||||||
} from "@app/services/folder-commit-changes/folder-commit-changes-dal";
|
} from "@app/services/folder-commit-changes/folder-commit-changes-dal";
|
||||||
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
|
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
@@ -21,6 +22,7 @@ type TPitServiceFactoryDep = {
|
|||||||
secretService: Pick<TSecretServiceFactory, "getSecretVersionsV2ByIds" | "getChangeVersions">;
|
secretService: Pick<TSecretServiceFactory, "getSecretVersionsV2ByIds" | "getChangeVersions">;
|
||||||
folderService: Pick<TSecretFolderServiceFactory, "getFolderById" | "getFolderVersions">;
|
folderService: Pick<TSecretFolderServiceFactory, "getFolderById" | "getFolderVersions">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TPitServiceFactory = ReturnType<typeof pitServiceFactory>;
|
export type TPitServiceFactory = ReturnType<typeof pitServiceFactory>;
|
||||||
@@ -29,7 +31,8 @@ export const pitServiceFactory = ({
|
|||||||
folderCommitService,
|
folderCommitService,
|
||||||
secretService,
|
secretService,
|
||||||
folderService,
|
folderService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
folderDAL
|
||||||
}: TPitServiceFactoryDep) => {
|
}: TPitServiceFactoryDep) => {
|
||||||
const getCommitsCount = async ({
|
const getCommitsCount = async ({
|
||||||
actor,
|
actor,
|
||||||
@@ -134,6 +137,8 @@ export const pitServiceFactory = ({
|
|||||||
commitId
|
commitId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(projectId, [changes.folderId]);
|
||||||
|
|
||||||
for (const change of changes.changes) {
|
for (const change of changes.changes) {
|
||||||
if (isSecretCommitChange(change)) {
|
if (isSecretCommitChange(change)) {
|
||||||
change.versions = await secretService.getChangeVersions(
|
change.versions = await secretService.getChangeVersions(
|
||||||
@@ -149,7 +154,9 @@ export const pitServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
change.folderId
|
changes.envId,
|
||||||
|
projectId,
|
||||||
|
folderWithPath?.path || ""
|
||||||
);
|
);
|
||||||
} else if (isFolderCommitChange(change)) {
|
} else if (isFolderCommitChange(change)) {
|
||||||
change.versions = await folderService.getFolderVersions(
|
change.versions = await folderService.getFolderVersions(
|
||||||
@@ -261,7 +268,9 @@ export const pitServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
diff.folderId
|
envId,
|
||||||
|
projectId,
|
||||||
|
diff.folderPath || ""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (change.type === ResourceType.FOLDER) {
|
} else if (change.type === ResourceType.FOLDER) {
|
||||||
|
|||||||
@@ -598,7 +598,8 @@ export const registerRoutes = async (
|
|||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
folderTreeCheckpointResourcesDAL,
|
folderTreeCheckpointResourcesDAL,
|
||||||
folderCommitQueueService,
|
folderCommitQueueService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
const scimService = scimServiceFactory({
|
const scimService = scimServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
@@ -1525,7 +1526,8 @@ export const registerRoutes = async (
|
|||||||
folderCommitService,
|
folderCommitService,
|
||||||
secretService,
|
secretService,
|
||||||
folderService,
|
folderService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
folderDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityOidcAuthService = identityOidcAuthServiceFactory({
|
const identityOidcAuthService = identityOidcAuthServiceFactory({
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export type FolderCommitChange = BaseCommitChangeInfo & {
|
|||||||
folderVersion: string;
|
folderVersion: string;
|
||||||
folderChangeId: string;
|
folderChangeId: string;
|
||||||
versions?: {
|
versions?: {
|
||||||
|
version: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const secretVersionSchema = z.object({
|
|||||||
|
|
||||||
// Folder-specific versions schema
|
// Folder-specific versions schema
|
||||||
const folderVersionSchema = z.object({
|
const folderVersionSchema = z.object({
|
||||||
|
version: z.string().optional(),
|
||||||
name: z.string().optional()
|
name: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas";
|
import { ProjectType, TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
@@ -53,6 +53,10 @@ describe("folderCommitServiceFactory", () => {
|
|||||||
findLatestCommitByFolderIds: vi.fn().mockResolvedValue({})
|
findLatestCommitByFolderIds: vi.fn().mockResolvedValue({})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockKmsService = {
|
||||||
|
createCipherPairWithDataKey: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
const mockFolderCommitChangesDAL = {
|
const mockFolderCommitChangesDAL = {
|
||||||
create: vi.fn().mockResolvedValue({}),
|
create: vi.fn().mockResolvedValue({}),
|
||||||
findByCommitId: vi.fn().mockResolvedValue([]),
|
findByCommitId: vi.fn().mockResolvedValue([]),
|
||||||
@@ -136,7 +140,8 @@ describe("folderCommitServiceFactory", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockProjectDAL = {
|
const mockProjectDAL = {
|
||||||
findById: vi.fn().mockResolvedValue({})
|
findById: vi.fn().mockResolvedValue({}),
|
||||||
|
findProjectByEnvId: vi.fn().mockResolvedValue({})
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockFolderCommitQueueService = {
|
const mockFolderCommitQueueService = {
|
||||||
@@ -181,7 +186,8 @@ describe("folderCommitServiceFactory", () => {
|
|||||||
secretV2BridgeDAL: mockSecretV2BridgeDAL,
|
secretV2BridgeDAL: mockSecretV2BridgeDAL,
|
||||||
folderCommitQueueService: mockFolderCommitQueueService,
|
folderCommitQueueService: mockFolderCommitQueueService,
|
||||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||||
permissionService: mockPermissionService
|
permissionService: mockPermissionService,
|
||||||
|
kmsService: mockKmsService
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -408,6 +414,11 @@ describe("folderCommitServiceFactory", () => {
|
|||||||
{ folderVersionId: "folder-version-1", referencedFolderId: "folder-1" }
|
{ folderVersionId: "folder-version-1", referencedFolderId: "folder-1" }
|
||||||
]);
|
]);
|
||||||
mockFolderCommitDAL.findCommitsToRecreate.mockResolvedValue([]);
|
mockFolderCommitDAL.findCommitsToRecreate.mockResolvedValue([]);
|
||||||
|
mockProjectDAL.findProjectByEnvId.mockResolvedValue({
|
||||||
|
id: "project-id",
|
||||||
|
name: "test-project",
|
||||||
|
type: ProjectType.SecretManager
|
||||||
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const result = await folderCommitService.compareFolderStates({
|
const result = await folderCommitService.compareFolderStates({
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { TFolderCommitChangesDALFactory } from "../folder-commit-changes/folder-
|
|||||||
import { TFolderTreeCheckpointDALFactory } from "../folder-tree-checkpoint/folder-tree-checkpoint-dal";
|
import { TFolderTreeCheckpointDALFactory } from "../folder-tree-checkpoint/folder-tree-checkpoint-dal";
|
||||||
import { TFolderTreeCheckpointResourcesDALFactory } from "../folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
|
import { TFolderTreeCheckpointResourcesDALFactory } from "../folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||||
@@ -80,7 +82,7 @@ type BaseChange = {
|
|||||||
fromVersion?: string;
|
fromVersion?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SecretChange = {
|
type SecretChange = BaseChange & {
|
||||||
type: ResourceType.SECRET;
|
type: ResourceType.SECRET;
|
||||||
secretKey?: string;
|
secretKey?: string;
|
||||||
secretVersion?: string;
|
secretVersion?: string;
|
||||||
@@ -98,7 +100,7 @@ type SecretChange = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type FolderChange = {
|
type FolderChange = BaseChange & {
|
||||||
type: ResourceType.FOLDER;
|
type: ResourceType.FOLDER;
|
||||||
folderName?: string;
|
folderName?: string;
|
||||||
folderVersion?: string;
|
folderVersion?: string;
|
||||||
@@ -107,7 +109,7 @@ type FolderChange = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ResourceChange = BaseChange & (SecretChange | FolderChange);
|
export type ResourceChange = SecretChange | FolderChange;
|
||||||
|
|
||||||
type ActorInfo = {
|
type ActorInfo = {
|
||||||
actorType: string;
|
actorType: string;
|
||||||
@@ -134,12 +136,13 @@ type TFolderCommitServiceFactoryDep = {
|
|||||||
folderVersionDAL: TSecretFolderVersionDALFactory;
|
folderVersionDAL: TSecretFolderVersionDALFactory;
|
||||||
secretVersionV2BridgeDAL: TSecretVersionV2DALFactory;
|
secretVersionV2BridgeDAL: TSecretVersionV2DALFactory;
|
||||||
secretV2BridgeDAL: secretV2BridgeDal.TSecretV2BridgeDALFactory;
|
secretV2BridgeDAL: secretV2BridgeDal.TSecretV2BridgeDALFactory;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectByEnvId">;
|
||||||
folderCommitQueueService?: Pick<
|
folderCommitQueueService?: Pick<
|
||||||
TFolderCommitQueueServiceFactory,
|
TFolderCommitQueueServiceFactory,
|
||||||
"scheduleTreeCheckpoint" | "createFolderTreeCheckpoint"
|
"scheduleTreeCheckpoint" | "createFolderTreeCheckpoint"
|
||||||
>;
|
>;
|
||||||
permissionService?: TPermissionServiceFactory;
|
permissionService?: TPermissionServiceFactory;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const folderCommitServiceFactory = ({
|
export const folderCommitServiceFactory = ({
|
||||||
@@ -157,7 +160,8 @@ export const folderCommitServiceFactory = ({
|
|||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
folderTreeCheckpointResourcesDAL,
|
folderTreeCheckpointResourcesDAL,
|
||||||
folderCommitQueueService,
|
folderCommitQueueService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
kmsService
|
||||||
}: TFolderCommitServiceFactoryDep) => {
|
}: TFolderCommitServiceFactoryDep) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
@@ -394,6 +398,12 @@ export const folderCommitServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `Commit with ID ${targetCommitId} not found` });
|
throw new NotFoundError({ message: `Commit with ID ${targetCommitId} not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const project = await projectDAL.findProjectByEnvId(targetCommit.envId, tx);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundError({ message: `No project found for envId ${targetCommit.envId}` });
|
||||||
|
}
|
||||||
|
|
||||||
// If currentCommitId is not provided, mark all resources in target as creates
|
// If currentCommitId is not provided, mark all resources in target as creates
|
||||||
if (!currentCommitId) {
|
if (!currentCommitId) {
|
||||||
const targetState = await reconstructFolderState(targetCommitId, tx);
|
const targetState = await reconstructFolderState(targetCommitId, tx);
|
||||||
@@ -565,7 +575,63 @@ export const folderCommitServiceFactory = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return differences;
|
const removeNoChangeUpdate: string[] = [];
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
differences.map(async (change) => {
|
||||||
|
if (change.changeType === ChangeType.UPDATE) {
|
||||||
|
if (change.type === ResourceType.FOLDER && change.folderVersion && change.fromVersion) {
|
||||||
|
const versions = await folderVersionDAL.find({
|
||||||
|
folderId: change.id,
|
||||||
|
$in: {
|
||||||
|
version: [Number(change.folderVersion), Number(change.fromVersion)]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const versionsShaped = [...new Set(versions.map((version) => version.name))];
|
||||||
|
if (versionsShaped.length === 1) {
|
||||||
|
removeNoChangeUpdate.push(change.id);
|
||||||
|
}
|
||||||
|
} else if (change.type === ResourceType.SECRET && change.secretVersion && change.fromVersion) {
|
||||||
|
const versions = await secretVersionV2BridgeDAL.findVersionsBySecretIdWithActors({
|
||||||
|
secretId: change.id,
|
||||||
|
projectId: project.id,
|
||||||
|
secretVersions: [change.secretVersion, change.fromVersion]
|
||||||
|
});
|
||||||
|
const versionsShaped = versions.map((el) => ({
|
||||||
|
secretKey: el.key,
|
||||||
|
secretComment: el.encryptedComment
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||||
|
: "",
|
||||||
|
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||||
|
secretReminderRepeatDays: el.reminderRepeatDays,
|
||||||
|
tags: el.tags,
|
||||||
|
metadata: el.metadata,
|
||||||
|
secretReminderNote: el.reminderNote,
|
||||||
|
secretValue: el.encryptedValue
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||||
|
: ""
|
||||||
|
}));
|
||||||
|
const uniqueVersions = versionsShaped.filter(
|
||||||
|
(item, index, arr) =>
|
||||||
|
arr.findIndex((other) =>
|
||||||
|
Object.entries(item).every(
|
||||||
|
([key, value]) => JSON.stringify(value) === JSON.stringify(other[key as keyof typeof other])
|
||||||
|
)
|
||||||
|
) === index
|
||||||
|
);
|
||||||
|
if (uniqueVersions.length === 1) {
|
||||||
|
removeNoChangeUpdate.push(change.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return differences.filter((change) => !removeNoChangeUpdate.includes(change.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1375,6 +1441,63 @@ export const folderCommitServiceFactory = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addNestedFolderChanges = async ({
|
||||||
|
changes,
|
||||||
|
beforeCommit,
|
||||||
|
folderId,
|
||||||
|
folderName,
|
||||||
|
folderPath,
|
||||||
|
step = 1,
|
||||||
|
tx
|
||||||
|
}: {
|
||||||
|
changes: {
|
||||||
|
folderId: string;
|
||||||
|
folderName: string;
|
||||||
|
changes: ResourceChange[];
|
||||||
|
folderPath?: string;
|
||||||
|
}[];
|
||||||
|
beforeCommit: bigint;
|
||||||
|
folderId: string;
|
||||||
|
folderName?: string;
|
||||||
|
folderPath?: string;
|
||||||
|
step?: number;
|
||||||
|
tx?: Knex;
|
||||||
|
}) => {
|
||||||
|
if (step > 20) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const latestFolderCommit = await folderCommitDAL.findCommitBefore(folderId, beforeCommit, tx);
|
||||||
|
if (!latestFolderCommit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const diff = await compareFolderStates({
|
||||||
|
targetCommitId: latestFolderCommit.id,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
changes.push({
|
||||||
|
folderId,
|
||||||
|
folderName: folderName || "",
|
||||||
|
changes: diff,
|
||||||
|
folderPath: folderPath || ""
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
diff.map(async (change) => {
|
||||||
|
if (change.type === ResourceType.FOLDER && change.changeType === ChangeType.CREATE) {
|
||||||
|
await addNestedFolderChanges({
|
||||||
|
changes,
|
||||||
|
beforeCommit,
|
||||||
|
folderId: change.id,
|
||||||
|
folderName: change.folderName,
|
||||||
|
folderPath: `${folderPath}/${change.folderName}`,
|
||||||
|
step: step + 1,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const deepCompareFolder = async ({
|
const deepCompareFolder = async ({
|
||||||
targetCommitId,
|
targetCommitId,
|
||||||
envId,
|
envId,
|
||||||
@@ -1463,7 +1586,12 @@ export const folderCommitServiceFactory = ({
|
|||||||
const folders = await folderDAL.findFoldersByRootAndIds({ rootId: targetCommit.folderId, folderIds }, tx);
|
const folders = await folderDAL.findFoldersByRootAndIds({ rootId: targetCommit.folderId, folderIds }, tx);
|
||||||
const sortedFolders = sortFoldersByHierarchy(folders);
|
const sortedFolders = sortFoldersByHierarchy(folders);
|
||||||
|
|
||||||
const response = [];
|
const response: {
|
||||||
|
folderId: string;
|
||||||
|
folderName: string;
|
||||||
|
changes: ResourceChange[];
|
||||||
|
folderPath?: string;
|
||||||
|
}[] = [];
|
||||||
for (const folder of sortedFolders) {
|
for (const folder of sortedFolders) {
|
||||||
const diff = folderDiffs.get(folder.id);
|
const diff = folderDiffs.get(folder.id);
|
||||||
if (diff) {
|
if (diff) {
|
||||||
@@ -1474,6 +1602,29 @@ export const folderCommitServiceFactory = ({
|
|||||||
changes: diff,
|
changes: diff,
|
||||||
folderPath: folderPath?.[0]?.path
|
folderPath: folderPath?.[0]?.path
|
||||||
});
|
});
|
||||||
|
const recreatedFolders = diff
|
||||||
|
.filter(
|
||||||
|
(change): change is FolderChange =>
|
||||||
|
change.type === ResourceType.FOLDER && change.changeType === ChangeType.CREATE
|
||||||
|
)
|
||||||
|
.map((change) => ({
|
||||||
|
id: change.id,
|
||||||
|
folderName: change.folderName,
|
||||||
|
folderPath: folderPath?.[0]?.path
|
||||||
|
}));
|
||||||
|
await Promise.all(
|
||||||
|
recreatedFolders.map(async (change) => {
|
||||||
|
const nestedFolderPath = folderPath?.[0]?.path;
|
||||||
|
await addNestedFolderChanges({
|
||||||
|
changes: response,
|
||||||
|
beforeCommit: targetCommit.commitId,
|
||||||
|
folderId: change.id,
|
||||||
|
folderName: change.folderName,
|
||||||
|
folderPath: `${nestedFolderPath !== "/" ? nestedFolderPath : ""}/${change.folderName}`,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
TProjectsUpdate
|
TProjectsUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { Filter, ProjectFilterType, SearchProjectSortBy } from "./project-types";
|
import { Filter, ProjectFilterType, SearchProjectSortBy } from "./project-types";
|
||||||
@@ -425,6 +425,16 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
return { docs, totalCount: Number(docs?.[0]?.count ?? 0) };
|
return { docs, totalCount: Number(docs?.[0]?.count ?? 0) };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findProjectByEnvId = async (envId: string, tx?: Knex) => {
|
||||||
|
const project = await (tx || db.replicaNode())(TableName.Project)
|
||||||
|
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter({ id: envId }, TableName.Environment))
|
||||||
|
.select(selectAllTableCols(TableName.Project))
|
||||||
|
.first();
|
||||||
|
return project;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...projectOrm,
|
...projectOrm,
|
||||||
findUserProjects,
|
findUserProjects,
|
||||||
@@ -437,6 +447,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
findProjectWithOrg,
|
findProjectWithOrg,
|
||||||
checkProjectUpgradeStatus,
|
checkProjectUpgradeStatus,
|
||||||
getProjectFromSplitId,
|
getProjectFromSplitId,
|
||||||
searchProjects
|
searchProjects,
|
||||||
|
findProjectByEnvId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -800,6 +800,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
change.isUpdate || change.changeType === ChangeType.UPDATE ? [currentVersion, fromVersion] : [currentVersion]
|
change.isUpdate || change.changeType === ChangeType.UPDATE ? [currentVersion, fromVersion] : [currentVersion]
|
||||||
});
|
});
|
||||||
return versions.map((v) => ({
|
return versions.map((v) => ({
|
||||||
|
version: v.version?.toString() || "1",
|
||||||
name: v.name
|
name: v.name
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2875,24 +2875,21 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
secretId,
|
secretId,
|
||||||
secretVersionNumbers,
|
secretVersionNumbers,
|
||||||
folderId
|
secretPath,
|
||||||
|
envId,
|
||||||
|
projectId
|
||||||
}: TGetSecretVersionsDTO & {
|
}: TGetSecretVersionsDTO & {
|
||||||
secretVersionNumbers: string[];
|
secretVersionNumbers: string[];
|
||||||
folderId: string;
|
secretPath: string;
|
||||||
|
envId: string;
|
||||||
|
projectId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const folder = await folderDAL.findById(folderId);
|
const environment = await projectEnvDAL.findOne({ id: envId });
|
||||||
if (!folder) throw new NotFoundError({ message: `Folder with ID '${folderId}' not found` });
|
|
||||||
|
|
||||||
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]);
|
|
||||||
|
|
||||||
if (!folderWithPath) {
|
|
||||||
throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId: folder.projectId,
|
projectId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
@@ -2906,11 +2903,11 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId: folder.projectId
|
projectId
|
||||||
});
|
});
|
||||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
|
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
|
||||||
secretId,
|
secretId,
|
||||||
projectId: folder.projectId,
|
projectId,
|
||||||
secretVersions: secretVersionNumbers
|
secretVersions: secretVersionNumbers
|
||||||
});
|
});
|
||||||
return secretVersions.map((el) => {
|
return secretVersions.map((el) => {
|
||||||
@@ -2918,8 +2915,8 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
permission,
|
permission,
|
||||||
ProjectPermissionSecretActions.ReadValue,
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
{
|
{
|
||||||
environment: folder.environment.envSlug,
|
environment: environment.slug,
|
||||||
secretPath: folderWithPath.path,
|
secretPath,
|
||||||
secretName: el.key,
|
secretName: el.key,
|
||||||
...(el.tags?.length && {
|
...(el.tags?.length && {
|
||||||
secretTags: el.tags.map((tag) => tag.slug)
|
secretTags: el.tags.map((tag) => tag.slug)
|
||||||
@@ -2928,9 +2925,9 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return reshapeBridgeSecret(
|
return reshapeBridgeSecret(
|
||||||
folder.projectId,
|
projectId,
|
||||||
folder.environment.envSlug,
|
environment.slug,
|
||||||
folderWithPath.path,
|
secretPath,
|
||||||
{
|
{
|
||||||
...el,
|
...el,
|
||||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||||
|
|||||||
@@ -2529,10 +2529,14 @@ export const secretServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
secretId,
|
secretId,
|
||||||
secretVersions,
|
secretVersions,
|
||||||
folderId
|
secretPath,
|
||||||
|
envId,
|
||||||
|
projectId
|
||||||
}: TGetSecretVersionsDTO & {
|
}: TGetSecretVersionsDTO & {
|
||||||
secretVersions: string[];
|
secretVersions: string[];
|
||||||
folderId: string;
|
secretPath: string;
|
||||||
|
envId: string;
|
||||||
|
projectId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const secretVersionV2 = await secretV2BridgeService.getSecretVersionsByIds({
|
const secretVersionV2 = await secretV2BridgeService.getSecretVersionsByIds({
|
||||||
actorId,
|
actorId,
|
||||||
@@ -2540,8 +2544,10 @@ export const secretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
secretId,
|
secretId,
|
||||||
folderId,
|
secretVersionNumbers: secretVersions,
|
||||||
secretVersionNumbers: secretVersions
|
secretPath,
|
||||||
|
envId,
|
||||||
|
projectId
|
||||||
});
|
});
|
||||||
return secretVersionV2;
|
return secretVersionV2;
|
||||||
};
|
};
|
||||||
@@ -3317,7 +3323,9 @@ export const secretServiceFactory = ({
|
|||||||
actor: ActorType,
|
actor: ActorType,
|
||||||
actorOrgId: string,
|
actorOrgId: string,
|
||||||
actorAuthMethod: ActorAuthMethod,
|
actorAuthMethod: ActorAuthMethod,
|
||||||
folderId: string
|
envId: string,
|
||||||
|
projectId: string,
|
||||||
|
secretPath: string
|
||||||
) => {
|
) => {
|
||||||
const currentVersion = change.secretVersion;
|
const currentVersion = change.secretVersion;
|
||||||
const secretId = change.secretId ? change.secretId : change.id;
|
const secretId = change.secretId ? change.secretId : change.id;
|
||||||
@@ -3335,7 +3343,9 @@ export const secretServiceFactory = ({
|
|||||||
change.isUpdate || change.changeType === ChangeType.UPDATE
|
change.isUpdate || change.changeType === ChangeType.UPDATE
|
||||||
? [currentVersion, previousVersion]
|
? [currentVersion, previousVersion]
|
||||||
: [currentVersion],
|
: [currentVersion],
|
||||||
folderId
|
secretPath,
|
||||||
|
envId,
|
||||||
|
projectId
|
||||||
});
|
});
|
||||||
return versions?.map((v) => ({
|
return versions?.map((v) => ({
|
||||||
secretKey: v.secretKey,
|
secretKey: v.secretKey,
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { Helmet } from "react-helmet";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionCommitsActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import { CommitDetailsTab } from "./components/CommitDetailsTab";
|
import { CommitDetailsTab } from "./components/CommitDetailsTab";
|
||||||
|
|
||||||
export const CommitDetailsPage = () => {
|
export const CommitDetailsPage = () => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const envSlug = useParams({
|
const envSlug = useParams({
|
||||||
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id,
|
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id,
|
||||||
select: (el) => el.environment
|
select: (el) => el.environment
|
||||||
@@ -64,17 +66,20 @@ export const CommitDetailsPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-7xl justify-center bg-bunker-800 pb-4 pt-2 text-white">
|
<div className="mx-auto flex w-full max-w-7xl justify-center bg-bunker-800 pb-4 pt-2 text-white">
|
||||||
<Helmet>
|
|
||||||
<title>{t("common.head-title", { title: "Commit History" })}</title>
|
|
||||||
</Helmet>
|
|
||||||
<div className="w-full max-w-[75vw]">
|
<div className="w-full max-w-[75vw]">
|
||||||
<CommitDetailsTab
|
<ProjectPermissionCan
|
||||||
selectedCommitId={selectedCommitId}
|
renderGuardBanner
|
||||||
workspaceId={currentWorkspace.id}
|
I={ProjectPermissionCommitsActions.Read}
|
||||||
goBackToHistory={handleGoBackToHistory}
|
a={ProjectPermissionSub.Commits}
|
||||||
envSlug={envSlug}
|
>
|
||||||
goToRollbackPreview={handleGoToRollbackPreview}
|
<CommitDetailsTab
|
||||||
/>
|
selectedCommitId={selectedCommitId}
|
||||||
|
workspaceId={currentWorkspace.id}
|
||||||
|
goBackToHistory={handleGoBackToHistory}
|
||||||
|
envSlug={envSlug}
|
||||||
|
goToRollbackPreview={handleGoToRollbackPreview}
|
||||||
|
/>
|
||||||
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-3
@@ -243,10 +243,10 @@ export const CommitDetailsTab = ({
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between pb-2">
|
<div className="flex justify-between pb-2">
|
||||||
<div>
|
<div className="w-5/6">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<h1 className="mr-4 text-3xl font-semibold text-white">
|
<h1 className="mr-4 truncate text-3xl font-semibold text-white">
|
||||||
{parsedCommitDetails.changes?.message || "No message"}
|
{parsedCommitDetails.changes?.message || "No message"}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -266,7 +266,7 @@ export const CommitDetailsTab = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-start">
|
<div className="flex items-center justify-start">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionCommitsActions.Read}
|
I={ProjectPermissionCommitsActions.PerformRollback}
|
||||||
a={ProjectPermissionSub.Commits}
|
a={ProjectPermissionSub.Commits}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
|
|||||||
+75
-63
@@ -6,6 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
@@ -17,6 +18,10 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionCommitsActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useCommitRollback, useGetRollbackPreview } from "@app/hooks/api/folderCommits/queries";
|
import { useCommitRollback, useGetRollbackPreview } from "@app/hooks/api/folderCommits/queries";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
@@ -294,71 +299,77 @@ export const RollbackPreviewTab = (): JSX.Element => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-7xl justify-center bg-bunker-800 pb-4 pt-2 text-white">
|
<div className="mx-auto flex w-full max-w-7xl justify-center bg-bunker-800 pb-4 pt-2 text-white">
|
||||||
<div className="w-full max-w-[75vw]">
|
<ProjectPermissionCan
|
||||||
<div className="h-full w-full">
|
renderGuardBanner
|
||||||
<div>
|
I={ProjectPermissionCommitsActions.PerformRollback}
|
||||||
<PageHeader
|
a={ProjectPermissionSub.Commits}
|
||||||
title={`Restore folder at commit ${selectedCommitId.substring(0, 8)}`}
|
>
|
||||||
description="This will restore all changes to how they appeared at the point in time of this commit. Any modifications made after this commit will be undone."
|
<div className="w-full max-w-[75vw]">
|
||||||
/>
|
<div className="h-full w-full">
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title={`Restore folder at commit ${selectedCommitId.substring(0, 8)}`}
|
||||||
|
description="This will restore all changes to how they appeared at the point in time of this commit. Any modifications made after this commit will be undone."
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex w-full border border-mineshaft-600">
|
<div className="flex w-full border border-mineshaft-600">
|
||||||
{renderSidebar()}
|
{renderSidebar()}
|
||||||
{renderMainContent()}
|
{renderMainContent()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-x border-mineshaft-600 bg-mineshaft-800 px-6 py-3">
|
<div className="border-x border-mineshaft-600 bg-mineshaft-800 px-6 py-3">
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Tooltip content="Enable/Disable deep rollback to restore all nested folders to their previous state">
|
<Tooltip content="Enable/Disable deep rollback to restore all nested folders to their previous state">
|
||||||
<FontAwesomeIcon icon={faInfoCircle} className="mr-2 text-mineshaft-400" />
|
<FontAwesomeIcon icon={faInfoCircle} className="mr-2 text-mineshaft-400" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Switch
|
<Switch
|
||||||
className="ml-2 bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/50"
|
className="ml-2 bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/50"
|
||||||
thumbClassName="bg-mineshaft-800"
|
thumbClassName="bg-mineshaft-800"
|
||||||
isChecked={deepRollback}
|
isChecked={deepRollback}
|
||||||
onCheckedChange={setDeepRollback}
|
onCheckedChange={setDeepRollback}
|
||||||
id="deep-rollback"
|
id="deep-rollback"
|
||||||
|
>
|
||||||
|
Recursively Restore Child Folders
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-mineshaft-600 py-4">
|
||||||
|
<div className="flex w-full items-center justify-between gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Restore Message"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
className="w-full border-mineshaft-500 bg-mineshaft-700 py-2 text-sm"
|
||||||
|
maxLength={256}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("rollbackConfirm");
|
||||||
|
}}
|
||||||
|
colorSchema="primary"
|
||||||
|
className="px-6 py-2"
|
||||||
|
isDisabled={
|
||||||
|
message.length === 0 ||
|
||||||
|
!rollbackChangesNested ||
|
||||||
|
!rollbackChangesNested?.some((folder) => {
|
||||||
|
return folder.changes.length > 0;
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Recursively Restore Child Folders
|
Restore
|
||||||
</Switch>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-mineshaft-600 py-4">
|
<DeleteActionModal
|
||||||
<div className="flex w-full items-center justify-between gap-2">
|
isOpen={popUp.rollbackConfirm.isOpen}
|
||||||
<Input
|
deleteKey="restore"
|
||||||
placeholder="Restore Message"
|
title={`Are you sure you want to restore to commit ${selectedCommitId?.substring(0, 8)}?`}
|
||||||
value={message}
|
subTitle={`
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
className="w-full border-mineshaft-500 bg-mineshaft-700 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("rollbackConfirm");
|
|
||||||
}}
|
|
||||||
colorSchema="primary"
|
|
||||||
className="px-6 py-2"
|
|
||||||
isDisabled={
|
|
||||||
message.length === 0 ||
|
|
||||||
!rollbackChangesNested ||
|
|
||||||
!rollbackChangesNested?.some((folder) => {
|
|
||||||
return folder.changes.length > 0;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Restore
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.rollbackConfirm.isOpen}
|
|
||||||
deleteKey="restore"
|
|
||||||
title={`Are you sure you want to restore to commit ${selectedCommitId?.substring(0, 8)}?`}
|
|
||||||
subTitle={`
|
|
||||||
${
|
${
|
||||||
deepRollback
|
deepRollback
|
||||||
? "This will revert this folder and all child folders to how they appeared at the point in time of this commit."
|
? "This will revert this folder and all child folders to how they appeared at the point in time of this commit."
|
||||||
@@ -366,12 +377,13 @@ export const RollbackPreviewTab = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
Any changes made after this commit will be permanently removed.
|
Any changes made after this commit will be permanently removed.
|
||||||
`}
|
`}
|
||||||
onChange={(isOpen) => handlePopUpToggle("rollbackConfirm", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("rollbackConfirm", isOpen)}
|
||||||
onDeleteApproved={handleRollback}
|
onDeleteApproved={handleRollback}
|
||||||
buttonText="Restore"
|
buttonText="Restore"
|
||||||
/>
|
/>
|
||||||
|
</div>{" "}
|
||||||
</div>{" "}
|
</div>{" "}
|
||||||
</div>{" "}
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
-3
@@ -505,7 +505,6 @@ export const SecretVersionDiffView = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sortedVersions = [...item.versions].sort((a, b) => b.version - a.version);
|
const sortedVersions = [...item.versions].sort((a, b) => b.version - a.version);
|
||||||
|
|
||||||
let oldVersion = null;
|
let oldVersion = null;
|
||||||
let newVersion = null;
|
let newVersion = null;
|
||||||
let oldVersionContent = null;
|
let oldVersionContent = null;
|
||||||
@@ -562,13 +561,11 @@ export const SecretVersionDiffView = ({
|
|||||||
} else if (item.isAdded) {
|
} else if (item.isAdded) {
|
||||||
[newVersion] = sortedVersions;
|
[newVersion] = sortedVersions;
|
||||||
const cleanNewVersion = cleanVersionForComparison(newVersion);
|
const cleanNewVersion = cleanVersionForComparison(newVersion);
|
||||||
oldVersionContent = <div className="italic text-gray-400">No previous version</div>;
|
|
||||||
newVersionContent = formatAddedJson(cleanNewVersion);
|
newVersionContent = formatAddedJson(cleanNewVersion);
|
||||||
} else if (item.isDeleted) {
|
} else if (item.isDeleted) {
|
||||||
[oldVersion] = sortedVersions;
|
[oldVersion] = sortedVersions;
|
||||||
const cleanOldVersion = cleanVersionForComparison(oldVersion);
|
const cleanOldVersion = cleanVersionForComparison(oldVersion);
|
||||||
oldVersionContent = formatDeletedJson(cleanOldVersion);
|
oldVersionContent = formatDeletedJson(cleanOldVersion);
|
||||||
newVersionContent = <div className="italic text-gray-400">Version deleted</div>;
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { Helmet } from "react-helmet";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { PageHeader } from "@app/components/v2";
|
import { PageHeader } from "@app/components/v2";
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionCommitsActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import { CommitHistoryTab } from "./components/CommitHistoryTab";
|
import { CommitHistoryTab } from "./components/CommitHistoryTab";
|
||||||
|
|
||||||
export const CommitsPage = () => {
|
export const CommitsPage = () => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const envSlug = useParams({
|
const envSlug = useParams({
|
||||||
from: ROUTE_PATHS.SecretManager.CommitsPage.id,
|
from: ROUTE_PATHS.SecretManager.CommitsPage.id,
|
||||||
select: (el) => el.environment
|
select: (el) => el.environment
|
||||||
@@ -45,20 +47,23 @@ export const CommitsPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex h-full w-full max-w-7xl justify-center bg-bunker-800 py-4 text-white">
|
<div className="mx-auto flex h-full w-full max-w-7xl justify-center bg-bunker-800 py-4 text-white">
|
||||||
<Helmet>
|
|
||||||
<title>{t("common.head-title", { title: "Commit History" })}</title>
|
|
||||||
</Helmet>
|
|
||||||
<div className="w-full max-w-[75vw]">
|
<div className="w-full max-w-[75vw]">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Commits"
|
title="Commits"
|
||||||
description="Track, inspect, and restore your secrets and folders with confidence. View the complete history of changes made to your environment, examine specific modifications at each commit point, and preview the exact impact before rolling back to previous states."
|
description="Track, inspect, and restore your secrets and folders with confidence. View the complete history of changes made to your environment, examine specific modifications at each commit point, and preview the exact impact before rolling back to previous states."
|
||||||
/>
|
/>
|
||||||
<CommitHistoryTab
|
<ProjectPermissionCan
|
||||||
onSelectCommit={handleSelectCommit}
|
renderGuardBanner
|
||||||
projectId={currentWorkspace.id}
|
I={ProjectPermissionCommitsActions.Read}
|
||||||
environment={envSlug}
|
a={ProjectPermissionSub.Commits}
|
||||||
secretPath={secretPath}
|
>
|
||||||
/>
|
<CommitHistoryTab
|
||||||
|
onSelectCommit={handleSelectCommit}
|
||||||
|
projectId={currentWorkspace.id}
|
||||||
|
environment={envSlug}
|
||||||
|
secretPath={secretPath}
|
||||||
|
/>
|
||||||
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user