Merge pull request #3555 from Infisical/feat/point-in-time-revamp-2710

Feat/point in time revamp 2710
This commit is contained in:
carlosmonastyrski
2025-05-20 09:46:08 -03:00
committed by GitHub
77 changed files with 7826 additions and 66 deletions
+2
View File
@@ -57,6 +57,7 @@ import { TCertificateTemplateServiceFactory } from "@app/services/certificate-te
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { THsmServiceFactory } from "@app/services/hsm/hsm-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
@@ -252,6 +253,7 @@ declare module "fastify" {
microsoftTeams: TMicrosoftTeamsServiceFactory;
assumePrivileges: TAssumePrivilegeServiceFactory;
githubOrgSync: TGithubOrgSyncServiceFactory;
folderCommit: TFolderCommitServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer
+48
View File
@@ -74,6 +74,24 @@ import {
TExternalKms,
TExternalKmsInsert,
TExternalKmsUpdate,
TFolderCheckpointResources,
TFolderCheckpointResourcesInsert,
TFolderCheckpointResourcesUpdate,
TFolderCheckpoints,
TFolderCheckpointsInsert,
TFolderCheckpointsUpdate,
TFolderCommitChanges,
TFolderCommitChangesInsert,
TFolderCommitChangesUpdate,
TFolderCommits,
TFolderCommitsInsert,
TFolderCommitsUpdate,
TFolderTreeCheckpointResources,
TFolderTreeCheckpointResourcesInsert,
TFolderTreeCheckpointResourcesUpdate,
TFolderTreeCheckpoints,
TFolderTreeCheckpointsInsert,
TFolderTreeCheckpointsUpdate,
TGateways,
TGatewaysInsert,
TGatewaysUpdate,
@@ -1048,5 +1066,35 @@ declare module "knex/types/tables" {
TGithubOrgSyncConfigsInsert,
TGithubOrgSyncConfigsUpdate
>;
[TableName.FolderCommit]: KnexOriginal.CompositeTableType<
TFolderCommits,
TFolderCommitsInsert,
TFolderCommitsUpdate
>;
[TableName.FolderCommitChanges]: KnexOriginal.CompositeTableType<
TFolderCommitChanges,
TFolderCommitChangesInsert,
TFolderCommitChangesUpdate
>;
[TableName.FolderCheckpoint]: KnexOriginal.CompositeTableType<
TFolderCheckpoints,
TFolderCheckpointsInsert,
TFolderCheckpointsUpdate
>;
[TableName.FolderCheckpointResources]: KnexOriginal.CompositeTableType<
TFolderCheckpointResources,
TFolderCheckpointResourcesInsert,
TFolderCheckpointResourcesUpdate
>;
[TableName.FolderTreeCheckpoint]: KnexOriginal.CompositeTableType<
TFolderTreeCheckpoints,
TFolderTreeCheckpointsInsert,
TFolderTreeCheckpointsUpdate
>;
[TableName.FolderTreeCheckpointResources]: KnexOriginal.CompositeTableType<
TFolderTreeCheckpointResources,
TFolderTreeCheckpointResourcesInsert,
TFolderTreeCheckpointResourcesUpdate
>;
}
}
@@ -0,0 +1,149 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
if (!hasFolderCommitTable) {
await knex.schema.createTable(TableName.FolderCommit, (t) => {
t.uuid("id").primary().defaultTo(knex.fn.uuid());
t.bigIncrements("commitId");
t.jsonb("actorMetadata").notNullable();
t.string("actorType").notNullable();
t.string("message");
t.uuid("folderId").notNullable();
t.uuid("envId").notNullable();
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
t.timestamps(true, true, true);
t.index("folderId");
t.index("envId");
});
}
const hasFolderCommitChangesTable = await knex.schema.hasTable(TableName.FolderCommitChanges);
if (!hasFolderCommitChangesTable) {
await knex.schema.createTable(TableName.FolderCommitChanges, (t) => {
t.uuid("id").primary().defaultTo(knex.fn.uuid());
t.uuid("folderCommitId").notNullable();
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
t.string("changeType").notNullable();
t.boolean("isUpdate").notNullable().defaultTo(false);
t.uuid("secretVersionId");
t.foreign("secretVersionId").references("id").inTable(TableName.SecretVersionV2).onDelete("CASCADE");
t.uuid("folderVersionId");
t.foreign("folderVersionId").references("id").inTable(TableName.SecretFolderVersion).onDelete("CASCADE");
t.timestamps(true, true, true);
t.index("folderCommitId");
t.index("secretVersionId");
t.index("folderVersionId");
});
}
const hasFolderCheckpointTable = await knex.schema.hasTable(TableName.FolderCheckpoint);
if (!hasFolderCheckpointTable) {
await knex.schema.createTable(TableName.FolderCheckpoint, (t) => {
t.uuid("id").primary().defaultTo(knex.fn.uuid());
t.uuid("folderCommitId").notNullable();
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
t.timestamps(true, true, true);
t.index("folderCommitId");
});
}
const hasFolderCheckpointResourcesTable = await knex.schema.hasTable(TableName.FolderCheckpointResources);
if (!hasFolderCheckpointResourcesTable) {
await knex.schema.createTable(TableName.FolderCheckpointResources, (t) => {
t.uuid("id").primary().defaultTo(knex.fn.uuid());
t.uuid("folderCheckpointId").notNullable();
t.foreign("folderCheckpointId").references("id").inTable(TableName.FolderCheckpoint).onDelete("CASCADE");
t.uuid("secretVersionId");
t.foreign("secretVersionId").references("id").inTable(TableName.SecretVersionV2).onDelete("CASCADE");
t.uuid("folderVersionId");
t.foreign("folderVersionId").references("id").inTable(TableName.SecretFolderVersion).onDelete("CASCADE");
t.timestamps(true, true, true);
t.index("folderCheckpointId");
t.index("secretVersionId");
t.index("folderVersionId");
});
}
const hasFolderTreeCheckpointTable = await knex.schema.hasTable(TableName.FolderTreeCheckpoint);
if (!hasFolderTreeCheckpointTable) {
await knex.schema.createTable(TableName.FolderTreeCheckpoint, (t) => {
t.uuid("id").primary().defaultTo(knex.fn.uuid());
t.uuid("folderCommitId").notNullable();
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
t.timestamps(true, true, true);
t.index("folderCommitId");
});
}
const hasFolderTreeCheckpointResourcesTable = await knex.schema.hasTable(TableName.FolderTreeCheckpointResources);
if (!hasFolderTreeCheckpointResourcesTable) {
await knex.schema.createTable(TableName.FolderTreeCheckpointResources, (t) => {
t.uuid("id").primary().defaultTo(knex.fn.uuid());
t.uuid("folderTreeCheckpointId").notNullable();
t.foreign("folderTreeCheckpointId").references("id").inTable(TableName.FolderTreeCheckpoint).onDelete("CASCADE");
t.uuid("folderId").notNullable();
t.uuid("folderCommitId").notNullable();
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
t.timestamps(true, true, true);
t.index("folderTreeCheckpointId");
t.index("folderId");
t.index("folderCommitId");
});
}
await createOnUpdateTrigger(knex, TableName.FolderCommit);
await createOnUpdateTrigger(knex, TableName.FolderCommitChanges);
await createOnUpdateTrigger(knex, TableName.FolderCheckpoint);
await createOnUpdateTrigger(knex, TableName.FolderCheckpointResources);
await createOnUpdateTrigger(knex, TableName.FolderTreeCheckpoint);
await createOnUpdateTrigger(knex, TableName.FolderTreeCheckpointResources);
}
export async function down(knex: Knex): Promise<void> {
const hasFolderCheckpointResourcesTable = await knex.schema.hasTable(TableName.FolderCheckpointResources);
const hasFolderTreeCheckpointResourcesTable = await knex.schema.hasTable(TableName.FolderTreeCheckpointResources);
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
const hasFolderCommitChangesTable = await knex.schema.hasTable(TableName.FolderCommitChanges);
const hasFolderTreeCheckpointTable = await knex.schema.hasTable(TableName.FolderTreeCheckpoint);
const hasFolderCheckpointTable = await knex.schema.hasTable(TableName.FolderCheckpoint);
if (hasFolderTreeCheckpointResourcesTable) {
await dropOnUpdateTrigger(knex, TableName.FolderTreeCheckpointResources);
await knex.schema.dropTableIfExists(TableName.FolderTreeCheckpointResources);
}
if (hasFolderCheckpointResourcesTable) {
await dropOnUpdateTrigger(knex, TableName.FolderCheckpointResources);
await knex.schema.dropTableIfExists(TableName.FolderCheckpointResources);
}
if (hasFolderTreeCheckpointTable) {
await dropOnUpdateTrigger(knex, TableName.FolderTreeCheckpoint);
await knex.schema.dropTableIfExists(TableName.FolderTreeCheckpoint);
}
if (hasFolderCheckpointTable) {
await dropOnUpdateTrigger(knex, TableName.FolderCheckpoint);
await knex.schema.dropTableIfExists(TableName.FolderCheckpoint);
}
if (hasFolderCommitChangesTable) {
await dropOnUpdateTrigger(knex, TableName.FolderCommitChanges);
await knex.schema.dropTableIfExists(TableName.FolderCommitChanges);
}
if (hasFolderCommitTable) {
await dropOnUpdateTrigger(knex, TableName.FolderCommit);
await knex.schema.dropTableIfExists(TableName.FolderCommit);
}
}
@@ -0,0 +1,29 @@
import { Knex } from "knex";
import { inMemoryKeyStore } from "@app/keystore/memory";
import { ProjectType, TableName } from "../schemas";
import { getMigrationPITServices } from "./utils/services";
export async function up(knex: Knex): Promise<void> {
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
if (hasFolderCommitTable) {
const keyStore = inMemoryKeyStore();
const { folderCommitService } = await getMigrationPITServices({ db: knex, keyStore });
const projects = await knex(TableName.Project).where({ version: 3, type: ProjectType.SecretManager }).select("id");
await knex.transaction(async (tx) => {
for (const project of projects) {
// eslint-disable-next-line no-await-in-loop
await folderCommitService.initializeProject(project.id, tx);
}
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
if (hasFolderCommitTable) {
// delete all existing entries
await knex(TableName.FolderCommit).del();
}
}
@@ -3,12 +3,25 @@ import { Knex } from "knex";
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal";
import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal";
import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal";
import { folderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
import { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-dal";
import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal";
import { folderTreeCheckpointResourcesDALFactory } from "@app/services/folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
import { identityDALFactory } from "@app/services/identity/identity-dal";
import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
import { kmsServiceFactory } from "@app/services/kms/kms-service";
import { orgDALFactory } from "@app/services/org/org-dal";
import { projectDALFactory } from "@app/services/project/project-dal";
import { secretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { secretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal";
import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { userDALFactory } from "@app/services/user/user-dal";
import { TMigrationEnvConfig } from "./env-config";
@@ -50,3 +63,37 @@ export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }
return { kmsService };
};
export const getMigrationPITServices = async ({ db, keyStore }: { db: Knex; keyStore: TKeyStoreFactory }) => {
const projectDAL = projectDALFactory(db);
const folderCommitDAL = folderCommitDALFactory(db);
const folderCommitChangesDAL = folderCommitChangesDALFactory(db);
const folderCheckpointDAL = folderCheckpointDALFactory(db);
const folderTreeCheckpointDAL = folderTreeCheckpointDALFactory(db);
const userDAL = userDALFactory(db);
const identityDAL = identityDALFactory(db);
const folderDAL = secretFolderDALFactory(db);
const folderVersionDAL = secretFolderVersionDALFactory(db);
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
const folderCheckpointResourcesDAL = folderCheckpointResourcesDALFactory(db);
const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore });
const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db);
const folderCommitService = folderCommitServiceFactory({
folderCommitDAL,
folderCommitChangesDAL,
folderCheckpointDAL,
folderTreeCheckpointDAL,
userDAL,
identityDAL,
folderDAL,
folderVersionDAL,
secretVersionV2BridgeDAL,
projectDAL,
folderCheckpointResourcesDAL,
secretV2BridgeDAL,
folderTreeCheckpointResourcesDAL
});
return { folderCommitService };
};
@@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const FolderCheckpointResourcesSchema = z.object({
id: z.string().uuid(),
folderCheckpointId: z.string().uuid(),
secretVersionId: z.string().uuid().nullable().optional(),
folderVersionId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TFolderCheckpointResources = z.infer<typeof FolderCheckpointResourcesSchema>;
export type TFolderCheckpointResourcesInsert = Omit<z.input<typeof FolderCheckpointResourcesSchema>, TImmutableDBKeys>;
export type TFolderCheckpointResourcesUpdate = Partial<
Omit<z.input<typeof FolderCheckpointResourcesSchema>, TImmutableDBKeys>
>;
@@ -0,0 +1,19 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const FolderCheckpointsSchema = z.object({
id: z.string().uuid(),
folderCommitId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TFolderCheckpoints = z.infer<typeof FolderCheckpointsSchema>;
export type TFolderCheckpointsInsert = Omit<z.input<typeof FolderCheckpointsSchema>, TImmutableDBKeys>;
export type TFolderCheckpointsUpdate = Partial<Omit<z.input<typeof FolderCheckpointsSchema>, TImmutableDBKeys>>;
@@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const FolderCommitChangesSchema = z.object({
id: z.string().uuid(),
folderCommitId: z.string().uuid(),
changeType: z.string(),
isUpdate: z.boolean().default(false),
secretVersionId: z.string().uuid().nullable().optional(),
folderVersionId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TFolderCommitChanges = z.infer<typeof FolderCommitChangesSchema>;
export type TFolderCommitChangesInsert = Omit<z.input<typeof FolderCommitChangesSchema>, TImmutableDBKeys>;
export type TFolderCommitChangesUpdate = Partial<Omit<z.input<typeof FolderCommitChangesSchema>, TImmutableDBKeys>>;
+24
View File
@@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const FolderCommitsSchema = z.object({
id: z.string().uuid(),
commitId: z.coerce.number(),
actorMetadata: z.unknown(),
actorType: z.string(),
message: z.string().nullable().optional(),
folderId: z.string().uuid(),
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TFolderCommits = z.infer<typeof FolderCommitsSchema>;
export type TFolderCommitsInsert = Omit<z.input<typeof FolderCommitsSchema>, TImmutableDBKeys>;
export type TFolderCommitsUpdate = Partial<Omit<z.input<typeof FolderCommitsSchema>, TImmutableDBKeys>>;
@@ -0,0 +1,26 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const FolderTreeCheckpointResourcesSchema = z.object({
id: z.string().uuid(),
folderTreeCheckpointId: z.string().uuid(),
folderId: z.string().uuid(),
folderCommitId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TFolderTreeCheckpointResources = z.infer<typeof FolderTreeCheckpointResourcesSchema>;
export type TFolderTreeCheckpointResourcesInsert = Omit<
z.input<typeof FolderTreeCheckpointResourcesSchema>,
TImmutableDBKeys
>;
export type TFolderTreeCheckpointResourcesUpdate = Partial<
Omit<z.input<typeof FolderTreeCheckpointResourcesSchema>, TImmutableDBKeys>
>;
@@ -0,0 +1,19 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const FolderTreeCheckpointsSchema = z.object({
id: z.string().uuid(),
folderCommitId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TFolderTreeCheckpoints = z.infer<typeof FolderTreeCheckpointsSchema>;
export type TFolderTreeCheckpointsInsert = Omit<z.input<typeof FolderTreeCheckpointsSchema>, TImmutableDBKeys>;
export type TFolderTreeCheckpointsUpdate = Partial<Omit<z.input<typeof FolderTreeCheckpointsSchema>, TImmutableDBKeys>>;
+6
View File
@@ -22,6 +22,12 @@ export * from "./dynamic-secret-leases";
export * from "./dynamic-secrets";
export * from "./external-group-org-role-mappings";
export * from "./external-kms";
export * from "./folder-checkpoint-resources";
export * from "./folder-checkpoints";
export * from "./folder-commit-changes";
export * from "./folder-commits";
export * from "./folder-tree-checkpoint-resources";
export * from "./folder-tree-checkpoints";
export * from "./gateways";
export * from "./git-app-install-sessions";
export * from "./git-app-org";
+8 -2
View File
@@ -152,10 +152,16 @@ export enum TableName {
MicrosoftTeamsIntegrations = "microsoft_teams_integrations",
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs",
SecretReminderRecipients = "secret_reminder_recipients",
GithubOrgSyncConfig = "github_org_sync_configs"
GithubOrgSyncConfig = "github_org_sync_configs",
FolderCommit = "folder_commits",
FolderCommitChanges = "folder_commit_changes",
FolderCheckpoint = "folder_checkpoints",
FolderCheckpointResources = "folder_checkpoint_resources",
FolderTreeCheckpoint = "folder_tree_checkpoints",
FolderTreeCheckpointResources = "folder_tree_checkpoint_resources"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId";
export const UserDeviceSchema = z
.object({
+2
View File
@@ -18,6 +18,7 @@ import { registerLdapRouter } from "./ldap-router";
import { registerLicenseRouter } from "./license-router";
import { registerOidcRouter } from "./oidc-router";
import { registerOrgRoleRouter } from "./org-role-router";
import { registerPITRouter } from "./pit-router";
import { registerProjectRoleRouter } from "./project-role-router";
import { registerProjectRouter } from "./project-router";
import { registerRateLimitRouter } from "./rate-limit-router";
@@ -53,6 +54,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
{ prefix: "/workspace" }
);
await server.register(registerSnapshotRouter, { prefix: "/secret-snapshot" });
await server.register(registerPITRouter, { prefix: "/pit" });
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
await server.register(registerSecretApprovalRequestRouter, {
prefix: "/secret-approval-requests"
+613
View File
@@ -0,0 +1,613 @@
/* eslint-disable @typescript-eslint/no-base-to-string */
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { NotFoundError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { booleanSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ChangeType } from "@app/services/folder-commit/folder-commit-service";
const commitHistoryItemSchema = z.object({
id: z.string(),
folderId: z.string(),
actorType: z.string(),
actorMetadata: z.unknown().optional(),
message: z.string().optional().nullable(),
commitId: z.string(),
createdAt: z.string().or(z.date()),
envId: z.string()
});
const versionSchema = z.object({
secretKey: z.string().optional(),
secretComment: z.string().optional().nullable(),
skipMultilineEncoding: z.boolean().optional().nullable(),
secretReminderRepeatDays: z.number().optional().nullable(),
secretReminderNote: z.string().optional().nullable(),
metadata: z.unknown().optional().nullable(),
tags: z.array(z.string()).optional().nullable(),
secretReminderRecipients: z.array(z.any()).optional().nullable(),
secretValue: z.string().optional().nullable(),
name: z.string().optional().nullable()
});
const folderStateSchema = z.array(
z.object({
type: z.string(),
id: z.string(),
versionId: z.string(),
secretKey: z.string().optional(),
secretVersion: z.number().optional(),
folderName: z.string().optional(),
folderVersion: z.number().optional()
})
);
export const registerPITRouter = async (server: FastifyZodProvider) => {
// Get commits count for a folder
server.route({
method: "GET",
url: "/commits/count/:workspaceId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
querystring: z.object({
environment: z.string().trim(),
path: z.string().trim().default("/").transform(removeTrailingSlash)
}),
response: {
200: z.object({
count: z.number(),
folderId: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const res = await server.services.folderCommit.getCommitsCount({
actor: req.permission?.type,
actorId: req.permission?.id,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
projectId: req.params.workspaceId,
environment: req.query.environment,
path: req.query.path
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.GET_PROJECT_PIT_COMMIT_COUNT,
metadata: {
environment: req.query.environment,
path: req.query.path,
commitCount: res.count.toString()
}
}
});
return res;
}
});
// Get all commits for a folder
server.route({
method: "GET",
url: "/commits/:workspaceId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
querystring: z.object({
environment: z.string().trim(),
path: z.string().trim().default("/").transform(removeTrailingSlash)
}),
response: {
200: commitHistoryItemSchema.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const commits = await server.services.folderCommit.getCommitsForFolder({
actor: req.permission?.type,
actorId: req.permission?.id,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
projectId: req.params.workspaceId,
environment: req.query.environment,
path: req.query.path
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.GET_PROJECT_PIT_COMMITS,
metadata: {
environment: req.query.environment,
path: req.query.path,
commitCount: commits.length.toString()
}
}
});
return commits.map((commit) => ({
...commit,
commitId: commit.commitId.toString()
}));
}
});
// Get commit changes for a specific commit
server.route({
method: "GET",
url: "/commits/:workspaceId/:commitId/changes",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim(),
commitId: z.string().trim()
}),
response: {
200: z.object({
changes: z.object({
id: z.string(),
commitId: z.string(),
actorMetadata: z
.union([
z.object({
id: z.string().optional(),
name: z.string().optional()
}),
z.unknown()
])
.optional(),
actorType: z.string(),
message: z.string().optional().nullable(),
folderId: z.string(),
envId: z.string(),
createdAt: z.string().or(z.date()),
updatedAt: z.string().or(z.date()),
changes: z.array(
z.object({
id: z.string(),
folderCommitId: z.string(),
changeType: z.string(),
isUpdate: z.boolean().optional(),
secretVersionId: z.string().optional().nullable(),
folderVersionId: z.string().optional().nullable(),
// Fix these two fields to accept either string or Date objects
createdAt: z.union([z.string(), z.date()]),
updatedAt: z.union([z.string(), z.date()]),
folderName: z.string().optional().nullable(),
folderChangeId: z.string().optional().nullable(),
folderVersion: z.union([z.string(), z.number()]).optional().nullable(),
secretKey: z.string().optional().nullable(),
secretVersion: z.union([z.string(), z.number()]).optional().nullable(),
secretId: z.string().optional().nullable(),
actorMetadata: z
.union([
z.object({
id: z.string().optional(),
name: z.string().optional()
}),
z.unknown()
])
.optional(),
actorType: z.string().optional(),
message: z.string().optional().nullable(),
folderId: z.string().optional().nullable(),
versions: z.array(versionSchema).optional()
})
)
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const changes = await server.services.folderCommit.getCommitChanges({
actor: req.permission?.type,
actorId: req.permission?.id,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
projectId: req.params.workspaceId,
commitId: req.params.commitId
});
for (const change of changes.changes) {
if (change.secretVersionId) {
const currentVersion = change.secretVersion || "1";
const previousVersion = (Number.parseInt(currentVersion, 10) - 1).toString();
if (change.secretId) {
// eslint-disable-next-line no-await-in-loop
const versions = await server.services.secret.getSecretVersionsV2ByIds({
actorId: req.permission?.id,
actor: req.permission?.type,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
secretId: change.secretId,
secretVersions: change.isUpdate ? [currentVersion, previousVersion] : [currentVersion],
folderId: change.folderId
});
change.versions = versions?.map((v) => ({
secretKey: v.secretKey,
secretComment: v.secretComment,
skipMultilineEncoding: v.skipMultilineEncoding,
secretReminderRepeatDays: v.secretReminderRepeatDays,
secretReminderNote: v.secretReminderNote,
metadata: v.secretMetadata,
tags: v.tags?.map((t) => t.name),
secretReminderRecipients: v.secretReminderRecipients?.map((r) => r.toString()),
secretValue: v.secretValue
}));
}
} else if (change.folderVersionId && change.folderChangeId) {
const currentVersion = change.folderVersion || "1";
const previousVersion = (Number.parseInt(currentVersion, 10) - 1).toString();
// eslint-disable-next-line no-await-in-loop
const versions = await server.services.folder.getFolderVersionsByIds({
folderId: change.folderChangeId,
folderVersions: change.isUpdate ? [currentVersion, previousVersion] : [currentVersion]
});
change.versions = versions.map((v) => ({
name: v.name
}));
}
}
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.GET_PROJECT_PIT_COMMIT_CHANGES,
metadata: {
commitId: req.params.commitId,
changesCount: (changes.changes?.length || 0).toString()
}
}
});
return {
changes: {
...changes,
commitId: changes.commitId.toString()
}
};
}
});
// Retrieve rollback changes for a commit
server.route({
method: "GET",
url: "/commits/:workspaceId/:commitId/compare",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim(),
commitId: z.string().trim()
}),
querystring: z.object({
folderId: z.string().trim(),
envId: z.string().trim(),
deepRollback: booleanSchema.default(false),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash)
}),
response: {
200: z.array(
z.object({
folderId: z.string(),
folderName: z.string(),
folderPath: z.string().optional(),
changes: z.any()
})
)
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const latestCommit = await server.services.folderCommit.getLatestCommit({
folderId: req.query.folderId,
actor: req.permission?.type,
actorId: req.permission?.id,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
projectId: req.params.workspaceId
});
if (!latestCommit) {
throw new NotFoundError({ message: "Latest commit not found" });
}
let diffs;
if (req.query.deepRollback) {
diffs = await server.services.folderCommit.deepCompareFolder({
targetCommitId: req.params.commitId,
envId: req.query.envId,
actorId: req.permission?.id,
actorType: req.permission?.type,
projectId: req.params.workspaceId
});
} else {
const folder = await server.services.folder.getFolderById({
actor: req.permission?.type,
actorId: req.permission?.id,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
id: req.query.folderId
});
diffs = [
{
folderId: folder.id,
folderName: folder.name,
folderPath: req.query.secretPath,
changes: await server.services.folderCommit.compareFolderStates({
targetCommitId: req.params.commitId,
currentCommitId: latestCommit.id
})
}
];
}
for (const diff of diffs) {
for (const change of diff.changes) {
if (change.secretKey) {
const currentVersion = change.secretVersion || "1";
const previousVersion = change.fromVersion || "1";
// eslint-disable-next-line no-await-in-loop
const versions = await server.services.secret.getSecretVersionsV2ByIds({
actorId: req.permission?.id,
actor: req.permission?.type,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
secretId: change.id,
// if it's update add also the previous secretversionid
secretVersions:
change.changeType === ChangeType.UPDATE ? [currentVersion, previousVersion] : [currentVersion],
folderId: req.query.folderId
});
change.versions = versions?.map((v) => ({
secretKey: v.secretKey,
secretComment: v.secretComment,
skipMultilineEncoding: v.skipMultilineEncoding,
secretReminderRepeatDays: v.secretReminderRepeatDays,
secretReminderNote: v.secretReminderNote,
metadata: v.metadata,
tags: v.tags?.map((t) => t.name),
secretReminderRecipients: v.secretReminderRecipients?.map((r) => r.toString()),
secretValue: v.secretValue
}));
}
if (change.folderVersion) {
const currentVersion = change.folderVersion || "1";
const previousVersion = change.fromVersion || "1";
// eslint-disable-next-line no-await-in-loop
const versions = await server.services.folder.getFolderVersionsByIds({
folderId: change.id,
folderVersions:
change.changeType === ChangeType.UPDATE ? [currentVersion, previousVersion] : [currentVersion]
});
change.versions = versions.map((v) => ({
name: v.name
}));
}
}
}
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.PIT_COMPARE_FOLDER_STATES,
metadata: {
targetCommitId: req.params.commitId,
folderId: req.query.folderId,
deepRollback: req.query.deepRollback,
diffsCount: diffs.length.toString()
}
}
});
return diffs;
}
});
// Rollback to a previous commit
server.route({
method: "POST",
url: "/commits/:workspaceId/:commitId/rollback",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim(),
commitId: z.string().trim()
}),
body: z.object({
folderId: z.string().trim(),
deepRollback: z.boolean().default(false),
message: z.string().trim().optional(),
envId: z.string().trim()
}),
response: {
200: z.object({
success: z.boolean(),
secretChangesCount: z.number().optional(),
folderChangesCount: z.number().optional(),
totalChanges: z.number().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const latestCommit = await server.services.folderCommit.getLatestCommit({
folderId: req.body.folderId,
actor: req.permission?.type,
actorId: req.permission?.id,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
projectId: req.params.workspaceId
});
if (!latestCommit) {
throw new NotFoundError({ message: "Latest commit not found" });
}
if (req.body.deepRollback) {
await server.services.folderCommit.deepRollbackFolder(
req.params.commitId,
req.body.envId,
req.permission.id,
req.permission.type,
req.params.workspaceId
);
return { success: true };
}
const diff = await server.services.folderCommit.compareFolderStates({
currentCommitId: latestCommit.id,
targetCommitId: req.params.commitId
});
const response = await server.services.folderCommit.applyFolderStateDifferences({
differences: diff,
actorInfo: {
actorType: req.permission.type,
actorId: req.permission.id,
message: req.body.message || "Rollback to previous commit"
},
folderId: req.body.folderId,
projectId: req.params.workspaceId,
reconstructNewFolders: req.body.deepRollback
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.PIT_ROLLBACK_COMMIT,
metadata: {
targetCommitId: req.params.commitId,
folderId: req.body.folderId,
deepRollback: req.body.deepRollback,
message: req.body.message || "Rollback to previous commit",
totalChanges: response.totalChanges.toString()
}
}
});
return {
success: true,
secretChangesCount: response.secretChangesCount,
folderChangesCount: response.folderChangesCount,
totalChanges: response.totalChanges
};
}
});
// Revert commit
server.route({
method: "POST",
url: "/commits/:workspaceId/:commitId/revert",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim(),
commitId: z.string().trim()
}),
response: {
200: z.object({
success: z.boolean(),
message: z.string(),
originalCommitId: z.string(),
revertCommitId: z.string().optional(),
changesReverted: z.number().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const response = await server.services.folderCommit.revertCommitChanges({
commitId: req.params.commitId,
actor: req.permission?.type,
actorId: req.permission?.id,
actorAuthMethod: req.permission?.authMethod,
actorOrgId: req.permission?.orgId,
projectId: req.params.workspaceId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.PIT_REVERT_COMMIT,
metadata: {
commitId: req.params.commitId,
revertCommitId: response.revertCommitId,
changesReverted: response.changesReverted?.toString()
}
}
});
return response;
}
});
// Folder state at commit
server.route({
method: "GET",
url: "/commits/:workspaceId/:commitId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim(),
commitId: z.string().trim()
}),
querystring: z.object({
folderId: z.string().trim()
}),
response: {
200: folderStateSchema
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const response = await server.services.folderCommit.reconstructFolderState(req.params.commitId);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.PIT_GET_FOLDER_STATE,
metadata: {
commitId: req.params.commitId,
folderId: req.query.folderId,
resourceCount: response.length.toString()
}
}
});
return response.map((item) => ({
...item,
secretVersion: item.secretVersion ? Number(item.secretVersion) : undefined,
folderVersion: item.folderVersion ? Number(item.folderVersion) : undefined
}));
}
});
};
@@ -343,7 +343,15 @@ export enum EventType {
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end",
GET_PROJECT_PIT_COMMITS = "get-project-pit-commits",
GET_PROJECT_PIT_COMMIT_CHANGES = "get-project-pit-commit-changes",
GET_PROJECT_PIT_COMMIT_COUNT = "get-project-pit-commit-count",
PIT_ROLLBACK_COMMIT = "pit-rollback-commit",
PIT_REVERT_COMMIT = "pit-revert-commit",
PIT_GET_FOLDER_STATE = "pit-get-folder-state",
PIT_COMPARE_FOLDER_STATES = "pit-compare-folder-states"
}
export const filterableSecretEvents: EventType[] = [
@@ -2694,6 +2702,71 @@ interface MicrosoftTeamsWorkflowIntegrationUpdateEvent {
};
}
interface GetProjectPitCommitsEvent {
type: EventType.GET_PROJECT_PIT_COMMITS;
metadata: {
commitCount: string;
environment: string;
path: string;
};
}
interface GetProjectPitCommitChangesEvent {
type: EventType.GET_PROJECT_PIT_COMMIT_CHANGES;
metadata: {
changesCount: string;
commitId: string;
};
}
interface GetProjectPitCommitCountEvent {
type: EventType.GET_PROJECT_PIT_COMMIT_COUNT;
metadata: {
environment: string;
path: string;
commitCount: string;
};
}
interface PitRollbackCommitEvent {
type: EventType.PIT_ROLLBACK_COMMIT;
metadata: {
targetCommitId: string;
folderId: string;
deepRollback: boolean;
message: string;
totalChanges: string;
};
}
interface PitRevertCommitEvent {
type: EventType.PIT_REVERT_COMMIT;
metadata: {
commitId: string;
revertCommitId?: string;
changesReverted?: string;
};
}
interface PitGetFolderStateEvent {
type: EventType.PIT_GET_FOLDER_STATE;
metadata: {
commitId: string;
folderId: string;
resourceCount: string;
};
}
interface PitCompareFolderStatesEvent {
type: EventType.PIT_COMPARE_FOLDER_STATES;
metadata: {
targetCommitId: string;
folderId: string;
deepRollback: boolean;
diffsCount: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@@ -2941,4 +3014,11 @@ export type Event =
| MicrosoftTeamsWorkflowIntegrationGetTeamsEvent
| MicrosoftTeamsWorkflowIntegrationGetEvent
| MicrosoftTeamsWorkflowIntegrationListEvent
| MicrosoftTeamsWorkflowIntegrationUpdateEvent;
| MicrosoftTeamsWorkflowIntegrationUpdateEvent
| GetProjectPitCommitsEvent
| GetProjectPitCommitChangesEvent
| PitRollbackCommitEvent
| GetProjectPitCommitCountEvent
| PitRevertCommitEvent
| PitCompareFolderStatesEvent
| PitGetFolderStateEvent;
@@ -17,6 +17,11 @@ export enum ProjectPermissionActions {
Delete = "delete"
}
export enum ProjectPermissionCommitsActions {
Read = "read",
PerformRollback = "perform-rollback"
}
export enum ProjectPermissionSecretActions {
DescribeAndReadValue = "read",
DescribeSecret = "describeSecret",
@@ -126,6 +131,7 @@ export enum ProjectPermissionSub {
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
SecretRotation = "secret-rotation",
Commits = "commits",
Identity = "identity",
CertificateAuthorities = "certificate-authorities",
Certificates = "certificates",
@@ -251,7 +257,8 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
| [ProjectPermissionCommitsActions, ProjectPermissionSub.Commits];
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
@@ -557,6 +564,12 @@ const GeneralPermissionSchema = [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.Commits).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCommitsActions).describe(
"Describe what action an entity can take."
)
})
];
@@ -829,6 +842,11 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.SecretRotation
);
can(
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
ProjectPermissionSub.Commits
);
return rules;
};
@@ -1011,6 +1029,11 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.SecretSyncs
);
can(
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
ProjectPermissionSub.Commits
);
return rules;
};
@@ -1046,6 +1069,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
return rules;
};
@@ -20,6 +20,7 @@ import { EnforcementLevel } from "@app/lib/types";
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
@@ -130,6 +131,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
};
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
@@ -161,7 +163,8 @@ export const secretApprovalRequestServiceFactory = ({
projectSlackConfigDAL,
resourceMetadataDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsService
microsoftTeamsService,
folderCommitService
}: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@@ -596,6 +599,10 @@ export const secretApprovalRequestServiceFactory = ({
? await fnSecretV2BridgeBulkInsert({
tx,
folderId,
actor: {
actorId,
type: actor
},
orgId: actorOrgId,
inputSecrets: secretCreationCommits.map((el) => ({
tagIds: el?.tags.map(({ id }) => id),
@@ -618,13 +625,18 @@ export const secretApprovalRequestServiceFactory = ({
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
folderCommitService
})
: [];
const updatedSecrets = secretUpdationCommits.length
? await fnSecretV2BridgeBulkUpdate({
folderId,
orgId: actorOrgId,
actor: {
actorId,
type: actor
},
tx,
inputSecrets: secretUpdationCommits.map((el) => {
const encryptedValue =
@@ -658,7 +670,8 @@ export const secretApprovalRequestServiceFactory = ({
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
resourceMetadataDAL
resourceMetadataDAL,
folderCommitService
})
: [];
const deletedSecret = secretDeletionCommits.length
@@ -666,10 +679,13 @@ export const secretApprovalRequestServiceFactory = ({
projectId,
folderId,
tx,
actorId: "",
actorId,
actorType: actor,
secretDAL: secretV2BridgeDAL,
secretQueueService,
inputSecrets: secretDeletionCommits.map(({ key }) => ({ secretKey: key, type: SecretType.Shared }))
inputSecrets: secretDeletionCommits.map(({ key }) => ({ secretKey: key, type: SecretType.Shared })),
folderCommitService,
secretVersionDAL: secretVersionV2BridgeDAL
})
: [];
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
@@ -10,6 +10,7 @@ import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueName, TQueueServiceFactory } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type";
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@@ -87,6 +88,7 @@ type TSecretReplicationServiceFactoryDep = {
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
};
export type TSecretReplicationServiceFactory = ReturnType<typeof secretReplicationServiceFactory>;
@@ -132,6 +134,7 @@ export const secretReplicationServiceFactory = ({
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
kmsService,
folderCommitService,
resourceMetadataDAL
}: TSecretReplicationServiceFactoryDep) => {
const $getReplicatedSecrets = (
@@ -446,6 +449,7 @@ export const secretReplicationServiceFactory = ({
tx,
secretTagDAL,
resourceMetadataDAL,
folderCommitService,
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
inputSecrets: locallyCreatedSecrets.map((doc) => {
return {
@@ -466,6 +470,7 @@ export const secretReplicationServiceFactory = ({
orgId,
folderId: destinationReplicationFolderId,
secretVersionDAL: secretVersionV2BridgeDAL,
folderCommitService,
secretDAL: secretV2BridgeDAL,
tx,
resourceMetadataDAL,
@@ -61,6 +61,7 @@ import { TAppConnectionDALFactory } from "@app/services/app-connection/app-conne
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { ActorType } from "@app/services/auth/auth-type";
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@@ -96,7 +97,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
TSecretV2BridgeDALFactory,
"bulkUpdate" | "insertMany" | "deleteMany" | "upsertSecretReferences" | "find" | "invalidateSecretCacheByProjectId"
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">;
@@ -104,6 +105,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
queueService: Pick<TQueueServiceFactory, "queuePg">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
};
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
@@ -141,6 +143,7 @@ export const secretRotationV2ServiceFactory = ({
snapshotService,
keyStore,
queueService,
folderCommitService,
appConnectionDAL
}: TSecretRotationV2ServiceFactoryDep) => {
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
@@ -533,7 +536,12 @@ export const secretRotationV2ServiceFactory = ({
secretVersionDAL: secretVersionV2BridgeDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
secretTagDAL,
resourceMetadataDAL
folderCommitService,
resourceMetadataDAL,
actor: {
type: actor.type,
actorId: actor.id
}
});
await secretRotationV2DAL.insertSecretMappings(
@@ -668,7 +676,12 @@ export const secretRotationV2ServiceFactory = ({
secretVersionDAL: secretVersionV2BridgeDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
secretTagDAL,
resourceMetadataDAL
folderCommitService,
resourceMetadataDAL,
actor: {
type: actor.type,
actorId: actor.id
}
});
secretsMappingUpdated = true;
@@ -786,6 +799,9 @@ export const secretRotationV2ServiceFactory = ({
projectId,
folderId,
actorId: actor.id, // not actually used since rotated secrets are shared
actorType: actor.type,
folderCommitService,
secretVersionDAL: secretVersionV2BridgeDAL,
tx
});
}
@@ -926,6 +942,10 @@ export const secretRotationV2ServiceFactory = ({
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
folderCommitService,
actor: {
type: ActorType.PLATFORM
},
secretTagDAL,
resourceMetadataDAL
});
@@ -14,6 +14,7 @@ import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type";
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@@ -53,6 +54,7 @@ type TSecretRotationQueueFactoryDep = {
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
};
// These error should stop the repeatable job and ask user to reconfigure rotation
@@ -77,6 +79,7 @@ export const secretRotationQueueFactory = ({
telemetryService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
folderCommitService,
kmsService
}: TSecretRotationQueueFactoryDep) => {
const addToQueue = async (rotationId: string, interval: number) => {
@@ -330,7 +333,7 @@ export const secretRotationQueueFactory = ({
})),
tx
);
await secretVersionV2BridgeDAL.insertMany(
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
...el,
actorType: ActorType.PLATFORM,
@@ -338,6 +341,22 @@ export const secretRotationQueueFactory = ({
})),
tx
);
await folderCommitService.createCommit(
{
actor: {
type: ActorType.PLATFORM
},
message: "Changed by Secret rotation",
folderId: secretVersions[0].folderId,
changes: secretVersions.map((sv) => ({
type: "add",
isUpdate: true,
secretVersionId: sv.id
}))
},
tx
);
});
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId);
+4
View File
@@ -228,6 +228,10 @@ const envSchema = z
DATADOG_SERVICE: zpStr(z.string().optional().default("infisical-core")),
DATADOG_HOSTNAME: zpStr(z.string().optional()),
// PIT
PIT_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("10")),
PIT_TREE_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("100")),
/* CORS ----------------------------------------------------------------------------- */
CORS_ALLOWED_ORIGINS: zpStr(
+10 -2
View File
@@ -49,7 +49,8 @@ export enum QueueName {
AccessTokenStatusUpdate = "access-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
AppConnectionSecretSync = "app-connection-secret-sync",
SecretRotationV2 = "secret-rotation-v2"
SecretRotationV2 = "secret-rotation-v2",
FolderTreeCheckpoint = "folder-tree-checkpoint"
}
export enum QueueJobs {
@@ -81,7 +82,8 @@ export enum QueueJobs {
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification"
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
CreateFolderTreeCheckpoint = "create-folder-tree-checkpoint"
}
export type TQueueJobTypes = {
@@ -191,6 +193,12 @@ export type TQueueJobTypes = {
name: QueueJobs.ProjectV3Migration;
payload: { projectId: string };
};
[QueueName.FolderTreeCheckpoint]: {
name: QueueJobs.CreateFolderTreeCheckpoint;
payload: {
envId: string;
};
};
[QueueName.ImportSecretsFromExternalSource]: {
name: QueueJobs.ImportSecretsFromExternalSource;
payload: {
+55 -5
View File
@@ -140,6 +140,14 @@ import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-gr
import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal";
import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal";
import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal";
import { folderCommitQueueServiceFactory } from "@app/services/folder-commit/folder-commit-queue";
import { folderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
import { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-dal";
import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal";
import { folderTreeCheckpointResourcesDALFactory } from "@app/services/folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
@@ -551,6 +559,37 @@ export const registerRoutes = async (
projectRoleDAL,
permissionService
});
const folderCommitChangesDAL = folderCommitChangesDALFactory(db);
const folderCheckpointDAL = folderCheckpointDALFactory(db);
const folderCheckpointResourcesDAL = folderCheckpointResourcesDALFactory(db);
const folderTreeCheckpointDAL = folderTreeCheckpointDALFactory(db);
const folderCommitDAL = folderCommitDALFactory(db);
const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db);
const folderCommitQueueService = folderCommitQueueServiceFactory({
queueService,
folderTreeCheckpointDAL,
folderTreeCheckpointResourcesDAL,
folderCommitDAL,
folderDAL
});
const folderCommitService = folderCommitServiceFactory({
folderCommitDAL,
folderCommitChangesDAL,
folderCheckpointDAL,
folderTreeCheckpointDAL,
userDAL,
identityDAL,
folderDAL,
folderVersionDAL,
secretVersionV2BridgeDAL,
projectDAL,
folderCheckpointResourcesDAL,
secretV2BridgeDAL,
folderTreeCheckpointResourcesDAL,
folderCommitQueueService,
permissionService
});
const scimService = scimServiceFactory({
licenseService,
scimDAL,
@@ -973,6 +1012,7 @@ export const registerRoutes = async (
projectMembershipDAL,
projectBotDAL,
secretDAL,
folderCommitService,
secretBlindIndexDAL,
secretVersionDAL,
secretTagDAL,
@@ -1019,6 +1059,7 @@ export const registerRoutes = async (
secretReminderRecipientsDAL,
orgService,
resourceMetadataDAL,
folderCommitService,
secretSyncQueue
});
@@ -1120,7 +1161,8 @@ export const registerRoutes = async (
folderVersionDAL,
projectEnvDAL,
snapshotService,
projectDAL
projectDAL,
folderCommitService
});
const secretImportService = secretImportServiceFactory({
@@ -1145,6 +1187,7 @@ export const registerRoutes = async (
const secretV2BridgeService = secretV2BridgeServiceFactory({
folderDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
folderCommitService,
secretQueueService,
secretDAL: secretV2BridgeDAL,
permissionService,
@@ -1188,7 +1231,8 @@ export const registerRoutes = async (
projectSlackConfigDAL,
resourceMetadataDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsService
microsoftTeamsService,
folderCommitService
});
const secretService = secretServiceFactory({
@@ -1273,7 +1317,8 @@ export const registerRoutes = async (
secretV2BridgeDAL,
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL,
secretVersionV2BridgeDAL,
resourceMetadataDAL
resourceMetadataDAL,
folderCommitService
});
const secretRotationQueue = secretRotationQueueFactory({
@@ -1285,6 +1330,7 @@ export const registerRoutes = async (
projectBotService,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
folderCommitService,
kmsService
});
@@ -1557,7 +1603,9 @@ export const registerRoutes = async (
secretDAL: secretV2BridgeDAL,
queueService,
secretV2BridgeService,
resourceMetadataDAL
resourceMetadataDAL,
folderCommitService,
folderVersionDAL
});
const migrationService = externalMigrationServiceFactory({
@@ -1619,6 +1667,7 @@ export const registerRoutes = async (
auditLogService,
secretV2BridgeDAL,
secretTagDAL,
folderCommitService,
secretVersionTagV2BridgeDAL,
secretVersionV2BridgeDAL,
keyStore,
@@ -1746,7 +1795,8 @@ export const registerRoutes = async (
secretRotationV2: secretRotationV2Service,
microsoftTeams: microsoftTeamsService,
assumePrivileges: assumePrivilegeService,
githubOrgSync: githubOrgSyncConfigService
githubOrgSync: githubOrgSyncConfigService,
folderCommit: folderCommitService
});
const cronJobs: CronJob[] = [];
@@ -10,6 +10,7 @@ import { chunkArray } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
@@ -18,6 +19,7 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { fnSecretBulkInsert, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
@@ -42,6 +44,8 @@ export type TImportDataIntoInfisicalDTO = {
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "create">;
input: TImportInfisicalDataCreate;
};
@@ -507,6 +511,8 @@ export const importDataIntoInfisicalFn = async ({
secretVersionTagDAL,
folderDAL,
resourceMetadataDAL,
folderVersionDAL,
folderCommitService,
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
}: TImportDataIntoInfisicalDTO) => {
// Import data to infisical
@@ -599,6 +605,36 @@ export const importDataIntoInfisicalFn = async ({
tx
);
const newFolderVersion = await folderVersionDAL.create(
{
name: newFolder.name,
envId: newFolder.envId,
version: newFolder.version,
folderId: newFolder.id
},
tx
);
await folderCommitService.createCommit(
{
actor: {
type: actor,
metadata: {
id: actorId
}
},
message: "Changed by external migration",
folderId: parentEnv.rootFolderId,
changes: [
{
type: "add",
folderVersionId: newFolderVersion.id
}
]
},
tx
);
originalToNewFolderId.set(folder.id, {
folderId: newFolder.id,
projectId: parentEnv.projectId
@@ -772,6 +808,7 @@ export const importDataIntoInfisicalFn = async ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderCommitService,
actor: {
type: actor,
actorId
@@ -3,6 +3,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectServiceFactory } from "../project/project-service";
@@ -10,6 +11,7 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
@@ -36,6 +38,8 @@ export type TExternalMigrationQueueFactoryDep = {
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
@@ -56,6 +60,8 @@ export const externalMigrationQueueFactory = ({
secretTagDAL,
secretVersionTagDAL,
folderDAL,
folderCommitService,
folderVersionDAL,
resourceMetadataDAL
}: TExternalMigrationQueueFactoryDep) => {
const startImport = async (dto: {
@@ -114,6 +120,8 @@ export const externalMigrationQueueFactory = ({
projectService,
projectEnvService,
secretV2BridgeService,
folderCommitService,
folderVersionDAL,
resourceMetadataDAL
});
@@ -0,0 +1,118 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import {
TableName,
TFolderCheckpointResources,
TFolderCheckpoints,
TSecretFolderVersions,
TSecretVersionsV2
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TFolderCheckpointResourcesDALFactory = ReturnType<typeof folderCheckpointResourcesDALFactory>;
export type ResourceWithCheckpointInfo = TFolderCheckpointResources & {
folderCommitId: string;
};
export const folderCheckpointResourcesDALFactory = (db: TDbClient) => {
const folderCheckpointResourcesOrm = ormify(db, TableName.FolderCheckpointResources);
const findByCheckpointId = async (
folderCheckpointId: string,
tx?: Knex
): Promise<
(TFolderCheckpointResources & {
referencedSecretId?: string;
referencedFolderId?: string;
folderName?: string;
folderVersion?: string;
secretKey?: string;
secretVersion?: string;
})[]
> => {
try {
const docs = await (tx || db.replicaNode())<TFolderCheckpointResources>(TableName.FolderCheckpointResources)
.where({ folderCheckpointId })
.leftJoin<TSecretVersionsV2>(
TableName.SecretVersionV2,
`${TableName.FolderCheckpointResources}.secretVersionId`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin<TSecretFolderVersions>(
TableName.SecretFolderVersion,
`${TableName.FolderCheckpointResources}.folderVersionId`,
`${TableName.SecretFolderVersion}.id`
)
.select(selectAllTableCols(TableName.FolderCheckpointResources))
.select(
db.ref("secretId").withSchema(TableName.SecretVersionV2).as("referencedSecretId"),
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("referencedFolderId"),
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderName"),
db.ref("version").withSchema(TableName.SecretFolderVersion).as("folderVersion"),
db.ref("key").withSchema(TableName.SecretVersionV2).as("secretKey"),
db.ref("version").withSchema(TableName.SecretVersionV2).as("secretVersion")
);
return docs.map((doc) => ({
...doc,
folderVersion: doc.folderVersion?.toString(),
secretVersion: doc.secretVersion?.toString()
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindByCheckpointId" });
}
};
const findBySecretVersionId = async (secretVersionId: string, tx?: Knex): Promise<ResourceWithCheckpointInfo[]> => {
try {
const docs = await (tx || db.replicaNode())<
TFolderCheckpointResources & Pick<TFolderCheckpoints, "folderCommitId" | "createdAt">
>(TableName.FolderCheckpointResources)
.where({ secretVersionId })
.select(selectAllTableCols(TableName.FolderCheckpointResources))
.join(
TableName.FolderCheckpoint,
`${TableName.FolderCheckpointResources}.folderCheckpointId`,
`${TableName.FolderCheckpoint}.id`
)
.select(
db.ref("folderCommitId").withSchema(TableName.FolderCheckpoint),
db.ref("createdAt").withSchema(TableName.FolderCheckpoint)
);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindBySecretVersionId" });
}
};
const findByFolderVersionId = async (folderVersionId: string, tx?: Knex): Promise<ResourceWithCheckpointInfo[]> => {
try {
const docs = await (tx || db.replicaNode())<
TFolderCheckpointResources & Pick<TFolderCheckpoints, "folderCommitId" | "createdAt">
>(TableName.FolderCheckpointResources)
.where({ folderVersionId })
.select(selectAllTableCols(TableName.FolderCheckpointResources))
.join(
TableName.FolderCheckpoint,
`${TableName.FolderCheckpointResources}.folderCheckpointId`,
`${TableName.FolderCheckpoint}.id`
)
.select(
db.ref("folderCommitId").withSchema(TableName.FolderCheckpoint),
db.ref("createdAt").withSchema(TableName.FolderCheckpoint)
);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindByFolderVersionId" });
}
};
return {
...folderCheckpointResourcesOrm,
findByCheckpointId,
findBySecretVersionId,
findByFolderVersionId
};
};
@@ -0,0 +1,138 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TFolderCheckpoints, TFolderCommits } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
export type TFolderCheckpointDALFactory = ReturnType<typeof folderCheckpointDALFactory>;
type CheckpointWithCommitInfo = TFolderCheckpoints & {
actorMetadata: unknown;
actorType: string;
message?: string | null;
commitDate: Date;
folderId: string;
};
export const folderCheckpointDALFactory = (db: TDbClient) => {
const folderCheckpointOrm = ormify(db, TableName.FolderCheckpoint);
const findByCommitId = async (folderCommitId: string, tx?: Knex): Promise<TFolderCheckpoints | undefined> => {
try {
const doc = await (tx || db.replicaNode())<TFolderCheckpoints>(TableName.FolderCheckpoint)
.where({ folderCommitId })
.select(selectAllTableCols(TableName.FolderCheckpoint))
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindByCommitId" });
}
};
const findByFolderId = async (folderId: string, limit?: number, tx?: Knex): Promise<CheckpointWithCommitInfo[]> => {
try {
let query = (tx || db.replicaNode())(TableName.FolderCheckpoint)
.join<TFolderCommits>(
TableName.FolderCommit,
`${TableName.FolderCheckpoint}.folderCommitId`,
`${TableName.FolderCommit}.id`
)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ folderId }, TableName.FolderCommit))
.select(selectAllTableCols(TableName.FolderCheckpoint))
.select(
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
db.ref("actorType").withSchema(TableName.FolderCommit),
db.ref("message").withSchema(TableName.FolderCommit),
db.ref("createdAt").withSchema(TableName.FolderCommit).as("commitDate"),
db.ref("folderId").withSchema(TableName.FolderCommit)
)
.orderBy(`${TableName.FolderCheckpoint}.createdAt`, "desc");
if (limit !== undefined) {
query = query.limit(limit);
}
return await query;
} catch (error) {
throw new DatabaseError({ error, name: "FindByFolderId" });
}
};
const findLatestByFolderId = async (folderId: string, tx?: Knex): Promise<CheckpointWithCommitInfo | undefined> => {
try {
const doc = await (tx || db.replicaNode())(TableName.FolderCheckpoint)
.join<TFolderCommits>(
TableName.FolderCommit,
`${TableName.FolderCheckpoint}.folderCommitId`,
`${TableName.FolderCommit}.id`
)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ folderId }, TableName.FolderCommit))
.select(selectAllTableCols(TableName.FolderCheckpoint))
.select(
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
db.ref("actorType").withSchema(TableName.FolderCommit),
db.ref("message").withSchema(TableName.FolderCommit),
db.ref("createdAt").withSchema(TableName.FolderCommit).as("commitDate"),
db.ref("folderId").withSchema(TableName.FolderCommit)
)
.orderBy(`${TableName.FolderCheckpoint}.createdAt`, "desc")
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestByFolderId" });
}
};
const findNearestCheckpoint = async (
folderCommitId: string,
tx?: Knex
): Promise<(CheckpointWithCommitInfo & { commitId: number }) | undefined> => {
try {
// First, get the commit info to find the folder ID and commit sequence number
const commit = await (tx || db.replicaNode())(TableName.FolderCommit)
.where({ id: folderCommitId })
.select("commitId", "folderId")
.first();
if (!commit) {
return undefined;
}
// Get the checkpoint with the highest commitId that's still less than or equal to our commit
const nearestCheckpoint = await (tx || db.replicaNode())(TableName.FolderCheckpoint)
.join<TFolderCommits>(
TableName.FolderCommit,
`${TableName.FolderCheckpoint}.folderCommitId`,
`${TableName.FolderCommit}.id`
)
.where(`${TableName.FolderCommit}.folderId`, commit.folderId)
.where(`${TableName.FolderCommit}.commitId`, "<=", commit.commitId.toString())
.select(selectAllTableCols(TableName.FolderCheckpoint))
.select(
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
db.ref("actorType").withSchema(TableName.FolderCommit),
db.ref("message").withSchema(TableName.FolderCommit),
db.ref("commitId").withSchema(TableName.FolderCommit),
db.ref("createdAt").withSchema(TableName.FolderCommit).as("commitDate"),
db.ref("folderId").withSchema(TableName.FolderCommit)
)
.orderBy(`${TableName.FolderCommit}.commitId`, "desc")
.first();
return nearestCheckpoint;
} catch (error) {
throw new DatabaseError({ error, name: "FindNearestCheckpoint" });
}
};
return {
...folderCheckpointOrm,
findByCommitId,
findByFolderId,
findLatestByFolderId,
findNearestCheckpoint
};
};
@@ -0,0 +1,137 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import {
TableName,
TFolderCommitChanges,
TFolderCommits,
TSecretFolderVersions,
TSecretVersionsV2
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TFolderCommitChangesDALFactory = ReturnType<typeof folderCommitChangesDALFactory>;
type CommitChangeWithCommitInfo = TFolderCommitChanges & {
actorMetadata: unknown;
actorType: string;
message?: string | null;
folderId: string;
folderName?: string;
folderVersion?: string;
secretKey?: string;
secretVersion?: string;
secretId?: string;
folderChangeId?: string;
versions?: {
secretKey?: string;
secretComment?: string;
skipMultilineEncoding?: boolean | null;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
metadata?: unknown;
tags?: string[] | null;
secretReminderRecipients?: string[] | null;
secretValue?: string;
name?: string;
}[];
};
export const folderCommitChangesDALFactory = (db: TDbClient) => {
const folderCommitChangesOrm = ormify(db, TableName.FolderCommitChanges);
const findByCommitId = async (folderCommitId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
try {
const docs = await (tx || db.replicaNode())<TFolderCommitChanges>(TableName.FolderCommitChanges)
.where({ folderCommitId })
.leftJoin<TFolderCommits>(
TableName.FolderCommit,
`${TableName.FolderCommitChanges}.folderCommitId`,
`${TableName.FolderCommit}.id`
)
.leftJoin<TSecretVersionsV2>(
TableName.SecretVersionV2,
`${TableName.FolderCommitChanges}.secretVersionId`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin<TSecretFolderVersions>(
TableName.SecretFolderVersion,
`${TableName.FolderCommitChanges}.folderVersionId`,
`${TableName.SecretFolderVersion}.id`
)
.select(selectAllTableCols(TableName.FolderCommitChanges))
.select(
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderName"),
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderChangeId"),
db.ref("version").withSchema(TableName.SecretFolderVersion).as("folderVersion"),
db.ref("key").withSchema(TableName.SecretVersionV2).as("secretKey"),
db.ref("version").withSchema(TableName.SecretVersionV2).as("secretVersion"),
db.ref("secretId").withSchema(TableName.SecretVersionV2),
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
db.ref("actorType").withSchema(TableName.FolderCommit),
db.ref("message").withSchema(TableName.FolderCommit),
db.ref("createdAt").withSchema(TableName.FolderCommit),
db.ref("folderId").withSchema(TableName.FolderCommit)
);
return docs.map((doc) => ({
...doc,
folderVersion: doc.folderVersion?.toString(),
secretVersion: doc.secretVersion?.toString()
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindByCommitId" });
}
};
const findBySecretVersionId = async (secretVersionId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
try {
const docs = await (tx || db.replicaNode())<
TFolderCommitChanges &
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
>(TableName.FolderCommitChanges)
.where({ secretVersionId })
.select(selectAllTableCols(TableName.FolderCommitChanges))
.join(TableName.FolderCommit, `${TableName.FolderCommitChanges}.folderCommitId`, `${TableName.FolderCommit}.id`)
.select(
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
db.ref("actorType").withSchema(TableName.FolderCommit),
db.ref("message").withSchema(TableName.FolderCommit),
db.ref("createdAt").withSchema(TableName.FolderCommit),
db.ref("folderId").withSchema(TableName.FolderCommit)
);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindBySecretVersionId" });
}
};
const findByFolderVersionId = async (folderVersionId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
try {
const docs = await (tx || db.replicaNode())<
TFolderCommitChanges &
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
>(TableName.FolderCommitChanges)
.where({ folderVersionId })
.select(selectAllTableCols(TableName.FolderCommitChanges))
.join(TableName.FolderCommit, `${TableName.FolderCommitChanges}.folderCommitId`, `${TableName.FolderCommit}.id`)
.select(
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
db.ref("actorType").withSchema(TableName.FolderCommit),
db.ref("message").withSchema(TableName.FolderCommit),
db.ref("createdAt").withSchema(TableName.FolderCommit),
db.ref("folderId").withSchema(TableName.FolderCommit)
);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindByFolderVersionId" });
}
};
return {
...folderCommitChangesOrm,
findByCommitId,
findBySecretVersionId,
findByFolderVersionId
};
};
@@ -0,0 +1,361 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import {
TableName,
TFolderCommitChanges,
TFolderCommits,
TSecretFolderVersions,
TSecretVersionsV2
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
export type TFolderCommitDALFactory = ReturnType<typeof folderCommitDALFactory>;
export const folderCommitDALFactory = (db: TDbClient) => {
const folderCommitOrm = ormify(db, TableName.FolderCommit);
const { delete: deleteOp, deleteById, ...restOfOrm } = folderCommitOrm;
const findByFolderId = async (folderId: string, tx?: Knex): Promise<TFolderCommits[]> => {
try {
const trx = tx || db.replicaNode();
// First, get all folder commits
const folderCommits = await trx(TableName.FolderCommit)
.where({ folderId })
.select("*")
.orderBy("createdAt", "desc");
if (folderCommits.length === 0) return [];
// Get all commit IDs
const commitIds = folderCommits.map((commit) => commit.id);
// Then get all related changes
const changes = await trx(TableName.FolderCommitChanges).whereIn("folderCommitId", commitIds).select("*");
const changesMap = changes.reduce(
(acc, change) => {
const { folderCommitId } = change;
if (!acc[folderCommitId]) acc[folderCommitId] = [];
acc[folderCommitId].push(change);
return acc;
},
{} as Record<string, TFolderCommitChanges[]>
);
return folderCommits.map((commit) => ({
...commit,
changes: changesMap[commit.id] || []
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindByFolderId" });
}
};
const findLatestCommit = async (folderId: string, tx?: Knex): Promise<TFolderCommits | undefined> => {
try {
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
.where({ folderId })
.select(selectAllTableCols(TableName.FolderCommit))
.orderBy("commitId", "desc")
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestCommit" });
}
};
const findLatestCommitByFolderIds = async (folderIds: string[], tx?: Knex): Promise<TFolderCommits[] | undefined> => {
try {
// First get max commitId for each folderId
const maxCommitIdSubquery = (tx || db.replicaNode())(TableName.FolderCommit)
.select("folderId")
.max("commitId as maxCommitId")
.whereIn("folderId", folderIds)
.groupBy("folderId");
// Join with main table to get complete records for each max commitId
const docs = await (tx || db.replicaNode())(TableName.FolderCommit)
.select(selectAllTableCols(TableName.FolderCommit))
// eslint-disable-next-line func-names
.join<TFolderCommits>(maxCommitIdSubquery.as("latest"), function () {
this.on(`${TableName.FolderCommit}.folderId`, "=", "latest.folderId").andOn(
`${TableName.FolderCommit}.commitId`,
"=",
"latest.maxCommitId"
);
});
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestCommitByFolderIds" });
}
};
const findLatestEnvCommit = async (envId: string, tx?: Knex): Promise<TFolderCommits | undefined> => {
try {
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
.where(`${TableName.FolderCommit}.envId`, "=", envId)
.select(selectAllTableCols(TableName.FolderCommit))
.orderBy("commitId", "desc")
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestCommit" });
}
};
const findMultipleLatestCommits = async (folderIds: string[], tx?: Knex): Promise<TFolderCommits[]> => {
try {
const knexInstance = tx || db.replicaNode();
// Get the latest commitId for each folderId
const subquery = knexInstance(TableName.FolderCommit)
.whereIn("folderId", folderIds)
.groupBy("folderId")
.select("folderId")
.max("commitId as maxCommitId");
// Then fetch the complete rows matching those latest commits
const docs = await knexInstance(TableName.FolderCommit)
// eslint-disable-next-line func-names
.innerJoin<TFolderCommits>(subquery.as("latest"), function () {
this.on(`${TableName.FolderCommit}.folderId`, "=", "latest.folderId").andOn(
`${TableName.FolderCommit}.commitId`,
"=",
"latest.maxCommitId"
);
})
.select(selectAllTableCols(TableName.FolderCommit));
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindMultipleLatestCommits" });
}
};
const getNumberOfCommitsSince = async (folderId: string, folderCommitId: string, tx?: Knex): Promise<number> => {
try {
const referencedCommit = await (tx || db.replicaNode())(TableName.FolderCommit)
.where({ id: folderCommitId })
.select("commitId")
.first();
if (referencedCommit?.commitId) {
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
.where({ folderId })
.where("commitId", ">", referencedCommit.commitId)
.count();
return Number(doc?.[0].count);
}
return 0;
} catch (error) {
throw new DatabaseError({ error, name: "getNumberOfCommitsSince" });
}
};
const getEnvNumberOfCommitsSince = async (envId: string, folderCommitId: string, tx?: Knex): Promise<number> => {
try {
const referencedCommit = await (tx || db.replicaNode())(TableName.FolderCommit)
.where({ id: folderCommitId })
.select("commitId")
.first();
if (referencedCommit?.commitId) {
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
.where(`${TableName.FolderCommit}.envId`, "=", envId)
.where("commitId", ">", referencedCommit.commitId)
.count();
return Number(doc?.[0].count);
}
return 0;
} catch (error) {
throw new DatabaseError({ error, name: "getNumberOfCommitsSince" });
}
};
const findCommitsToRecreate = async (
folderId: string,
targetCommitNumber: number,
checkpointCommitNumber: number,
tx?: Knex
): Promise<
(TFolderCommits & {
changes: (TFolderCommitChanges & {
referencedSecretId?: string;
referencedFolderId?: string;
folderName?: string;
folderVersion?: string;
secretKey?: string;
secretVersion?: string;
})[];
})[]
> => {
try {
// First get all the commits in the range
const commits = await (tx || db.replicaNode())(TableName.FolderCommit)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ folderId }, TableName.FolderCommit))
.andWhere(`${TableName.FolderCommit}.commitId`, ">", checkpointCommitNumber)
.andWhere(`${TableName.FolderCommit}.commitId`, "<=", targetCommitNumber)
.select(selectAllTableCols(TableName.FolderCommit))
.orderBy(`${TableName.FolderCommit}.commitId`, "asc");
// If no commits found, return empty array
if (!commits.length) {
return [];
}
// Get all the commit IDs
const commitIds = commits.map((commit) => commit.id);
// Get all changes for these commits in a single query
const allChanges = await (tx || db.replicaNode())(TableName.FolderCommitChanges)
.whereIn("folderCommitId", commitIds)
.leftJoin<TSecretVersionsV2>(
TableName.SecretVersionV2,
`${TableName.FolderCommitChanges}.secretVersionId`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin<TSecretFolderVersions>(
TableName.SecretFolderVersion,
`${TableName.FolderCommitChanges}.folderVersionId`,
`${TableName.SecretFolderVersion}.id`
)
.select(selectAllTableCols(TableName.FolderCommitChanges))
.select(
db.ref("secretId").withSchema(TableName.SecretVersionV2).as("referencedSecretId"),
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("referencedFolderId"),
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderName"),
db.ref("version").withSchema(TableName.SecretFolderVersion).as("folderVersion"),
db.ref("key").withSchema(TableName.SecretVersionV2).as("secretKey"),
db.ref("version").withSchema(TableName.SecretVersionV2).as("secretVersion")
);
// Organize changes by commit ID
const changesByCommitId = allChanges.reduce(
(acc, change) => {
if (!acc[change.folderCommitId]) {
acc[change.folderCommitId] = [];
}
acc[change.folderCommitId].push(change);
return acc;
},
{} as Record<string, TFolderCommitChanges[]>
);
// Attach changes to each commit
return commits.map((commit) => ({
...commit,
changes: changesByCommitId[commit.id] || []
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindCommitsToRecreate" });
}
};
const findLatestCommitBetween = async ({
folderId,
startCommitId,
endCommitId,
tx
}: {
folderId: string;
startCommitId?: string;
endCommitId: string;
tx?: Knex;
}): Promise<TFolderCommits | undefined> => {
try {
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
.where("commitId", "<=", endCommitId)
.where({ folderId })
.where((qb) => {
if (startCommitId) {
void qb.where("commitId", ">=", startCommitId);
}
})
.select(selectAllTableCols(TableName.FolderCommit))
.orderBy("commitId", "desc")
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestCommitBetween" });
}
};
const findAllCommitsBetween = async ({
envId,
startCommitId,
endCommitId,
tx
}: {
folderId?: string;
envId?: string;
startCommitId?: string;
endCommitId: string;
tx?: Knex;
}): Promise<TFolderCommits[]> => {
try {
const docs = await (tx || db.replicaNode())(TableName.FolderCommit)
.where("commitId", "<=", endCommitId)
.where((qb) => {
if (envId) {
void qb.where(`${TableName.FolderCommit}.envId`, "=", envId);
}
if (startCommitId) {
void qb.where("commitId", ">=", startCommitId);
}
})
.select(selectAllTableCols(TableName.FolderCommit))
.orderBy("commitId", "desc");
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestCommitBetween" });
}
};
const findAllFolderCommitsAfter = async ({
envId,
startCommitId,
tx
}: {
folderId?: string;
envId?: string;
startCommitId?: string;
tx?: Knex;
}): Promise<TFolderCommits[]> => {
try {
const docs = await (tx || db.replicaNode())(TableName.FolderCommit)
.where((qb) => {
if (envId) {
void qb.where(`${TableName.FolderCommit}.envId`, "=", envId);
}
if (startCommitId) {
void qb.where("commitId", ">=", startCommitId);
}
})
.select(selectAllTableCols(TableName.FolderCommit))
.orderBy("commitId", "desc");
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestCommitBetween" });
}
};
return {
...restOfOrm,
findByFolderId,
findLatestCommit,
getNumberOfCommitsSince,
findCommitsToRecreate,
findMultipleLatestCommits,
findAllCommitsBetween,
findLatestCommitBetween,
findLatestEnvCommit,
getEnvNumberOfCommitsSince,
findLatestCommitByFolderIds,
findAllFolderCommitsAfter
};
};
@@ -0,0 +1,180 @@
import { TSecretFolders } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TFolderTreeCheckpointDALFactory } from "../folder-tree-checkpoint/folder-tree-checkpoint-dal";
import { TFolderTreeCheckpointResourcesDALFactory } from "../folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TFolderCommitDALFactory } from "./folder-commit-dal";
type TFolderCommitQueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
folderTreeCheckpointDAL: Pick<
TFolderTreeCheckpointDALFactory,
"create" | "findLatestByEnvId" | "findNearestCheckpoint"
>;
folderTreeCheckpointResourcesDAL: Pick<
TFolderTreeCheckpointResourcesDALFactory,
"insertMany" | "findByTreeCheckpointId"
>;
folderCommitDAL: Pick<
TFolderCommitDALFactory,
"findLatestEnvCommit" | "getEnvNumberOfCommitsSince" | "findMultipleLatestCommits"
>;
folderDAL: Pick<TSecretFolderDALFactory, "findByEnvId">;
};
export type TFolderCommitQueueServiceFactory = ReturnType<typeof folderCommitQueueServiceFactory>;
export const folderCommitQueueServiceFactory = ({
queueService,
folderTreeCheckpointDAL,
folderTreeCheckpointResourcesDAL,
folderCommitDAL,
folderDAL
}: TFolderCommitQueueServiceFactoryDep) => {
const appCfg = getConfig();
const scheduleTreeCheckpoint = async (envId: string) => {
await queueService.queue(
QueueName.FolderTreeCheckpoint,
QueueJobs.CreateFolderTreeCheckpoint,
{ envId },
{
jobId: envId,
backoff: {
type: "exponential",
delay: 3000
},
removeOnFail: {
count: 3
},
removeOnComplete: true
}
);
};
const schedulePeriodicTreeCheckpoint = async (envId: string, intervalMs: number) => {
await queueService.queue(
QueueName.FolderTreeCheckpoint,
QueueJobs.CreateFolderTreeCheckpoint,
{ envId },
{
jobId: `periodic-${envId}`,
repeat: {
every: intervalMs
},
backoff: {
type: "exponential",
delay: 3000
},
removeOnFail: false,
removeOnComplete: false
}
);
};
const cancelScheduledTreeCheckpoint = async (envId: string) => {
await queueService.stopJobById(QueueName.FolderTreeCheckpoint, envId);
await queueService.stopRepeatableJobByJobId(QueueName.FolderTreeCheckpoint, `periodic-${envId}`);
};
// Sort folders by hierarchy (copied from the source code)
const sortFoldersByHierarchy = (folders: TSecretFolders[]) => {
const childrenMap = new Map<string, TSecretFolders[]>();
const allFolderIds = new Set<string>();
folders.forEach((folder) => {
if (folder.id) allFolderIds.add(folder.id);
});
folders.forEach((folder) => {
if (folder.parentId) {
const children = childrenMap.get(folder.parentId) || [];
children.push(folder);
childrenMap.set(folder.parentId, children);
}
});
const rootFolders = folders.filter((folder) => !folder.parentId || !allFolderIds.has(folder.parentId));
const result = [];
let currentLevel = rootFolders;
while (currentLevel.length > 0) {
result.push(...currentLevel);
const nextLevel = [];
for (const folder of currentLevel) {
if (folder.id) {
const children = childrenMap.get(folder.id) || [];
nextLevel.push(...children);
}
}
currentLevel = nextLevel;
}
return result;
};
queueService.start(QueueName.FolderTreeCheckpoint, async (job) => {
try {
if (job.name === QueueJobs.CreateFolderTreeCheckpoint) {
const { envId } = job.data as { envId: string };
logger.info("Folder tree checkpoint creation started:", envId, job.id);
const latestTreeCheckpoint = await folderTreeCheckpointDAL.findLatestByEnvId(envId);
const latestCommit = await folderCommitDAL.findLatestEnvCommit(envId);
if (!latestCommit) {
logger.info(`Latest commit ID not found for envId ${envId}`);
return;
}
const latestCommitId = latestCommit.id;
if (latestTreeCheckpoint) {
const commitsSinceLastCheckpoint = await folderCommitDAL.getEnvNumberOfCommitsSince(
envId,
latestTreeCheckpoint.folderCommitId
);
if (commitsSinceLastCheckpoint < Number(appCfg.PIT_TREE_CHECKPOINT_WINDOW)) {
logger.info(
`Commits since last checkpoint ${commitsSinceLastCheckpoint} is less than ${appCfg.PIT_TREE_CHECKPOINT_WINDOW}`
);
return;
}
}
const folders = await folderDAL.findByEnvId(envId);
const sortedFolders = sortFoldersByHierarchy(folders);
const filteredFoldersIds = sortedFolders.filter((folder) => !folder.isReserved).map((folder) => folder.id);
const folderCommits = await folderCommitDAL.findMultipleLatestCommits(filteredFoldersIds);
const folderTreeCheckpoint = await folderTreeCheckpointDAL.create({
folderCommitId: latestCommitId
});
await folderTreeCheckpointResourcesDAL.insertMany(
folderCommits.map((folderCommit) => ({
folderTreeCheckpointId: folderTreeCheckpoint.id,
folderId: folderCommit.folderId,
folderCommitId: folderCommit.id
}))
);
logger.info("Folder tree checkpoint created successfully:", folderTreeCheckpoint.id);
}
} catch (error) {
logger.error(error, "Error creating folder tree checkpoint:");
throw error;
}
});
return {
scheduleTreeCheckpoint,
schedulePeriodicTreeCheckpoint,
cancelScheduledTreeCheckpoint
};
};
@@ -0,0 +1,608 @@
import { Knex } from "knex";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ActorType } from "../auth/auth-type";
import { ChangeType, folderCommitServiceFactory, TFolderCommitServiceFactory } from "./folder-commit-service";
// Mock config
vi.mock("@app/lib/config/env", () => ({
getConfig: () => ({
PIT_CHECKPOINT_WINDOW: 5,
PIT_TREE_CHECKPOINT_WINDOW: 10
})
}));
// Mock logger
vi.mock("@app/lib/logger", () => ({
logger: {
info: vi.fn(),
error: vi.fn()
}
}));
describe("folderCommitServiceFactory", () => {
// Properly type the mock functions
type TransactionCallback<T> = (trx: Knex) => Promise<T>;
// Mock dependencies
const mockFolderCommitDAL = {
create: vi.fn().mockResolvedValue({}),
findById: vi.fn().mockResolvedValue({}),
findByFolderId: vi.fn().mockResolvedValue([]),
findLatestCommit: vi.fn().mockResolvedValue({}),
transaction: vi.fn().mockImplementation(<T>(callback: TransactionCallback<T>) => callback({} as Knex)),
getNumberOfCommitsSince: vi.fn().mockResolvedValue(0),
getEnvNumberOfCommitsSince: vi.fn().mockResolvedValue(0),
findCommitsToRecreate: vi.fn().mockResolvedValue([]),
findMultipleLatestCommits: vi.fn().mockResolvedValue([]),
findLatestCommitBetween: vi.fn().mockResolvedValue({}),
findAllCommitsBetween: vi.fn().mockResolvedValue([]),
findLatestEnvCommit: vi.fn().mockResolvedValue({}),
findLatestCommitByFolderIds: vi.fn().mockResolvedValue({})
};
const mockFolderCommitChangesDAL = {
create: vi.fn().mockResolvedValue({}),
findByCommitId: vi.fn().mockResolvedValue([]),
insertMany: vi.fn().mockResolvedValue([])
};
const mockFolderCheckpointDAL = {
create: vi.fn().mockResolvedValue({}),
findByFolderId: vi.fn().mockResolvedValue([]),
findLatestByFolderId: vi.fn().mockResolvedValue(null),
findNearestCheckpoint: vi.fn().mockResolvedValue({})
};
const mockFolderCheckpointResourcesDAL = {
insertMany: vi.fn().mockResolvedValue([]),
findByCheckpointId: vi.fn().mockResolvedValue([])
};
const mockFolderTreeCheckpointDAL = {
create: vi.fn().mockResolvedValue({}),
findByProjectId: vi.fn().mockResolvedValue([]),
findLatestByProjectId: vi.fn().mockResolvedValue({}),
findNearestCheckpoint: vi.fn().mockResolvedValue({}),
findLatestByEnvId: vi.fn().mockResolvedValue({})
};
const mockFolderTreeCheckpointResourcesDAL = {
insertMany: vi.fn().mockResolvedValue([]),
findByTreeCheckpointId: vi.fn().mockResolvedValue([])
};
const mockUserDAL = {
findById: vi.fn().mockResolvedValue({})
};
const mockIdentityDAL = {
findById: vi.fn().mockResolvedValue({})
};
const mockFolderDAL = {
findByParentId: vi.fn().mockResolvedValue([]),
findByProjectId: vi.fn().mockResolvedValue([]),
deleteById: vi.fn().mockResolvedValue({}),
create: vi.fn().mockResolvedValue({}),
updateById: vi.fn().mockResolvedValue({}),
update: vi.fn().mockResolvedValue({}),
find: vi.fn().mockResolvedValue([]),
findById: vi.fn().mockResolvedValue({}),
findByEnvId: vi.fn().mockResolvedValue([]),
findFoldersByRootAndIds: vi.fn().mockResolvedValue([])
};
const mockFolderVersionDAL = {
findLatestFolderVersions: vi.fn().mockResolvedValue({}),
findById: vi.fn().mockResolvedValue({}),
deleteById: vi.fn().mockResolvedValue({}),
create: vi.fn().mockResolvedValue({}),
updateById: vi.fn().mockResolvedValue({}),
find: vi.fn().mockResolvedValue([]),
findByIdsWithLatestVersion: vi.fn().mockResolvedValue({})
};
const mockSecretVersionV2BridgeDAL = {
findLatestVersionByFolderId: vi.fn().mockResolvedValue([]),
findById: vi.fn().mockResolvedValue({}),
deleteById: vi.fn().mockResolvedValue({}),
create: vi.fn().mockResolvedValue({}),
updateById: vi.fn().mockResolvedValue({}),
find: vi.fn().mockResolvedValue([]),
findByIdsWithLatestVersion: vi.fn().mockResolvedValue({}),
findLatestVersionMany: vi.fn().mockResolvedValue({})
};
const mockSecretV2BridgeDAL = {
deleteById: vi.fn().mockResolvedValue({}),
create: vi.fn().mockResolvedValue({}),
updateById: vi.fn().mockResolvedValue({}),
update: vi.fn().mockResolvedValue({}),
insertMany: vi.fn().mockResolvedValue([]),
invalidateSecretCacheByProjectId: vi.fn().mockResolvedValue({})
};
const mockProjectDAL = {
findById: vi.fn().mockResolvedValue({})
};
const mockFolderCommitQueueService = {
scheduleTreeCheckpoint: vi.fn().mockResolvedValue({})
};
const mockPermissionService = {
getProjectPermission: vi.fn().mockResolvedValue({})
};
let folderCommitService: TFolderCommitServiceFactory;
beforeEach(() => {
vi.clearAllMocks();
folderCommitService = folderCommitServiceFactory({
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
folderCommitDAL: mockFolderCommitDAL,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
folderCommitChangesDAL: mockFolderCommitChangesDAL,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
folderCheckpointDAL: mockFolderCheckpointDAL,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
folderCheckpointResourcesDAL: mockFolderCheckpointResourcesDAL,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
folderTreeCheckpointDAL: mockFolderTreeCheckpointDAL,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
folderTreeCheckpointResourcesDAL: mockFolderTreeCheckpointResourcesDAL,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
userDAL: mockUserDAL,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
identityDAL: mockIdentityDAL,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
folderDAL: mockFolderDAL,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
folderVersionDAL: mockFolderVersionDAL,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
secretVersionV2BridgeDAL: mockSecretVersionV2BridgeDAL,
projectDAL: mockProjectDAL,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
secretV2BridgeDAL: mockSecretV2BridgeDAL,
folderCommitQueueService: mockFolderCommitQueueService,
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
permissionService: mockPermissionService
});
});
afterEach(() => {
vi.resetAllMocks();
});
describe("createCommit", () => {
it("should successfully create a commit with user actor", async () => {
// Arrange
const userData = { id: "user-id", username: "testuser" };
const folderData = { id: "folder-id", envId: "env-id" };
const commitData = { id: "commit-id", folderId: "folder-id" };
mockUserDAL.findById.mockResolvedValue(userData);
mockFolderDAL.findById.mockResolvedValue(folderData);
mockFolderCommitDAL.create.mockResolvedValue(commitData);
mockFolderCheckpointDAL.findLatestByFolderId.mockResolvedValue(null);
mockFolderCommitDAL.findLatestCommit.mockResolvedValue({ id: "latest-commit-id" });
mockFolderDAL.findByParentId.mockResolvedValue([]);
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]);
const data = {
actor: {
type: ActorType.USER,
metadata: { id: userData.id }
},
message: "Test commit",
folderId: folderData.id,
changes: [
{
type: "add",
secretVersionId: "secret-version-1"
}
]
};
// Act
const result = await folderCommitService.createCommit(data);
// Assert
expect(mockUserDAL.findById).toHaveBeenCalledWith(userData.id, undefined);
expect(mockFolderDAL.findById).toHaveBeenCalledWith(folderData.id, undefined);
expect(mockFolderCommitDAL.create).toHaveBeenCalledWith(
expect.objectContaining({
actorType: ActorType.USER,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
actorMetadata: expect.objectContaining({ name: userData.username }),
message: data.message,
folderId: data.folderId,
envId: folderData.envId
}),
undefined
);
expect(mockFolderCommitChangesDAL.insertMany).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
folderCommitId: commitData.id,
changeType: data.changes[0].type,
secretVersionId: data.changes[0].secretVersionId
})
]),
undefined
);
expect(mockFolderCommitQueueService.scheduleTreeCheckpoint).toHaveBeenCalledWith(folderData.envId);
expect(result).toEqual(commitData);
});
it("should successfully create a commit with identity actor", async () => {
// Arrange
const identityData = { id: "identity-id", name: "testidentity" };
const folderData = { id: "folder-id", envId: "env-id" };
const commitData = { id: "commit-id", folderId: "folder-id" };
mockIdentityDAL.findById.mockResolvedValue(identityData);
mockFolderDAL.findById.mockResolvedValue(folderData);
mockFolderCommitDAL.create.mockResolvedValue(commitData);
mockFolderCheckpointDAL.findLatestByFolderId.mockResolvedValue(null);
mockFolderCommitDAL.findLatestCommit.mockResolvedValue({ id: "latest-commit-id" });
mockFolderDAL.findByParentId.mockResolvedValue([]);
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]);
const data = {
actor: {
type: ActorType.IDENTITY,
metadata: { id: identityData.id }
},
message: "Test commit",
folderId: folderData.id,
changes: [
{
type: "add",
folderVersionId: "folder-version-1"
}
]
};
// Act
const result = await folderCommitService.createCommit(data);
// Assert
expect(mockIdentityDAL.findById).toHaveBeenCalledWith(identityData.id, undefined);
expect(mockFolderDAL.findById).toHaveBeenCalledWith(folderData.id, undefined);
expect(mockFolderCommitDAL.create).toHaveBeenCalledWith(
expect.objectContaining({
actorType: ActorType.IDENTITY,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
actorMetadata: expect.objectContaining({ name: identityData.name }),
message: data.message,
folderId: data.folderId,
envId: folderData.envId
}),
undefined
);
expect(mockFolderCommitChangesDAL.insertMany).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
folderCommitId: commitData.id,
changeType: data.changes[0].type,
folderVersionId: data.changes[0].folderVersionId
})
]),
undefined
);
expect(mockFolderCommitQueueService.scheduleTreeCheckpoint).toHaveBeenCalledWith(folderData.envId);
expect(result).toEqual(commitData);
});
it("should throw NotFoundError when folder does not exist", async () => {
// Arrange
mockFolderDAL.findById.mockResolvedValue(null);
const data = {
actor: {
type: ActorType.PLATFORM
},
message: "Test commit",
folderId: "non-existent-folder",
changes: []
};
// Act & Assert
await expect(folderCommitService.createCommit(data)).rejects.toThrow(NotFoundError);
expect(mockFolderDAL.findById).toHaveBeenCalledWith("non-existent-folder", undefined);
});
});
describe("addCommitChange", () => {
it("should successfully add a change to an existing commit", async () => {
// Arrange
const commitData = { id: "commit-id", folderId: "folder-id" };
const changeData = { id: "change-id", folderCommitId: "commit-id" };
mockFolderCommitDAL.findById.mockResolvedValue(commitData);
mockFolderCommitChangesDAL.create.mockResolvedValue(changeData);
const data = {
folderCommitId: commitData.id,
changeType: "add",
secretVersionId: "secret-version-1"
};
// Act
const result = await folderCommitService.addCommitChange(data);
// Assert
expect(mockFolderCommitDAL.findById).toHaveBeenCalledWith(commitData.id, undefined);
expect(mockFolderCommitChangesDAL.create).toHaveBeenCalledWith(data, undefined);
expect(result).toEqual(changeData);
});
it("should throw BadRequestError when neither secretVersionId nor folderVersionId is provided", async () => {
// Arrange
const data = {
folderCommitId: "commit-id",
changeType: "add"
};
// Act & Assert
await expect(folderCommitService.addCommitChange(data)).rejects.toThrow(BadRequestError);
});
it("should throw NotFoundError when commit does not exist", async () => {
// Arrange
mockFolderCommitDAL.findById.mockResolvedValue(null);
const data = {
folderCommitId: "non-existent-commit",
changeType: "add",
secretVersionId: "secret-version-1"
};
// Act & Assert
await expect(folderCommitService.addCommitChange(data)).rejects.toThrow(NotFoundError);
expect(mockFolderCommitDAL.findById).toHaveBeenCalledWith("non-existent-commit", undefined);
});
});
// Note: reconstructFolderState is an internal function not exposed in the public API
// We'll test it indirectly through compareFolderStates
describe("compareFolderStates", () => {
it("should mark all resources as creates when currentCommitId is not provided", async () => {
// Arrange
const targetCommitId = "target-commit-id";
const targetCommit = { id: targetCommitId, commitId: 1, folderId: "folder-id" };
mockFolderCommitDAL.findById.mockResolvedValue(targetCommit);
// Mock how compareFolderStates would process the results internally
mockFolderCheckpointDAL.findNearestCheckpoint.mockResolvedValue({ id: "checkpoint-id", commitId: "hash-0" });
mockFolderCheckpointResourcesDAL.findByCheckpointId.mockResolvedValue([
{ secretVersionId: "secret-version-1", referencedSecretId: "secret-1" },
{ folderVersionId: "folder-version-1", referencedFolderId: "folder-1" }
]);
mockFolderCommitDAL.findCommitsToRecreate.mockResolvedValue([]);
// Act
const result = await folderCommitService.compareFolderStates({
targetCommitId
});
// Assert
expect(mockFolderCommitDAL.findById).toHaveBeenCalledWith(targetCommitId, undefined);
// Verify we get resources marked as create
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
changeType: "create",
commitId: targetCommit.commitId
})
])
);
});
});
describe("createFolderCheckpoint", () => {
it("should successfully create a checkpoint when force is true", async () => {
// Arrange
const folderCommitId = "commit-id";
const folderId = "folder-id";
const checkpointData = { id: "checkpoint-id", folderCommitId };
mockFolderDAL.findByParentId.mockResolvedValue([{ id: "subfolder-id" }]);
mockFolderVersionDAL.findLatestFolderVersions.mockResolvedValue({ "subfolder-id": { id: "folder-version-1" } });
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([{ id: "secret-version-1" }]);
mockFolderCheckpointDAL.create.mockResolvedValue(checkpointData);
// Act
const result = await folderCommitService.createFolderCheckpoint({
folderId,
folderCommitId,
force: true
});
// Assert
expect(mockFolderCheckpointDAL.create).toHaveBeenCalledWith({ folderCommitId }, undefined);
expect(mockFolderCheckpointResourcesDAL.insertMany).toHaveBeenCalled();
expect(result).toBe(folderCommitId);
});
});
describe("deepRollbackFolder", () => {
it("should throw NotFoundError when commit doesn't exist", async () => {
// Arrange
const targetCommitId = "non-existent-commit";
const envId = "env-id";
const actorId = "user-id";
const actorType = ActorType.USER;
const projectId = "project-id";
mockFolderCommitDAL.findById.mockResolvedValue(null);
// Act & Assert
await expect(
folderCommitService.deepRollbackFolder(targetCommitId, envId, actorId, actorType, projectId)
).rejects.toThrow(NotFoundError);
});
});
describe("createFolderTreeCheckpoint", () => {
it("should create a tree checkpoint when checkpoint window is exceeded", async () => {
// Arrange
const envId = "env-id";
const folderCommitId = "commit-id";
const latestCommit = { id: folderCommitId };
const latestTreeCheckpoint = { id: "tree-checkpoint-id", folderCommitId: "old-commit-id" };
const folders = [
{ id: "folder-1", isReserved: false },
{ id: "folder-2", isReserved: false },
{ id: "folder-3", isReserved: true } // Reserved folders should be filtered out
];
const folderCommits = [
{ folderId: "folder-1", id: "commit-1" },
{ folderId: "folder-2", id: "commit-2" }
];
const treeCheckpoint = { id: "new-tree-checkpoint-id" };
mockFolderCommitDAL.findLatestEnvCommit.mockResolvedValue(latestCommit);
mockFolderTreeCheckpointDAL.findLatestByEnvId.mockResolvedValue(latestTreeCheckpoint);
mockFolderCommitDAL.getEnvNumberOfCommitsSince.mockResolvedValue(15); // More than PIT_TREE_CHECKPOINT_WINDOW (10)
mockFolderDAL.findByEnvId.mockResolvedValue(folders);
mockFolderCommitDAL.findMultipleLatestCommits.mockResolvedValue(folderCommits);
mockFolderTreeCheckpointDAL.create.mockResolvedValue(treeCheckpoint);
// Act
await folderCommitService.createFolderTreeCheckpoint(envId);
// Assert
expect(mockFolderCommitDAL.findLatestEnvCommit).toHaveBeenCalledWith(envId, undefined);
expect(mockFolderTreeCheckpointDAL.create).toHaveBeenCalledWith({ folderCommitId }, undefined);
});
});
describe("applyFolderStateDifferences", () => {
it("should process changes correctly", async () => {
// Arrange
const folderId = "folder-id";
const projectId = "project-id";
const actorId = "user-id";
const actorType = ActorType.USER;
const differences = [
{ type: "secret", id: "secret-1", versionId: "v1", changeType: ChangeType.CREATE, commitId: 1 },
{ type: "folder", id: "folder-1", versionId: "v2", changeType: ChangeType.UPDATE, commitId: 1 }
];
const secretVersions = {
"secret-1": {
id: "secret-version-1",
createdAt: new Date(),
updatedAt: new Date(),
type: "shared",
folderId: "folder-1",
secretId: "secret-1",
version: 1,
key: "SECRET_KEY",
encryptedValue: Buffer.from("encrypted"),
encryptedComment: Buffer.from("comment"),
skipMultilineEncoding: false,
userId: "user-1",
envId: "env-1",
metadata: {}
} as TSecretVersionsV2
};
const folderVersions = {
"folder-1": {
folderId: "folder-1",
version: 1,
name: "Test Folder",
envId: "env-1"
} as TSecretFolderVersions
};
// Mock folder lookup for the folder being processed
mockFolderDAL.findById.mockImplementation((id) => {
if (id === folderId) {
return Promise.resolve({ id: folderId, envId: "env-1" });
}
return Promise.resolve(null);
});
// Mock latest commit lookup
mockFolderCommitDAL.findLatestCommit.mockImplementation((id) => {
if (id === folderId) {
return Promise.resolve({ id: "latest-commit-id", folderId });
}
return Promise.resolve(null);
});
// Make sure findByParentId returns an array, not undefined
mockFolderDAL.findByParentId.mockResolvedValue([]);
// Make sure other required functions return appropriate values
mockFolderCheckpointDAL.findLatestByFolderId.mockResolvedValue(null);
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]);
// These mocks need to return objects with an id field
mockSecretVersionV2BridgeDAL.findByIdsWithLatestVersion.mockResolvedValue(Object.values(secretVersions));
mockFolderVersionDAL.findByIdsWithLatestVersion.mockResolvedValue(Object.values(folderVersions));
mockSecretV2BridgeDAL.insertMany.mockResolvedValue([{ id: "new-secret-1" }]);
mockSecretVersionV2BridgeDAL.create.mockResolvedValue({ id: "new-secret-version-1" });
mockFolderDAL.updateById.mockResolvedValue({ id: "updated-folder-1" });
mockFolderVersionDAL.create.mockResolvedValue({ id: "new-folder-version-1" });
mockFolderCommitDAL.create.mockResolvedValue({ id: "new-commit-id" });
mockSecretVersionV2BridgeDAL.findLatestVersionMany.mockResolvedValue([
{
id: "secret-version-1",
createdAt: new Date(),
updatedAt: new Date(),
type: "shared",
folderId: "folder-1",
secretId: "secret-1",
version: 1,
key: "SECRET_KEY",
encryptedValue: Buffer.from("encrypted"),
encryptedComment: Buffer.from("comment"),
skipMultilineEncoding: false,
userId: "user-1",
envId: "env-1",
metadata: {}
}
]);
// Mock transaction
mockFolderCommitDAL.transaction.mockImplementation(<T>(callback: TransactionCallback<T>) => callback({} as Knex));
// Act
const result = await folderCommitService.applyFolderStateDifferences({
differences,
actorInfo: {
actorType,
actorId,
message: "Applying changes"
},
folderId,
projectId,
reconstructNewFolders: false
});
// Assert
expect(mockFolderCommitDAL.create).toHaveBeenCalled();
expect(mockSecretV2BridgeDAL.invalidateSecretCacheByProjectId).toHaveBeenCalledWith(projectId);
// Check that we got the right counts
expect(result).toEqual({
secretChangesCount: 1,
folderChangesCount: 1,
totalChanges: 2
});
});
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TFolderTreeCheckpointResources } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
export type TFolderTreeCheckpointResourcesDALFactory = ReturnType<typeof folderTreeCheckpointResourcesDALFactory>;
type TFolderTreeCheckpointResourcesWithCommitId = TFolderTreeCheckpointResources & {
commitId: number;
};
export const folderTreeCheckpointResourcesDALFactory = (db: TDbClient) => {
const folderTreeCheckpointResourcesOrm = ormify(db, TableName.FolderTreeCheckpointResources);
const findByTreeCheckpointId = async (
folderTreeCheckpointId: string,
tx?: Knex
): Promise<TFolderTreeCheckpointResourcesWithCommitId[]> => {
try {
const docs = await (tx || db.replicaNode())<TFolderTreeCheckpointResources>(
TableName.FolderTreeCheckpointResources
)
.join(
TableName.FolderCommit,
`${TableName.FolderTreeCheckpointResources}.folderCommitId`,
`${TableName.FolderCommit}.id`
)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ folderTreeCheckpointId }, TableName.FolderTreeCheckpointResources))
.select(selectAllTableCols(TableName.FolderTreeCheckpointResources))
.select(db.ref("commitId").withSchema(TableName.FolderCommit).as("commitId"));
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindByTreeCheckpointId" });
}
};
return {
...folderTreeCheckpointResourcesOrm,
findByTreeCheckpointId
};
};
@@ -0,0 +1,86 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TFolderCommits, TFolderTreeCheckpoints } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TFolderTreeCheckpointDALFactory = ReturnType<typeof folderTreeCheckpointDALFactory>;
type TreeCheckpointWithCommitInfo = TFolderTreeCheckpoints & {
commitId: number;
};
export const folderTreeCheckpointDALFactory = (db: TDbClient) => {
const folderTreeCheckpointOrm = ormify(db, TableName.FolderTreeCheckpoint);
const findByCommitId = async (folderCommitId: string, tx?: Knex): Promise<TFolderTreeCheckpoints | undefined> => {
try {
const doc = await (tx || db.replicaNode())<TFolderTreeCheckpoints>(TableName.FolderTreeCheckpoint)
.where({ folderCommitId })
.select(selectAllTableCols(TableName.FolderTreeCheckpoint))
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindByCommitId" });
}
};
const findNearestCheckpoint = async (
folderCommitId: string,
envId: string,
tx?: Knex
): Promise<TreeCheckpointWithCommitInfo | undefined> => {
try {
const targetCommit = await (tx || db.replicaNode())(TableName.FolderCommit)
.where({ id: folderCommitId })
.select("id", "commitId", "folderId")
.first();
if (!targetCommit) {
return undefined;
}
const nearestCheckpoint = await (tx || db.replicaNode())(TableName.FolderTreeCheckpoint)
.join<TFolderCommits>(
TableName.FolderCommit,
`${TableName.FolderTreeCheckpoint}.folderCommitId`,
`${TableName.FolderCommit}.id`
)
.where(`${TableName.FolderCommit}.commitId`, "<=", targetCommit.commitId.toString())
.where(`${TableName.FolderCommit}.envId`, envId)
.select(selectAllTableCols(TableName.FolderTreeCheckpoint))
.select(db.ref("commitId").withSchema(TableName.FolderCommit))
.orderBy(`${TableName.FolderCommit}.commitId`, "desc")
.first();
return nearestCheckpoint;
} catch (error) {
throw new DatabaseError({ error, name: "FindNearestCheckpoint" });
}
};
const findLatestByEnvId = async (envId: string, tx?: Knex): Promise<TFolderTreeCheckpoints | undefined> => {
try {
const doc = await (tx || db.replicaNode())<TFolderTreeCheckpoints>(TableName.FolderTreeCheckpoint)
.join<TFolderCommits>(
TableName.FolderCommit,
`${TableName.FolderTreeCheckpoint}.folderCommitId`,
`${TableName.FolderCommit}.id`
)
.where(`${TableName.FolderCommit}.envId`, envId)
.orderBy(`${TableName.FolderTreeCheckpoint}.createdAt`, "desc")
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestByFolderId" });
}
};
return {
...folderTreeCheckpointOrm,
findByCommitId,
findNearestCheckpoint,
findLatestByEnvId
};
};
@@ -488,6 +488,73 @@ export const secretFolderDALFactory = (db: TDbClient) => {
}
};
const findFoldersByRootAndIds = async ({ rootId, folderIds }: { rootId: string; folderIds: string[] }, tx?: Knex) => {
try {
// First, get all descendant folders of rootId
const descendants = await (tx || db.replicaNode())
.withRecursive("descendants", (qb) =>
qb
.select(
selectAllTableCols(TableName.SecretFolder),
db.raw("0 as depth"),
db.raw(`'/' as path`),
db.ref(`${TableName.Environment}.slug`).as("environment")
)
.from(TableName.SecretFolder)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.where(`${TableName.SecretFolder}.id`, rootId)
.union((un) => {
void un
.select(
selectAllTableCols(TableName.SecretFolder),
db.raw("descendants.depth + 1 as depth"),
db.raw(
`CONCAT(
CASE WHEN descendants.path = '/' THEN '' ELSE descendants.path END,
CASE WHEN ${TableName.SecretFolder}."parentId" is NULL THEN '' ELSE CONCAT('/', secret_folders.name) END
)`
),
db.ref("descendants.environment")
)
.from(TableName.SecretFolder)
.where(`${TableName.SecretFolder}.isReserved`, false)
.join("descendants", `${TableName.SecretFolder}.parentId`, "descendants.id");
})
)
.select<(TSecretFolders & { path: string; depth: number; environment: string })[]>("*")
.from("descendants")
.whereIn(`id`, folderIds)
.orderBy("depth")
.orderBy(`name`);
return descendants;
} catch (error) {
throw new DatabaseError({ error, name: "FindFoldersByRootAndIds" });
}
};
const findByParentId = async (parentId: string, tx?: Knex) => {
try {
const folders = await (tx || db.replicaNode())(TableName.SecretFolder)
.where({ parentId })
.select(selectAllTableCols(TableName.SecretFolder));
return folders;
} catch (error) {
throw new DatabaseError({ error, name: "findByParentId" });
}
};
const findByEnvId = async (envId: string, tx?: Knex) => {
try {
const folders = await (tx || db.replicaNode())(TableName.SecretFolder)
.where({ envId })
.select(selectAllTableCols(TableName.SecretFolder));
return folders;
} catch (error) {
throw new DatabaseError({ error, name: "findByEnvId" });
}
};
return {
...secretFolderOrm,
update,
@@ -499,6 +566,9 @@ export const secretFolderDALFactory = (db: TDbClient) => {
findClosestFolder,
findByProjectId,
findByMultiEnv,
findByEnvsDeep
findByEnvsDeep,
findByParentId,
findByEnvId,
findFoldersByRootAndIds
};
};
@@ -10,6 +10,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "./secret-folder-dal";
@@ -29,7 +30,8 @@ type TSecretFolderServiceFactoryDep = {
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
folderDAL: TSecretFolderDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
folderVersionDAL: TSecretFolderVersionDALFactory;
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestFolderVersions" | "create" | "insertMany" | "find">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
};
@@ -41,6 +43,7 @@ export const secretFolderServiceFactory = ({
permissionService,
projectEnvDAL,
folderVersionDAL,
folderCommitService,
projectDAL
}: TSecretFolderServiceFactoryDep) => {
const createFolder = async ({
@@ -111,7 +114,7 @@ export const secretFolderServiceFactory = ({
});
parentFolderId = newFolders.at(-1)?.id as string;
const docs = await folderDAL.insertMany(newFolders, tx);
await folderVersionDAL.insertMany(
const folderVersions = await folderVersionDAL.insertMany(
docs.map((doc) => ({
name: doc.name,
envId: doc.envId,
@@ -120,6 +123,23 @@ export const secretFolderServiceFactory = ({
})),
tx
);
await folderCommitService.createCommit(
{
actor: {
type: actor,
metadata: {
id: actorId
}
},
message: "Folder created",
folderId: parentFolderId,
changes: folderVersions.map((fv) => ({
type: "add",
folderVersionId: fv.id
}))
},
tx
);
}
}
@@ -127,7 +147,7 @@ export const secretFolderServiceFactory = ({
{ name, envId: env.id, version: 1, parentId: parentFolderId, description },
tx
);
await folderVersionDAL.create(
const folderVersion = await folderVersionDAL.create(
{
name: doc.name,
envId: doc.envId,
@@ -136,6 +156,25 @@ export const secretFolderServiceFactory = ({
},
tx
);
await folderCommitService.createCommit(
{
actor: {
type: actor,
metadata: {
id: actorId
}
},
message: "Folder created",
folderId: parentFolderId,
changes: [
{
type: "add",
folderVersionId: folderVersion.id
}
]
},
tx
);
return doc;
});
@@ -225,7 +264,7 @@ export const secretFolderServiceFactory = ({
{ name, description },
tx
);
await folderVersionDAL.create(
const folderVersion = await folderVersionDAL.create(
{
name: doc.name,
envId: doc.envId,
@@ -234,6 +273,26 @@ export const secretFolderServiceFactory = ({
},
tx
);
await folderCommitService.createCommit(
{
actor: {
type: actor,
metadata: {
id: actorId
}
},
message: "Folder updated",
folderId: parentFolder.id,
changes: [
{
type: "add",
isUpdate: true,
folderVersionId: folderVersion.id
}
]
},
tx
);
if (!doc) {
throw new NotFoundError({
message: `Failed to update folder with id '${id}', not found`,
@@ -321,7 +380,7 @@ export const secretFolderServiceFactory = ({
{ name, description },
tx
);
await folderVersionDAL.create(
const folderVersion = await folderVersionDAL.create(
{
name: doc.name,
envId: doc.envId,
@@ -330,6 +389,26 @@ export const secretFolderServiceFactory = ({
},
tx
);
await folderCommitService.createCommit(
{
actor: {
type: actor,
metadata: {
id: actorId
}
},
message: "Folder updated",
folderId: parentFolder.id,
changes: [
{
type: "add",
isUpdate: true,
folderVersionId: folderVersion.id
}
]
},
tx
);
if (!doc) throw new NotFoundError({ message: `Failed to update folder with ID '${id}'`, name: "UpdateFolder" });
return doc;
});
@@ -381,7 +460,30 @@ export const secretFolderServiceFactory = ({
},
tx
);
if (!doc) throw new NotFoundError({ message: `Failed to delete folder with ID '${idOrName}', not found` });
const folderVersions = await folderVersionDAL.findLatestFolderVersions([doc.id]);
await folderCommitService.createCommit(
{
actor: {
type: actor,
metadata: {
id: actorId
}
},
message: "Folder deleted",
folderId: parentFolder.id,
changes: [
{
type: "delete",
folderVersionId: folderVersions[doc.id].id
}
]
},
tx
);
return doc;
});
@@ -665,6 +767,22 @@ export const secretFolderServiceFactory = ({
return environmentFolders;
};
const getFolderVersionsByIds = async ({
folderId,
folderVersions
}: {
folderId: string;
folderVersions: string[];
}) => {
const versions = await folderVersionDAL.find({
folderId,
$in: {
version: folderVersions.map((v) => Number.parseInt(v, 10))
}
});
return versions;
};
return {
createFolder,
updateFolder,
@@ -675,6 +793,7 @@ export const secretFolderServiceFactory = ({
getProjectFolderCount,
getFoldersMultiEnv,
getFoldersDeepByEnvs,
getProjectEnvironmentsFolders
getProjectEnvironmentsFolders,
getFolderVersionsByIds
};
};
@@ -43,7 +43,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
const docs: Array<TSecretFolderVersions & { max: number }> = await (tx || db.replicaNode())(
TableName.SecretFolderVersion
)
.whereIn("folderId", folderIds)
.whereIn(`${TableName.SecretFolderVersion}.folderId`, folderIds)
.join(
(tx || db)(TableName.SecretFolderVersion)
.groupBy("folderId")
@@ -97,5 +97,80 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret folder versions completed`);
};
return { ...secretFolderVerOrm, findLatestFolderVersions, findLatestVersionByFolderId, pruneExcessVersions };
const findByIdsWithLatestVersion = async (folderIds: string[], versionIds?: string[], tx?: Knex) => {
try {
if (!folderIds.length && (!versionIds || !versionIds.length)) return {};
const knexInstance = tx || db.replicaNode();
let allDocs: Array<TSecretFolderVersions & { max?: number }> = [];
// Process latest versions for folderIds
if (folderIds.length) {
const latestVersions: Array<TSecretFolderVersions & { max: number }> = await knexInstance(
TableName.SecretFolderVersion
)
.whereIn(`${TableName.SecretFolderVersion}.folderId`, folderIds)
.join(
knexInstance(TableName.SecretFolderVersion)
.groupBy("folderId")
.max("version")
.select("folderId")
.as("latestVersion"),
(bd) => {
bd.on(`${TableName.SecretFolderVersion}.folderId`, "latestVersion.folderId").andOn(
`${TableName.SecretFolderVersion}.version`,
"latestVersion.max"
);
}
);
allDocs = [...allDocs, ...latestVersions];
}
// Process specific versions by versionIds
if (versionIds && versionIds.length) {
// Get the specific versions
const specificVersions = await knexInstance(TableName.SecretFolderVersion).whereIn("id", versionIds);
// Get the folderIds from these versions
const specificFolderIds = [...new Set(specificVersions.map((v) => v.folderId).filter(Boolean))];
// Get max versions for these folderIds
const maxVersionsQuery = (await knexInstance(TableName.SecretFolderVersion)
.whereIn("folderId", specificFolderIds)
.groupBy("folderId")
.select("folderId")
.max("version as max")) as Array<{ folderId: string; max: number }>;
// Create a lookup map for max versions
const maxVersionMap = maxVersionsQuery.reduce<Record<string, number>>((acc, item) => {
acc[item.folderId] = item.max;
return acc;
}, {});
// Add max version to each specific version
const specificVersionsWithMax = specificVersions.map((version) => ({
...version,
max: maxVersionMap[version.folderId]
}));
allDocs = [...allDocs, ...specificVersionsWithMax];
}
return allDocs.reduce<Record<string, TSecretFolderVersions & { max?: number }>>(
(prev, curr) => ({ ...prev, [curr.folderId || ""]: curr }),
{}
);
} catch (error) {
throw new DatabaseError({ error, name: "FindByIdsWithLatestVersion" });
}
};
return {
...secretFolderVerOrm,
findLatestFolderVersions,
findLatestVersionByFolderId,
pruneExcessVersions,
findByIdsWithLatestVersion
};
};
@@ -58,6 +58,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
@@ -93,6 +94,7 @@ type TSecretSyncQueueFactoryDep = {
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
};
type SecretSyncActionJob = Job<
@@ -133,7 +135,8 @@ export const secretSyncQueueFactory = ({
secretVersionTagDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL,
resourceMetadataDAL
resourceMetadataDAL,
folderCommitService
}: TSecretSyncQueueFactoryDep) => {
const appCfg = getConfig();
@@ -164,7 +167,8 @@ export const secretSyncQueueFactory = ({
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL,
resourceMetadataDAL
resourceMetadataDAL,
folderCommitService
});
const $updateManySecretsRawFn = updateManySecretsRawFnFactory({
@@ -180,7 +184,8 @@ export const secretSyncQueueFactory = ({
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL,
resourceMetadataDAL
resourceMetadataDAL,
folderCommitService
});
const $getInfisicalSecrets = async (
@@ -73,6 +73,7 @@ export const fnSecretBulkInsert = async ({
resourceMetadataDAL,
secretTagDAL,
secretVersionTagDAL,
folderCommitService,
actor,
tx
}: TFnSecretBulkInsert) => {
@@ -131,6 +132,30 @@ export const fnSecretBulkInsert = async ({
tx
);
const commitChanges = secretVersions
.filter(({ type }) => type === SecretType.Shared)
.map((sv) => ({
type: "add",
secretVersionId: sv.id
}));
if (commitChanges.length > 0) {
await folderCommitService.createCommit(
{
actor: {
type: actorType || ActorType.PLATFORM,
metadata: {
id: actor?.actorId
}
},
message: "Secret Created",
folderId,
changes: commitChanges
},
tx
);
}
await secretDAL.upsertSecretReferences(
inputSecrets.map(({ references = [], key }) => ({
secretId: newSecretGroupedByKeyName[key][0].id,
@@ -185,6 +210,7 @@ export const fnSecretBulkUpdate = async ({
orgId,
secretDAL,
secretVersionDAL,
folderCommitService,
secretTagDAL,
secretVersionTagDAL,
resourceMetadataDAL,
@@ -259,6 +285,31 @@ export const fnSecretBulkUpdate = async ({
),
tx
);
const commitChanges = secretVersions
.filter(({ type }) => type === SecretType.Shared)
.map((sv) => ({
type: "add",
isUpdate: true,
secretVersionId: sv.id
}));
if (commitChanges.length > 0) {
await folderCommitService.createCommit(
{
actor: {
type: actorType || ActorType.PLATFORM,
metadata: {
id: actor?.actorId
}
},
message: "Secret Updated",
folderId,
changes: commitChanges
},
tx
);
}
await secretDAL.upsertSecretReferences(
inputSecrets
.filter(({ data: { references } }) => Boolean(references))
@@ -337,8 +388,11 @@ export const fnSecretBulkDelete = async ({
inputSecrets,
tx,
actorId,
actorType,
secretDAL,
secretQueueService
secretQueueService,
folderCommitService,
secretVersionDAL
}: TFnSecretBulkDelete) => {
const deletedSecrets = await secretDAL.deleteMany(
inputSecrets.map(({ type, secretKey }) => ({
@@ -358,6 +412,35 @@ export const fnSecretBulkDelete = async ({
)
);
const secretVersions = await secretVersionDAL.findLatestVersionMany(
folderId,
deletedSecrets.map(({ id }) => id),
tx
);
const commitChanges = deletedSecrets
.filter(({ type }) => type === SecretType.Shared)
.map(({ id }) => ({
type: "delete",
secretVersionId: secretVersions[id].id
}));
if (commitChanges.length > 0) {
await folderCommitService.createCommit(
{
actor: {
type: actorType || ActorType.PLATFORM,
metadata: {
id: actorId
}
},
message: "Secret Deleted",
folderId,
changes: commitChanges
},
tx
);
}
return deletedSecrets;
};
@@ -34,6 +34,7 @@ import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ActorType } from "../auth/auth-type";
import { 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";
@@ -90,6 +91,7 @@ type TSecretV2BridgeServiceFactoryDep = {
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
secretTagDAL: TSecretTagDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
folderDAL: Pick<
TSecretFolderDALFactory,
@@ -124,6 +126,7 @@ export const secretV2BridgeServiceFactory = ({
projectEnvDAL,
secretTagDAL,
secretVersionDAL,
folderCommitService,
folderDAL,
permissionService,
snapshotService,
@@ -327,6 +330,7 @@ export const secretV2BridgeServiceFactory = ({
resourceMetadataDAL,
secretDAL,
secretVersionDAL,
folderCommitService,
secretTagDAL,
secretVersionTagDAL,
actor: {
@@ -510,6 +514,7 @@ export const secretV2BridgeServiceFactory = ({
folderId,
orgId: actorOrgId,
resourceMetadataDAL,
folderCommitService,
inputSecrets: [
{
filter: { id: secretId },
@@ -650,6 +655,9 @@ export const secretV2BridgeServiceFactory = ({
projectId,
folderId,
actorId,
actorType: actor,
folderCommitService,
secretVersionDAL,
secretDAL,
secretQueueService,
inputSecrets: [
@@ -1590,6 +1598,7 @@ export const secretV2BridgeServiceFactory = ({
orgId: actorOrgId,
secretDAL,
resourceMetadataDAL,
folderCommitService,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
@@ -1859,6 +1868,7 @@ export const secretV2BridgeServiceFactory = ({
const bulkUpdatedSecrets = await fnSecretBulkUpdate({
folderId,
orgId: actorOrgId,
folderCommitService,
tx,
inputSecrets: secretsToUpdate.map((el) => {
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
@@ -1928,6 +1938,7 @@ export const secretV2BridgeServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderCommitService,
actor: {
type: actor,
actorId
@@ -2061,6 +2072,8 @@ export const secretV2BridgeServiceFactory = ({
fnSecretBulkDelete({
secretDAL,
secretQueueService,
folderCommitService,
secretVersionDAL,
inputSecrets: inputSecrets.map(({ type, secretKey }) => ({
secretKey,
type: type || SecretType.Shared
@@ -2068,6 +2081,7 @@ export const secretV2BridgeServiceFactory = ({
projectId,
folderId,
actorId,
actorType: actor,
tx
})
);
@@ -2164,10 +2178,14 @@ export const secretV2BridgeServiceFactory = ({
type: KmsDataKey.SecretManager,
projectId: folder.projectId
});
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors(secretId, folder.projectId, {
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
secretId,
projectId: folder.projectId,
findOpt: {
offset,
limit,
sort: [["createdAt", "desc"]]
}
});
return secretVersions.map((el) => {
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
@@ -2469,6 +2487,7 @@ export const secretV2BridgeServiceFactory = ({
tx,
secretTagDAL,
resourceMetadataDAL,
folderCommitService,
secretVersionTagDAL,
actor: {
type: actor,
@@ -2495,6 +2514,7 @@ export const secretV2BridgeServiceFactory = ({
folderId: destinationFolder.id,
orgId: actorOrgId,
resourceMetadataDAL,
folderCommitService,
secretVersionDAL,
secretDAL,
tx,
@@ -2840,6 +2860,73 @@ export const secretV2BridgeServiceFactory = ({
};
};
const getSecretVersionsByIds = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
secretId,
secretVersionNumbers,
folderId
}: TGetSecretVersionsDTO & {
secretVersionNumbers: string[];
folderId: string;
}) => {
const folder = await folderDAL.findById(folderId);
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({
actor,
actorId,
projectId: folder.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: folder.projectId
});
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
secretId,
projectId: folder.projectId,
secretVersions: secretVersionNumbers
});
return secretVersions.map((el) => {
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment: folder.environment.envSlug,
secretPath: folderWithPath.path,
secretName: el.key,
...(el.tags?.length && {
secretTags: el.tags.map((tag) => tag.slug)
})
}
);
return reshapeBridgeSecret(
folder.projectId,
folder.environment.envSlug,
folderWithPath.path,
{
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
},
secretValueHidden
);
});
};
return {
createSecret,
deleteSecret,
@@ -2858,6 +2945,7 @@ export const secretV2BridgeServiceFactory = ({
getSecretReferenceTree,
getSecretsByFolderMappings,
getSecretById,
getAccessibleSecrets
getAccessibleSecrets,
getSecretVersionsByIds
};
};
@@ -8,6 +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 { 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";
@@ -178,9 +179,10 @@ export type TFnSecretBulkInsert = {
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
actor?: {
type: string;
actorId: string;
actorId?: string;
};
};
@@ -206,9 +208,10 @@ export type TFnSecretBulkUpdate = {
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
actor?: {
type: string;
actorId: string;
actorId?: string;
};
tx?: Knex;
};
@@ -218,11 +221,14 @@ export type TFnSecretBulkDelete = {
projectId: string;
inputSecrets: Array<{ type: SecretType; secretKey: string }>;
actorId: string;
actorType?: string;
tx?: Knex;
secretDAL: Pick<TSecretV2BridgeDALFactory, "deleteMany">;
secretQueueService: {
removeSecretReminder: (data: TRemoveSecretReminderDTO, tx?: Knex) => Promise<void>;
};
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "findLatestVersionMany">;
};
export type THandleReminderDTO = {
@@ -138,7 +138,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
{}
);
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestVersinMany" });
throw new DatabaseError({ error, name: "FindLatestVersionMany" });
}
};
@@ -174,13 +174,21 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
};
const findVersionsBySecretIdWithActors = async (
secretId: string,
projectId: string,
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TSecretVersionsV2> = {},
tx?: Knex
) => {
const findVersionsBySecretIdWithActors = async ({
secretId,
projectId,
secretVersions,
findOpt = {},
tx
}: {
secretId: string;
projectId: string;
secretVersions?: string[];
findOpt?: TFindOpt<TSecretVersionsV2>;
tx?: Knex;
}) => {
try {
const { offset, limit, sort = [["createdAt", "desc"]] } = findOpt;
const query = (tx || db)(TableName.SecretVersionV2)
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`)
.leftJoin(
@@ -203,10 +211,12 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
.where((qb) => {
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions);
})
.orWhere((qb) => {
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
void qb.whereNull(`${TableName.ProjectMembership}.projectId`);
if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions);
})
.select(
selectAllTableCols(TableName.SecretVersionV2),
@@ -262,6 +272,84 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
}
};
const findByIdsWithLatestVersion = async (
folderId: string,
secretIds: string[],
versionIds?: string[],
tx?: Knex
) => {
try {
if (!secretIds.length && (!versionIds || !versionIds.length)) return {};
const knexInstance = tx || db.replicaNode();
let allDocs: Array<TSecretVersionsV2 & { max?: number }> = [];
// Process latest versions for secretIds
if (secretIds.length) {
const latestVersions: Array<TSecretVersionsV2 & { max: number }> = await knexInstance(TableName.SecretVersionV2)
.where("folderId", folderId)
.whereIn(`${TableName.SecretVersionV2}.secretId`, secretIds)
.join(
knexInstance(TableName.SecretVersionV2)
.groupBy("secretId")
.max("version")
.select("secretId")
.as("latestVersion"),
(bd) => {
bd.on(`${TableName.SecretVersionV2}.secretId`, "latestVersion.secretId").andOn(
`${TableName.SecretVersionV2}.version`,
"latestVersion.max"
);
}
);
allDocs = [...allDocs, ...latestVersions];
}
// Process specific versions by versionIds
if (versionIds && versionIds.length) {
// Get the specific versions
const specificVersions = await knexInstance(TableName.SecretVersionV2)
.where("folderId", folderId)
.whereIn("id", versionIds);
// Get the secretIds from these versions
const specificSecretIds = [...new Set(specificVersions.map((v) => v.secretId).filter(Boolean))];
// Get max versions for these secretIds
const maxVersionsQuery = (await knexInstance(TableName.SecretVersionV2)
.whereIn("secretId", specificSecretIds)
.groupBy("secretId")
.select("secretId")
.max("version as max")) as Array<{ secretId: string; max: number }>;
// Create a lookup map for max versions
const maxVersionMap = maxVersionsQuery.reduce(
(acc, item) => {
acc[item.secretId] = item.max;
return acc;
},
{} as Record<string, number>
);
// Add max version to each specific version
const specificVersionsWithMax = specificVersions.map((version) => ({
...version,
max: maxVersionMap[version.secretId]
}));
allDocs = [...allDocs, ...specificVersionsWithMax];
}
return allDocs.reduce<Record<string, TSecretVersionsV2 & { max?: number }>>(
(prev, curr) => ({ ...prev, [curr.secretId || ""]: curr }),
{}
);
} catch (error) {
throw new DatabaseError({ error, name: "FindByIdsWithLatestVersion" });
}
};
return {
...secretVersionV2Orm,
pruneExcessVersions,
@@ -269,6 +357,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
bulkUpdate,
findLatestVersionByFolderId,
findVersionsBySecretIdWithActors,
findBySecretId
findBySecretId,
findByIdsWithLatestVersion
};
};
+5 -1
View File
@@ -778,6 +778,7 @@ export const createManySecretsRawFnFactory = ({
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL,
folderCommitService,
kmsService,
resourceMetadataDAL
}: TCreateManySecretsRawFnFactory) => {
@@ -850,6 +851,7 @@ export const createManySecretsRawFnFactory = ({
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
folderCommitService,
tx
})
);
@@ -942,6 +944,7 @@ export const updateManySecretsRawFnFactory = ({
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
resourceMetadataDAL,
folderCommitService,
kmsService
}: TUpdateManySecretsRawFnFactory) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
@@ -1032,7 +1035,8 @@ export const updateManySecretsRawFnFactory = ({
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
folderCommitService
})
);
+8 -3
View File
@@ -35,6 +35,7 @@ import { TSecretSyncQueueFactory } from "@app/services/secret-sync/secret-sync-q
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { ActorType } from "../auth/auth-type";
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
@@ -112,6 +113,7 @@ type TSecretQueueFactoryDep = {
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
secretReminderRecipientsDAL: Pick<
TSecretReminderRecipientsDALFactory,
"delete" | "findUsersBySecretId" | "insertMany" | "transaction"
@@ -178,7 +180,8 @@ export const secretQueueFactory = ({
projectKeyDAL,
resourceMetadataDAL,
secretReminderRecipientsDAL,
secretSyncQueue
secretSyncQueue,
folderCommitService
}: TSecretQueueFactoryDep) => {
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
@@ -366,7 +369,8 @@ export const secretQueueFactory = ({
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL,
resourceMetadataDAL
resourceMetadataDAL,
folderCommitService
});
const updateManySecretsRawFn = updateManySecretsRawFnFactory({
@@ -382,7 +386,8 @@ export const secretQueueFactory = ({
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL,
resourceMetadataDAL
resourceMetadataDAL,
folderCommitService
});
/**
+32 -1
View File
@@ -2521,6 +2521,36 @@ export const secretServiceFactory = ({
});
};
const getSecretVersionsV2ByIds = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
secretId,
secretVersions,
folderId
}: TGetSecretVersionsDTO & {
secretVersions: string[];
folderId: string;
}) => {
const secretVersionV2 = await secretV2BridgeService
.getSecretVersionsByIds({
actorId,
actor,
actorOrgId,
actorAuthMethod,
secretId,
folderId,
secretVersionNumbers: secretVersions
})
.catch((err) => {
if ((err as Error).message === "BadRequest: Failed to find secret") {
return null;
}
});
return secretVersionV2;
};
const attachTags = async ({
secretName,
tagSlugs,
@@ -3309,6 +3339,7 @@ export const secretServiceFactory = ({
getSecretsRawByFolderMappings,
getSecretAccessList,
getSecretByIdRaw,
getAccessibleSecrets
getAccessibleSecrets,
getSecretVersionsV2ByIds
};
};
@@ -14,6 +14,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { ActorType } from "../auth/auth-type";
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
@@ -441,6 +442,7 @@ export type TCreateManySecretsRawFnFactory = {
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
};
export type TCreateManySecretsRawFn = {
@@ -478,6 +480,7 @@ export type TUpdateManySecretsRawFnFactory = {
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
};
export type TUpdateManySecretsRawFn = {
@@ -14,13 +14,15 @@ type Props = {
selectedPathSegmentIndex: number;
environmentSlug: string;
projectId: string;
disableCopy?: boolean;
};
export const SecretDashboardPathBreadcrumb = ({
secretPathSegments,
selectedPathSegmentIndex,
environmentSlug,
projectId
projectId,
disableCopy
}: Props) => {
const [, isCopying, setIsCopying] = useTimedReset({
initialState: false
@@ -32,7 +34,7 @@ export const SecretDashboardPathBreadcrumb = ({
return (
<div className="flex items-center space-x-3">
{isLastItem ? (
{isLastItem && !disableCopy ? (
<div className="group flex items-center space-x-2">
<span
className={twMerge(
+12
View File
@@ -75,6 +75,18 @@ export const ROUTE_PATHS = Object.freeze({
"/secret-manager/$projectId/secrets/$envSlug",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug"
),
RollbackPreviewPage: setRoute(
"/secret-manager/$projectId/commits/$environment/$folderId/$commitId/restore",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
),
CommitDetailsPage: setRoute(
"/secret-manager/$projectId/commits/$environment/$folderId/$commitId",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId"
),
CommitsPage: setRoute(
"/secret-manager/$projectId/commits/$environment/$folderId",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId"
),
OverviewPage: setRoute(
"/secret-manager/$projectId/overview",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview"
@@ -106,6 +106,11 @@ export enum PermissionConditionOperators {
$ELEMENTMATCH = "$elemMatch"
}
export enum ProjectPermissionCommitsActions {
Read = "read",
PerformRollback = "perform-rollback"
}
export type IdentityManagementSubjectFields = {
identityId: string;
};
@@ -181,7 +186,8 @@ export enum ProjectPermissionSub {
Kms = "kms",
Cmek = "cmek",
SecretSyncs = "secret-syncs",
Kmip = "kmip"
Kmip = "kmip",
Commits = "commits"
}
export type SecretSubjectFields = {
@@ -284,6 +290,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip];
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
| [ProjectPermissionCommitsActions, ProjectPermissionSub.Commits];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
@@ -0,0 +1,2 @@
export { useGetFolderCommitHistory, useGetFolderCommitsCount } from "./queries";
export * from "./types";
@@ -0,0 +1,251 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { CommitHistoryItem, CommitWithChanges, RollbackPreview } from "./types";
export const commitKeys = {
count: ({
workspaceId,
environment,
directory
}: {
workspaceId: string;
environment: string;
directory?: string;
}) => [{ workspaceId, environment, directory }, "folder-commits-count"] as const,
history: ({
workspaceId,
environment,
directory
}: {
workspaceId: string;
environment: string;
directory?: string;
}) => [{ workspaceId, environment, directory }, "folder-commits"] as const,
details: ({ workspaceId, commitId }: { workspaceId: string; commitId: string }) =>
[{ workspaceId, commitId }, "commit-details"] as const,
rollbackPreview: ({
folderId,
commitId,
envId,
projectId,
deepRollback
}: {
folderId: string;
commitId: string;
envId: string;
projectId: string;
deepRollback: boolean;
}) => [{ folderId, commitId, envId, projectId, deepRollback }, "rollback-preview"] as const
};
const fetchFolderCommitsCount = async ({
workspaceId,
environment,
directory
}: {
workspaceId: string;
environment: string;
directory?: string;
}) => {
const res = await apiRequest.get<{ count: number; folderId: string }>(
`/api/v1/pit/commits/count/${workspaceId}`,
{
params: {
environment,
path: directory
}
}
);
return res.data;
};
const fetchFolderCommitHistory = async (
workspaceId: string,
environment: string,
directory: string
): Promise<CommitHistoryItem[]> => {
const res = await apiRequest.get<CommitHistoryItem[]>(`/api/v1/pit/commits/${workspaceId}`, {
params: {
environment,
path: directory
}
});
return res.data;
};
export const fetchCommitDetails = async (workspaceId: string, commitId: string) => {
const { data } = await apiRequest.get<CommitWithChanges>(
`/api/v1/pit/commits/${workspaceId}/${commitId}/changes`
);
return data;
};
export const fetchRollbackPreview = async (
folderId: string,
commitId: string,
envId: string,
projectId: string,
deepRollback: boolean,
secretPath: string
): Promise<RollbackPreview[]> => {
const { data } = await apiRequest.get<RollbackPreview[]>(
`/api/v1/pit/commits/${projectId}/${commitId}/compare`,
{
params: {
folderId,
envId,
deepRollback,
secretPath
}
}
);
return data;
};
const fetchRollback = async (
folderId: string,
commitId: string,
projectId: string,
deepRollback: boolean,
message?: string,
envId?: string
) => {
const { data } = await apiRequest.post<{ success: boolean }>(
`/api/v1/pit/commits/${projectId}/${commitId}/rollback`,
{
folderId,
deepRollback,
message,
envId
}
);
return data;
};
const fetchRevert = async (commitId: string, projectId: string) => {
const { data } = await apiRequest.post<{ success: boolean; message: string }>(
`/api/v1/pit/commits/${projectId}/${commitId}/revert`
);
return data;
};
export const useCommitRevert = ({
commitId,
projectId,
environment,
directory
}: {
commitId: string;
projectId: string;
environment: string;
directory: string;
}) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => fetchRevert(commitId, projectId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [
commitKeys.details({ workspaceId: projectId, commitId }),
commitKeys.history({ workspaceId: projectId, environment, directory }),
commitKeys.count({ workspaceId: projectId, environment, directory })
]
});
}
});
};
export const useCommitRollback = ({
workspaceId,
commitId,
folderId,
deepRollback,
environment,
directory,
envId
}: {
workspaceId: string;
commitId: string;
folderId: string;
deepRollback: boolean;
environment: string;
directory: string;
envId: string;
}) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (message: string) =>
fetchRollback(folderId, commitId, workspaceId, deepRollback, message, envId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [
commitKeys.details({ workspaceId, commitId }),
commitKeys.history({ workspaceId, environment, directory }),
commitKeys.count({ workspaceId, environment, directory })
]
});
}
});
};
export const useGetFolderCommitsCount = ({
workspaceId,
environment,
directory,
isPaused
}: {
workspaceId: string;
environment: string;
directory: string;
isPaused?: boolean;
}) =>
useQuery({
enabled: Boolean(workspaceId && environment) && !isPaused,
queryKey: commitKeys.count({ workspaceId, environment, directory }),
queryFn: () => fetchFolderCommitsCount({ workspaceId, environment, directory })
});
export const useGetFolderCommitHistory = ({
workspaceId,
environment,
directory
}: {
workspaceId: string;
environment: string;
directory: string;
}) => {
return useQuery({
queryKey: commitKeys.history({ workspaceId, environment, directory }),
queryFn: () => fetchFolderCommitHistory(workspaceId, environment, directory),
enabled: Boolean(workspaceId && environment)
});
};
export const useGetCommitDetails = (workspaceId: string, commitId: string) => {
return useQuery({
queryKey: commitKeys.details({ workspaceId, commitId }),
queryFn: () => fetchCommitDetails(workspaceId, commitId),
enabled: Boolean(workspaceId) && Boolean(commitId)
});
};
export const useGetRollbackPreview = (
folderId: string,
commitId: string,
envId: string,
projectId: string,
deepRollback: boolean,
secretPath: string
) => {
return useQuery({
queryKey: commitKeys.rollbackPreview({ folderId, commitId, envId, projectId, deepRollback }),
queryFn: () =>
fetchRollbackPreview(folderId, commitId, envId, projectId, deepRollback, secretPath),
enabled: Boolean(folderId) && Boolean(commitId)
});
};
@@ -0,0 +1,63 @@
import { SecretVersions } from "../types";
export type CommitHistoryItem = {
id: string;
commitId: string;
actorMetadata: {
id: string;
name?: string;
};
actorType: string;
message: string;
folderId: string;
envId: string;
createdAt: string;
updatedAt: string;
};
export type TFolderCommitChanges = {
id: string;
folderCommitId: string;
changeType: "add" | "delete";
isUpdate: boolean;
secretVersionId: string | null;
folderVersionId: string | null;
createdAt: string;
updatedAt: string;
versions: SecretVersions[];
secretKey?: string;
folderName?: string;
secretVersion?: string;
folderVersion?: string;
};
export type FolderReconstructedItem = {
type: string;
id: string;
versionId: string;
folderName?: string;
folderVersion?: number;
secretKey?: string;
secretVersion?: number;
};
export type CommitWithChanges = {
changes: CommitHistoryItem & {
changes: TFolderCommitChanges[];
};
};
export type RollbackChange = {
type: "folder" | "secret";
id: string;
versionId: string;
changeType: "create" | "update" | "delete";
commitId: string;
};
export type RollbackPreview = {
folderId: string;
folderName: string;
folderPath: string;
changes: RollbackChange[];
};
@@ -10,6 +10,7 @@ import {
import { apiRequest } from "@app/config/request";
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import { commitKeys } from "../folderCommits/queries";
import { secretSnapshotKeys } from "../secretSnapshots/queries";
import {
TCreateFolderDTO,
@@ -166,6 +167,9 @@ export const useCreateFolder = () => {
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId: projectId, environment, directory: path })
});
}
});
};
@@ -200,6 +204,12 @@ export const useUpdateFolder = () => {
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId: projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId: projectId, environment, directory: path })
});
}
});
};
@@ -234,6 +244,12 @@ export const useDeleteFolder = () => {
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId: projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId: projectId, environment, directory: path })
});
}
});
};
@@ -279,6 +295,20 @@ export const useUpdateFolderBatch = () => {
directory: folder.path
})
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({
workspaceId: projectId,
environment: folder.environment,
directory: folder.path
})
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({
workspaceId: projectId,
environment: folder.environment,
directory: folder.path
})
});
});
}
});
@@ -3,6 +3,7 @@ import { MutationOptions, useMutation, useQueryClient } from "@tanstack/react-qu
import { apiRequest } from "@app/config/request";
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import { commitKeys } from "../folderCommits/queries";
import { secretApprovalRequestKeys } from "../secretApprovalRequest/queries";
import { secretSnapshotKeys } from "../secretSnapshots/queries";
import { secretKeys } from "./queries";
@@ -59,6 +60,12 @@ export const useCreateSecretV3 = ({
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
},
...options
@@ -118,6 +125,12 @@ export const useUpdateSecretV3 = ({
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
},
...options
@@ -164,6 +177,12 @@ export const useDeleteSecretV3 = ({
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
},
...options
@@ -200,6 +219,12 @@ export const useCreateSecretBatch = ({
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
},
...options
@@ -236,6 +261,12 @@ export const useUpdateSecretBatch = ({
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
},
...options
@@ -274,6 +305,12 @@ export const useDeleteSecretBatch = ({
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
},
...options
@@ -347,6 +384,20 @@ export const useMoveSecrets = ({
directory: sourceSecretPath
})
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({
workspaceId: projectId,
environment: sourceEnvironment,
directory: sourceSecretPath
})
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({
workspaceId: projectId,
environment: sourceEnvironment,
directory: sourceSecretPath
})
});
queryClient.invalidateQueries({
queryKey: secretApprovalRequestKeys.count({ workspaceId: projectId })
});
@@ -11,6 +11,7 @@ import {
} from "@app/context";
import {
PermissionConditionOperators,
ProjectPermissionCommitsActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionGroupActions,
ProjectPermissionIdentityActions,
@@ -70,6 +71,11 @@ const SecretSyncPolicyActionSchema = z.object({
[ProjectPermissionSecretSyncActions.RemoveSecrets]: z.boolean().optional()
});
const CommitPolicyActionSchema = z.object({
[ProjectPermissionCommitsActions.Read]: z.boolean().optional(),
[ProjectPermissionCommitsActions.PerformRollback]: z.boolean().optional()
});
const SecretRotationPolicyActionSchema = z.object({
[ProjectPermissionSecretRotationActions.Read]: z.boolean().optional(),
[ProjectPermissionSecretRotationActions.ReadGeneratedCredentials]: z.boolean().optional(),
@@ -248,7 +254,8 @@ export const projectRoleFormSchema = z.object({
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Kmip]: KmipPolicyActionSchema.array().default([])
[ProjectPermissionSub.Kmip]: KmipPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Commits]: CommitPolicyActionSchema.array().default([])
})
.partial()
.optional()
@@ -382,7 +389,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.SshCertificateTemplates,
ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshHostGroups
ProjectPermissionSub.SshHostGroups,
ProjectPermissionSub.Commits
].includes(subject)
) {
// from above statement we are sure it won't be undefined
@@ -975,6 +983,13 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.Commits]: {
title: "Commits",
actions: [
{ label: "View", value: ProjectPermissionCommitsActions.Read },
{ label: "Perform Rollback", value: ProjectPermissionCommitsActions.PerformRollback }
]
},
[ProjectPermissionSub.Tags]: {
title: "Tags",
actions: [
@@ -0,0 +1,81 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { ROUTE_PATHS } from "@app/const/routes";
import { useWorkspace } from "@app/context";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { CommitDetailsTab } from "./components/CommitDetailsTab";
export const CommitDetailsPage = () => {
const { t } = useTranslation();
const envSlug = useParams({
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id,
select: (el) => el.environment
});
const selectedCommitId = useParams({
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id,
select: (el) => el.commitId
});
const folderId = useParams({
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id,
select: (el) => el.folderId
});
const { currentWorkspace } = useWorkspace();
const navigate = useNavigate();
const routerQueryParams: { secretPath?: string } = useSearch({
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id
});
const secretPath = (routerQueryParams.secretPath as string) || "/";
const handleGoBackToHistory = () => {
navigate({
to: `/${ProjectType.SecretManager}/$projectId/commits/$environment/$folderId` as const,
params: {
projectId: currentWorkspace.id,
folderId,
environment: envSlug
},
search: (query) => ({
...query,
secretPath
})
});
};
const handleGoToRollbackPreview = () => {
navigate({
to: `/${ProjectType.SecretManager}/$projectId/commits/$environment/$folderId/$commitId/restore` as const,
params: {
projectId: currentWorkspace.id,
folderId,
environment: envSlug,
commitId: selectedCommitId
},
search: (query) => ({
...query,
secretPath
})
});
};
return (
<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]">
<CommitDetailsTab
selectedCommitId={selectedCommitId}
workspaceId={currentWorkspace.id}
goBackToHistory={handleGoBackToHistory}
envSlug={envSlug}
goToRollbackPreview={handleGoToRollbackPreview}
/>
</div>
</div>
);
};
@@ -0,0 +1,327 @@
import { useEffect, useState } from "react";
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DropdownMenuItem } from "@radix-ui/react-dropdown-menu";
import { useSearch } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import {
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
IconButton,
Spinner
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { usePopUp } from "@app/hooks";
import { CommitWithChanges } from "@app/hooks/api/folderCommits";
import { useCommitRevert, useGetCommitDetails } from "@app/hooks/api/folderCommits/queries";
import { SecretVersionDiffView } from "../SecretVersionDiffView";
import { MergedItem } from "./types";
const formatDisplayDate = (dateString: string): string => {
try {
const date = new Date(dateString);
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "numeric",
hour12: true
};
return new Intl.DateTimeFormat("en-US", options).format(date);
} catch {
return dateString;
}
};
export const CommitDetailsTab = ({
selectedCommitId,
workspaceId,
envSlug,
goBackToHistory,
goToRollbackPreview
}: {
selectedCommitId: string;
workspaceId: string;
envSlug: string;
goBackToHistory: () => void;
goToRollbackPreview: () => void;
}): JSX.Element => {
// State for tracking collapsed items (empty by default means all are expanded)
const [collapsedItems, setCollapsedItems] = useState<Record<string, boolean>>({});
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"revertChanges"
] as const);
const { data: commitDetails, isLoading } = useGetCommitDetails(workspaceId, selectedCommitId);
const routerQueryParams: { secretPath?: string } = useSearch({
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id
});
const secretPath = (routerQueryParams.secretPath as string) || "/";
const { mutateAsync: revert } = useCommitRevert({
commitId: selectedCommitId,
projectId: workspaceId,
environment: envSlug,
directory: secretPath
});
useEffect(() => {
setCollapsedItems({});
}, [selectedCommitId]);
const toggleItemCollapsed = (itemId: string): void => {
setCollapsedItems((prev) => ({ ...prev, [itemId]: !prev[itemId] }));
};
const handleRevertChanges = async (): Promise<void> => {
const response = await revert();
if (!response.success) {
createNotification({
type: "error",
text: response.message
});
return;
}
createNotification({
type: "success",
text: response.message
});
handlePopUpClose("revertChanges");
goBackToHistory();
};
// If no commit is selected or data is loading, show appropriate message
if (!selectedCommitId) {
return (
<div className="flex h-64 items-center justify-center">
<p className="text-gray-400">Select a commit to view details</p>
</div>
);
}
if (isLoading) {
return (
<div className="flex h-64 items-center justify-center">
<Spinner size="lg" />
</div>
);
}
if (!commitDetails) {
return (
<div className="flex h-64 items-center justify-center">
<p className="text-gray-400">No details found for this commit</p>
</div>
);
}
// Parse the commit details if it's a string
let parsedCommitDetails: CommitWithChanges;
try {
parsedCommitDetails =
typeof commitDetails === "string" ? JSON.parse(commitDetails) : commitDetails;
} catch (error) {
console.error("Failed to parse commit details:", error);
return (
<div className="flex h-64 items-center justify-center">
<p className="text-gray-400">Error parsing commit details</p>
</div>
);
}
// Get all changes from the commit
const commitChanges = parsedCommitDetails.changes?.changes || [];
// Separate changes by type
const addedChanges = commitChanges.filter((c) => c.changeType === "add" && !c.isUpdate);
const updatedChanges = commitChanges.filter((c) => c.changeType === "add" && c.isUpdate);
const deletedChanges = commitChanges.filter((c) => c.changeType === "delete");
// Create merged item list from changes only
const changedItems: MergedItem[] = [];
// Add items from added changes
addedChanges.forEach((change) => {
changedItems.push({
id: change.id,
type: change.secretVersionId || change.secretKey ? "secret" : "folder",
versionId: change.secretVersionId || change.id,
folderName: change.folderName,
folderVersion: change.folderVersion,
secretKey: change.secretKey,
secretVersion: change.secretVersion,
isAdded: true,
versions: change.versions,
changeId: change.id
});
});
// Add items from updated changes
updatedChanges.forEach((change) => {
changedItems.push({
id: change.id,
type: change.secretVersionId || change.secretKey ? "secret" : "folder",
versionId: change.secretVersionId || change.id,
folderName: change.folderName,
folderVersion: change.folderVersion,
secretKey: change.secretKey,
secretVersion: change.secretVersion,
isUpdated: true,
versions: change.versions,
changeId: change.id
});
});
// Add deleted items
deletedChanges.forEach((change) => {
changedItems.push({
id: change.id,
type: change.secretVersionId || change.secretKey ? "secret" : "folder",
secretKey: change.secretKey,
folderName: change.folderName,
secretVersion: change.secretVersion,
folderVersion: change.folderVersion,
isDeleted: true,
versions: change.versions,
changeId: change.id
});
});
// Sort items: deleted first, then folders, then secrets, all alphabetically
const sortedChangedItems = [...changedItems].sort((a, b) => {
// First sort deleted items to the top
if (a.isDeleted !== b.isDeleted) {
return a.isDeleted ? -1 : 1;
}
// Then sort by type (folders before secrets)
if (a.type !== b.type) {
return a.type === "folder" ? -1 : 1;
}
// Finally sort alphabetically by name
const aName = a.type === "folder" ? a.folderName || "" : a.secretKey || "";
const bName = b.type === "folder" ? b.folderName || "" : b.secretKey || "";
return aName.localeCompare(bName);
});
// Render an item from the merged list
const renderMergedItem = (item: MergedItem): JSX.Element => {
return (
<div key={item.id} className="mb-2">
<SecretVersionDiffView
item={item}
isCollapsed={collapsedItems[item.id]}
onToggleCollapse={(id) => toggleItemCollapsed(id)}
/>
</div>
);
};
// Format actor display
const actorDisplay =
parsedCommitDetails.changes?.actorMetadata?.name ||
parsedCommitDetails.changes?.actorType ||
"Unknown";
return (
<div className="w-full">
<div>
<div className="flex justify-between pb-2">
<div>
<div>
<div className="flex items-center">
<h1 className="mr-4 text-3xl font-semibold text-white">
{parsedCommitDetails.changes?.message || "No message"}
</h1>
</div>
</div>
<div className="font-small mb-4 mt-2 flex items-center text-sm">
<p>
<span> Commited by </span>
<b>{actorDisplay}</b>
<span> on </span>
<b>
{formatDisplayDate(
parsedCommitDetails.changes?.createdAt || new Date().toISOString()
)}
</b>
</p>
</div>
</div>
<div className="flex items-center justify-start">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="commit-options"
variant="outline_bg"
className="h-10 rounded border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-sm font-medium"
>
<p className="mr-2">Restore Options</p>
<FontAwesomeIcon icon={faAngleDown} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
sideOffset={2}
className="animate-in fade-in-50 zoom-in-95 min-w-[200px] p-1"
style={{ marginTop: "0" }}
>
<DropdownMenuItem
className="h-10 w-full cursor-pointer px-3 py-2 text-sm transition-colors hover:bg-mineshaft-700"
onClick={() => goToRollbackPreview()}
>
<div className="flex w-full items-center justify-start">
<span>Restore entire folder</span>
</div>
</DropdownMenuItem>
<DropdownMenuItem
className="h-10 w-full cursor-pointer px-3 py-2 text-sm transition-colors hover:bg-mineshaft-700"
onClick={() => handlePopUpOpen("revertChanges")}
>
<div className="flex w-full items-center justify-start">
<span>Revert commit changes</span>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="py-2">
<div className="overflow-hidden">
<div className="space-y-2">
{sortedChangedItems.length > 0 ? (
sortedChangedItems.map((item) => renderMergedItem(item))
) : (
<div className="flex h-32 items-center justify-center rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<p className="text-gray-400">No changed items found</p>
</div>
)}
</div>
</div>
</div>
</div>
<DeleteActionModal
isOpen={popUp.revertChanges.isOpen}
deleteKey="revert"
title="Are you sure you want to revert changes from this commit?"
subTitle="This action will undo all changes made in this commit."
onChange={(isOpen) => handlePopUpToggle("revertChanges", isOpen)}
onDeleteApproved={handleRevertChanges}
buttonText="Revert Changes"
/>
</div>
);
};
@@ -0,0 +1 @@
export { CommitDetailsTab } from "./CommitDetailsTab";
@@ -0,0 +1,14 @@
export interface MergedItem {
id: string;
type: "secret" | "folder";
versionId?: string;
folderName?: string;
folderVersion?: string;
secretKey?: string;
secretVersion?: string;
isAdded?: boolean;
isUpdated?: boolean;
isDeleted?: boolean;
versions?: any[];
changeId: string;
}
@@ -0,0 +1,377 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { useEffect, useState } from "react";
import { faFolder, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import {
Button,
DeleteActionModal,
Input,
PageHeader,
Spinner,
Switch,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useCommitRollback, useGetRollbackPreview } from "@app/hooks/api/folderCommits/queries";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { SecretVersionDiffView } from "../SecretVersionDiffView";
interface Version {
// Common fields
id?: string;
version: number;
createdAt?: string;
updatedAt?: string;
// Secret-specific fields
secretKey?: string;
secretValue?: string;
secretComment?: string;
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretReminderRecipients?: string[];
tags?: string[];
metadata?: Record<string, unknown>;
// Folder-specific fields
name?: string;
envId?: string;
folderId?: string;
[key: string]: any;
}
interface RollbackChange {
type: "secret" | "folder";
id: string;
versionId: string;
fromVersion?: number;
changeType: "create" | "update" | "delete";
commitId: string;
secretKey?: string;
secretVersion?: number;
folderName?: string;
folderVersion?: number;
versions?: Version[];
createdAt?: string;
}
interface FolderChanges {
folderId: string;
folderName: string;
folderPath: string;
changes: RollbackChange[];
}
export const RollbackPreviewTab = (): JSX.Element => {
const [deepRollback, setDeepRollback] = useState<boolean>(false);
const [message, setMessage] = useState<string>("");
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
const { currentWorkspace } = useWorkspace();
const envSlug = useParams({
from: ROUTE_PATHS.SecretManager.RollbackPreviewPage.id,
select: (el) => el.environment
});
const selectedCommitId = useParams({
from: ROUTE_PATHS.SecretManager.RollbackPreviewPage.id,
select: (el) => el.commitId
});
const folderId = useParams({
from: ROUTE_PATHS.SecretManager.RollbackPreviewPage.id,
select: (el) => el.folderId
});
const navigate = useNavigate();
const routerQueryParams = useSearch({
from: ROUTE_PATHS.SecretManager.RollbackPreviewPage.id
});
const secretPath = (routerQueryParams.secretPath as string) || "/";
const goBackToHistory = () => {
navigate({
to: `/${ProjectType.SecretManager}/$projectId/commits/$environment/$folderId` as const,
params: {
projectId: currentWorkspace.id,
folderId,
environment: envSlug
},
search: (query) => ({
...query,
secretPath
})
});
};
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"rollbackConfirm"
] as const);
const { mutateAsync: rollback } = useCommitRollback({
workspaceId: currentWorkspace.id,
commitId: selectedCommitId,
folderId,
deepRollback,
environment: envSlug,
directory: secretPath,
envId: currentWorkspace.environments.find((e) => e.slug === envSlug)?.id || ""
});
const { data: rollbackChangesNested, isLoading } = useGetRollbackPreview(
folderId,
selectedCommitId,
currentWorkspace.environments.find((e) => e.slug === envSlug)?.id || "",
currentWorkspace.id,
deepRollback,
secretPath
);
const handleRollback = async (): Promise<void> => {
try {
await rollback(message);
createNotification({
type: "success",
text: "Rollback completed successfully"
});
handlePopUpClose("rollbackConfirm");
goBackToHistory();
} catch (error) {
createNotification({
type: "error",
text: error instanceof Error ? error.message : "Failed to rollback changes"
});
}
};
const folderChanges: FolderChanges[] = rollbackChangesNested || [];
const currentFolderChanges: FolderChanges = folderChanges.find(
(folder) => folder.folderId === folderId
) || {
folderId: folderId || "",
folderName: "Current Folder",
folderPath: secretPath,
changes: []
};
const nestedFolderChanges: FolderChanges[] = folderChanges.filter(
(folder) => folder.folderId !== folderId
);
useEffect(() => {
// Select the current folder by default
if (folderChanges.length > 0) {
setSelectedFolderId(currentFolderChanges.folderId);
}
}, [folderChanges, currentFolderChanges.folderId]);
if (!selectedCommitId) {
return (
<div className="flex h-64 items-center justify-center">
<p className="text-gray-400">Select a commit to view rollback preview</p>
</div>
);
}
if (isLoading) {
return (
<div className="flex h-64 items-center justify-center">
<Spinner size="lg" />
</div>
);
}
const renderSidebar = (): JSX.Element => {
return (
<div className="h-[70vh] w-72 overflow-y-auto bg-mineshaft-900">
<div
className={`cursor-pointer border-b border-mineshaft-600 ${
selectedFolderId === currentFolderChanges.folderId ? "bg-mineshaft-700" : ""
}`}
onClick={() => setSelectedFolderId(currentFolderChanges.folderId)}
>
<div className="flex items-center justify-between px-4 py-3">
<div className="flex items-center">
<FontAwesomeIcon icon={faFolder} className="mr-2 text-yellow-500" />
<span className="font-medium text-white">
{currentFolderChanges.folderPath || currentFolderChanges.folderName}
</span>
</div>
</div>
</div>
{deepRollback && nestedFolderChanges.length > 0 && (
<>
<div className="border-b border-mineshaft-600 bg-mineshaft-800 px-4 py-2">
<span className="text-sm font-semibold text-white">Affected Child Folders</span>
</div>
{nestedFolderChanges.map((folder) => (
<div
key={folder.folderId}
className={`cursor-pointer border-b border-mineshaft-600 ${
selectedFolderId === folder.folderId ? "bg-mineshaft-700" : ""
}`}
onClick={() => setSelectedFolderId(folder.folderId)}
>
<div className="flex items-center justify-between px-4 py-2">
<div className="flex items-center">
<FontAwesomeIcon icon={faFolder} className="mr-2 text-yellow-500" size="sm" />
<span className="max-w-[150px] truncate text-sm font-medium text-white">
{folder.folderPath || folder.folderName}
</span>
</div>
</div>
</div>
))}
</>
)}
</div>
);
};
const getSelectedFolderChanges = (): RollbackChange[] => {
if (!selectedFolderId) return [];
const folder = folderChanges.find((f) => f.folderId === selectedFolderId);
return folder?.changes || [];
};
const renderMainContent = (): JSX.Element => {
const selectedFolderChanges = getSelectedFolderChanges();
const selectedFolder = folderChanges.find((f) => f.folderId === selectedFolderId);
if (!selectedFolder || selectedFolderChanges.length === 0) {
return (
<div className="flex h-[70vh] w-full items-center justify-center border border-mineshaft-600">
<p className="text-gray-400">No changes in selected folder</p>
</div>
);
}
return (
<div className="h-[70vh] w-full overflow-y-auto border-l border-mineshaft-600">
<div className="space-y-4 p-4">
{selectedFolderChanges.map((change) => (
<div key={change.id} className="mb-4">
<SecretVersionDiffView
item={{
id: change.id,
type: change.type,
isAdded: change.changeType === "create",
isDeleted: change.changeType === "delete",
isUpdated: change.changeType === "update",
versions: change.versions,
isRollback: true,
secretKey: change.secretKey,
folderName: change.folderName
}}
/>
</div>
))}
</div>
</div>
);
};
return (
<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]">
<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."
/>
{folderChanges.length > 0 ? (
<div className="flex w-full border border-mineshaft-600">
{renderSidebar()}
{renderMainContent()}
</div>
) : (
<div className="flex h-32 items-center justify-center">
<p className="text-gray-400">No changes will be applied with this restore</p>
</div>
)}
<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">
<Tooltip content="Enable/Disable deep rollback to restore all nested folders to their previous state">
<FontAwesomeIcon icon={faInfoCircle} className="mr-2 text-mineshaft-400" />
</Tooltip>
<Switch
className="ml-2 bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/50"
thumbClassName="bg-mineshaft-800"
isChecked={deepRollback}
onCheckedChange={setDeepRollback}
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">
<div className="flex-1">
<Input
placeholder="Restore Message"
value={message}
onChange={(e) => setMessage(e.target.value)}
className="w-full border-mineshaft-500 bg-mineshaft-700 text-sm"
/>
</div>
<div className="flex gap-2">
<Button
onClick={() => {
if (!message) {
createNotification({
type: "error",
text: "Please enter a restore message"
});
return;
}
handlePopUpOpen("rollbackConfirm");
}}
colorSchema="primary"
className="px-6 py-2"
>
Restore
</Button>
</div>
</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
? "This will revert all changes 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."
}
Any changes made after this commit will be permanently removed.
`}
onChange={(isOpen) => handlePopUpToggle("rollbackConfirm", isOpen)}
onDeleteApproved={handleRollback}
buttonText="Restore"
/>
</div>{" "}
</div>{" "}
</div>
);
};
@@ -0,0 +1 @@
export { RollbackPreviewTab } from "./RollbackPreviewTab";
@@ -0,0 +1,91 @@
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { SecretDashboardPathBreadcrumb } from "@app/components/navigation/SecretDashboardPathBreadcrumb";
import { BreadcrumbTypes } from "@app/components/v2";
import { RollbackPreviewTab } from "./RollbackPreviewTab";
const RollbackPreviewTabQueryParamsSchema = z.object({
secretPath: z.string().catch("/")
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
)({
component: RollbackPreviewTab,
validateSearch: zodValidator(RollbackPreviewTabQueryParamsSchema),
search: {
middlewares: [stripSearchParams({ secretPath: "/" })]
},
beforeLoad: ({ context, params, search }) => {
const secretPathSegments = search.secretPath.split("/").filter(Boolean);
return {
breadcrumbs: [
...context.breadcrumbs,
{
type: BreadcrumbTypes.Dropdown,
label:
context.project.environments.find((el) => el.slug === params.environment)?.name || "",
dropdownTitle: "Environments",
links: context.project.environments.map((el) => ({
label: el.name,
link: linkOptions({
to: "/secret-manager/$projectId/secrets/$envSlug",
params: {
projectId: params.projectId,
envSlug: el.slug
}
})
}))
},
...secretPathSegments.map((_, index) => ({
type: BreadcrumbTypes.Component,
component: () => (
<SecretDashboardPathBreadcrumb
secretPathSegments={secretPathSegments}
selectedPathSegmentIndex={index}
environmentSlug={params.environment}
projectId={params.projectId}
disableCopy
/>
)
})),
{
label: "Commits",
link: linkOptions({
to: "/secret-manager/$projectId/commits/$environment/$folderId",
params: {
projectId: params.projectId,
environment: params.environment,
folderId: params.folderId
},
search: {
secretPath: search.secretPath
}
})
},
{
label: params.commitId,
link: linkOptions({
to: "/secret-manager/$projectId/commits/$environment/$folderId/$commitId",
params: {
projectId: params.projectId,
environment: params.environment,
folderId: params.folderId,
commitId: params.commitId
},
search: {
secretPath: search.secretPath
}
})
},
{
label: "Restore"
}
]
};
}
});
@@ -0,0 +1,585 @@
/* eslint-disable react/prop-types */
import { useRef, useState } from "react";
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export interface Version {
id?: string;
version: number;
// Secret-specific fields
secretKey?: string;
secretValue?: string;
secretComment?: string;
skipMultilineEncoding?: boolean;
// Folder-specific fields
name?: string;
// Allow other properties
[key: string]: any;
}
export interface DiffViewItem {
type: "secret" | "folder";
isAdded?: boolean;
isDeleted?: boolean;
isUpdated?: boolean;
versions?: Version[];
isRollback?: boolean;
id: string;
secretKey?: string;
folderName?: string;
}
interface SecretVersionDiffViewProps {
item: DiffViewItem;
isCollapsed?: boolean;
onToggleCollapse?: (id: string) => void;
showHeader?: boolean;
customHeader?: JSX.Element;
}
const highlightChangedFields = (
json: any,
changedFields: Set<string>,
isOldVersion: boolean
): JSX.Element => {
const lines = JSON.stringify(json, null, 2).split("\n");
return (
<div className="font-mono text-sm">
{lines.map((line, idx) => {
const fieldMatch = line.match(/"([^"]+)":/);
if (fieldMatch && changedFields.has(fieldMatch[1])) {
// Check for different value types and highlight them
// 1. String values: "field": "value"
const stringMatch = line.match(/: "([^"]*)"(,?)$/);
if (stringMatch) {
const beforeValue = line.substring(0, line.indexOf(': "'));
const value = `"${stringMatch[1]}"`;
const afterValue = stringMatch[2] || "";
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>
{beforeValue}:{" "}
<span
className={`rounded border border-none px-1 ${isOldVersion ? "bg-red-900" : "bg-green-900"}`}
>
{value}
</span>
{afterValue}
</div>
</div>
);
}
// 2. Number values: "field": 123
const numberMatch = line.match(/: (-?\d+(?:\.\d+)?)(,?)$/);
if (numberMatch) {
const beforeValue = line.substring(0, line.indexOf(": ") + 2);
const value = numberMatch[1];
const afterValue = numberMatch[2] || "";
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>
{beforeValue.substring(0, beforeValue.length - 2)}:{" "}
<span
className={`rounded border border-none px-1 ${isOldVersion ? "bg-red-900" : "bg-green-900"}`}
>
{value}
</span>
{afterValue}
</div>
</div>
);
}
// 3. Null values: "field": null
const nullMatch = line.match(/: (null)(,?)$/);
if (nullMatch) {
const beforeValue = line.substring(0, line.indexOf(": ") + 2);
const value = nullMatch[1];
const afterValue = nullMatch[2] || "";
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>
{beforeValue.substring(0, beforeValue.length - 2)}:{" "}
<span
className={`rounded border border-none px-1 ${isOldVersion ? "bg-red-900" : "bg-green-900"}`}
>
{value}
</span>
{afterValue}
</div>
</div>
);
}
// 4. Boolean values: "field": true|false
const booleanMatch = line.match(/: (true|false)(,?)$/);
if (booleanMatch) {
const beforeValue = line.substring(0, line.indexOf(": ") + 2);
const value = booleanMatch[1];
const afterValue = booleanMatch[2] || "";
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>
{beforeValue.substring(0, beforeValue.length - 2)}:{" "}
<span
className={`rounded border border-none px-1 ${isOldVersion ? "bg-red-900" : "bg-green-900"}`}
>
{value}
</span>
{afterValue}
</div>
</div>
);
}
// 5. Array values: Handle the first line of an array - "field": [
const arrayStartMatch = line.match(/: \[(,?)$/);
if (arrayStartMatch) {
// This is the start of an array, highlight the whole line
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>{line}</div>
</div>
);
}
// 6. Empty Array values: "field": []
const emptyArrayMatch = line.match(/: \[\](,?)$/);
if (emptyArrayMatch) {
const beforeValue = line.substring(0, line.indexOf(": ") + 2);
const value = "[]";
const afterValue = emptyArrayMatch[1] || "";
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>
{beforeValue.substring(0, beforeValue.length - 2)}:{" "}
<span
className={`rounded border border-none px-1 ${isOldVersion ? "bg-red-900" : "bg-green-900"}`}
>
{value}
</span>
{afterValue}
</div>
</div>
);
}
// Check if this is part of an array or object that belongs to a changed field
// This handles array items, closing brackets, and other complex structure contents
const belongsToChangedField = () => {
// If we're inside an array or object of a changed field, highlight it
let openBrackets = 0;
let openBraces = 0;
let currentFieldName = null;
for (let i = idx - 1; i >= 0; i -= 1) {
const prevLine = lines[i];
// Count brackets and braces to track nesting
openBrackets += (prevLine.match(/\[/g) || []).length;
openBrackets -= (prevLine.match(/\]/g) || []).length;
openBraces += (prevLine.match(/{/g) || []).length;
openBraces -= (prevLine.match(/}/g) || []).length;
// If we find a field and we're still inside its value, check if it's a changed field
const fieldNameMatch = prevLine.match(/"([^"]+)":/);
if (fieldNameMatch && (openBrackets > 0 || openBraces > 0)) {
[, currentFieldName] = fieldNameMatch;
return changedFields.has(currentFieldName);
}
// If we've reached the root level, stop looking
if (openBrackets <= 0 && openBraces <= 0) {
return false;
}
}
return false;
};
// Regular line with changed field or part of a changed complex structure
if (belongsToChangedField()) {
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>{line}</div>
</div>
);
}
// Simple fallback for any other cases of changed fields
return (
<div
key={`${idx + 1}`}
className={
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
}
>
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
<div>{line}</div>
</div>
);
}
// Unchanged lines
return (
<div key={`${idx + 1}`} className="flex">
<div className="w-4 flex-shrink-0" />
<div>{line}</div>
</div>
);
})}
</div>
);
};
// Helper: Format added JSON with + in a separate column
const formatAddedJson = (json: any): JSX.Element => {
const lines = JSON.stringify(json, null, 2).split("\n");
return (
<div className="font-mono text-sm text-green-300">
{lines.map((line, idx) => (
<div key={`${idx + 1}`} className="flex">
<div className="w-4 flex-shrink-0">+</div>
<div>{line}</div>
</div>
))}
</div>
);
};
// Helper: Format deleted JSON with - in a separate column
const formatDeletedJson = (json: any): JSX.Element => {
const lines = JSON.stringify(json, null, 2).split("\n");
return (
<div className="font-mono text-sm text-red-300">
{lines.map((line, idx) => (
<div key={`${idx + 1}`} className="flex">
<div className="w-4 flex-shrink-0">-</div>
<div>{line}</div>
</div>
))}
</div>
);
};
// Helper: Get differences between secret versions
export const getVersionDifferences = (versions: Version[]) => {
if (!versions || versions.length === 0) return [];
// Sort versions by version number (descending)
const sortedVersions = [...versions].sort((a, b) => b.version - a.version);
const newVersion = sortedVersions[0];
// Fields to process
const fieldsToProcess = [
"secretKey",
"secretValue",
"secretComment",
"skipMultilineEncoding",
"secretReminderRepeatDays",
"secretReminderNote",
"metadata",
"tags",
"secretReminderRecipients",
"name"
];
// If only one version exists
if (sortedVersions.length === 1) {
return fieldsToProcess.reduce(
(differences, field) => {
if (newVersion[field] !== undefined && newVersion[field] !== null) {
let newVal = newVersion[field];
if (field === "tags" && Array.isArray(newVersion[field])) {
if (newVersion[field].length > 0 && typeof newVersion[field][0] === "object") {
newVal = newVersion[field].map((tag) => tag.name).join(", ");
} else if (Array.isArray(newVersion[field])) {
newVal = newVersion[field].join(", ");
}
}
differences.push({
field,
oldValue: null,
newValue: newVal
});
}
return differences;
},
[] as { field: string; oldValue: any; newValue: any }[]
);
}
// Otherwise, compare the two versions
const oldVersion = sortedVersions[1];
return fieldsToProcess.reduce(
(differences, field) => {
if (JSON.stringify(oldVersion[field]) !== JSON.stringify(newVersion[field])) {
let oldVal = oldVersion[field];
let newVal = newVersion[field];
if (field === "tags" && Array.isArray(oldVersion[field])) {
if (oldVersion[field].length > 0 && typeof oldVersion[field][0] === "object") {
oldVal = oldVersion[field].map((tag) => tag.name).join(", ");
} else if (Array.isArray(oldVersion[field])) {
oldVal = oldVersion[field].join(", ");
}
}
if (field === "tags" && Array.isArray(newVersion[field])) {
if (newVersion[field].length > 0 && typeof newVersion[field][0] === "object") {
newVal = newVersion[field].map((tag) => tag.name).join(", ");
} else if (Array.isArray(newVersion[field])) {
newVal = newVersion[field].join(", ");
}
}
differences.push({
field,
oldValue: oldVal,
newValue: newVal
});
}
return differences;
},
[] as { field: string; oldValue: any; newValue: any }[]
);
};
// Helper: Get differences between folder versions
export const getFolderDifferences = (versions: Version[]) => {
if (!versions || versions.length === 0) return [];
const sortedVersions = [...versions].sort((a, b) => b.version - a.version);
const newVersion = sortedVersions[0];
if (sortedVersions.length === 1) {
return [
{
field: "folderName",
oldValue: null,
newValue: newVersion.name
}
];
}
const oldVersion = sortedVersions[1];
return [
{
field: "folderName",
oldValue: oldVersion.name,
newValue: newVersion.name
}
];
};
export const SecretVersionDiffView: React.FC<SecretVersionDiffViewProps> = ({
item,
isCollapsed = false,
onToggleCollapse,
showHeader = true,
customHeader
}) => {
const oldContainerRef = useRef<HTMLDivElement>(null);
const newContainerRef = useRef<HTMLDivElement>(null);
const [isScrollingSynced, setIsScrollingSynced] = useState(false);
const [internalCollapsed, setInternalCollapsed] = useState(isCollapsed);
const collapsed = onToggleCollapse ? isCollapsed : internalCollapsed;
const handleToggle = () => {
if (onToggleCollapse && item.id) {
onToggleCollapse(item.id);
} else {
setInternalCollapsed(!internalCollapsed);
}
};
if (!item.versions) {
return <div className="px-6 py-3 text-gray-400">No details available</div>;
}
const differences = getVersionDifferences(item.versions);
if (differences.length === 0) {
return <div className="px-6 py-3 text-gray-400">No details available</div>;
}
const changedFields = new Set<string>();
differences.forEach((diff) => {
if (JSON.stringify(diff.oldValue) !== JSON.stringify(diff.newValue)) {
changedFields.add(diff.field);
}
});
const handleScroll = (container: "old" | "new") => {
if (isScrollingSynced) return;
setIsScrollingSynced(true);
if (container === "old" && oldContainerRef.current && newContainerRef.current) {
newContainerRef.current.scrollTop = oldContainerRef.current.scrollTop;
} else if (container === "new" && oldContainerRef.current && newContainerRef.current) {
oldContainerRef.current.scrollTop = newContainerRef.current.scrollTop;
}
setTimeout(() => setIsScrollingSynced(false), 50);
};
const sortedVersions = [...item.versions].sort((a, b) => b.version - a.version);
let oldVersion = null;
let newVersion = null;
let oldVersionContent = null;
let newVersionContent = null;
if (item.isUpdated) {
if (item.isRollback) {
[oldVersion, newVersion] = sortedVersions;
} else {
[newVersion, oldVersion] = sortedVersions;
}
oldVersionContent = highlightChangedFields(oldVersion, changedFields, true);
newVersionContent = highlightChangedFields(newVersion, changedFields, false);
} else if (item.isAdded) {
[newVersion] = sortedVersions;
oldVersionContent = <div className="italic text-gray-400" />;
newVersionContent = formatAddedJson(newVersion);
} else if (item.isDeleted) {
[oldVersion] = sortedVersions;
oldVersionContent = formatDeletedJson(oldVersion);
newVersionContent = <div className="italic text-gray-400" />;
}
const renderHeader = () => {
if (customHeader) {
return customHeader;
}
const isSecret = item.type === "secret";
const key = isSecret ? item.secretKey || "" : item.folderName || "";
let textStyle = "text-white";
let changeBadge = null;
if (item.isDeleted) {
textStyle = "line-through text-red-300";
changeBadge = (
<span className="ml-2 rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
{isSecret ? "Secret" : "Folder"} Deleted
</span>
);
} else if (item.isAdded) {
changeBadge = (
<span className="ml-2 rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
{isSecret ? "Secret" : "Folder"} Added
</span>
);
} else if (item.isUpdated) {
changeBadge = (
<span className="ml-2 rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
{isSecret ? "Secret" : "Folder"} Updated
</span>
);
}
return (
<div
className="flex cursor-pointer items-center justify-between p-4 hover:bg-mineshaft-700"
onClick={handleToggle}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleToggle();
e.preventDefault();
}
}}
role="button"
tabIndex={0}
aria-expanded={!collapsed}
>
<div className="flex items-center">
<span className={textStyle}>{key}</span>
{changeBadge}
</div>
<FontAwesomeIcon icon={collapsed ? faChevronDown : faChevronUp} className="text-gray-400" />
</div>
);
};
return (
<div className="overflow-hidden rounded-lg border border-mineshaft-600 bg-mineshaft-800">
{showHeader && renderHeader()}
{!collapsed && (
<div className="border-t border-mineshaft-700 bg-mineshaft-900 px-6 py-4">
<div className="grid grid-cols-2 gap-4">
<div
ref={oldContainerRef}
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
onScroll={() => handleScroll("old")}
>
{oldVersionContent}
</div>
<div
ref={newContainerRef}
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
onScroll={() => handleScroll("new")}
>
{newVersionContent}
</div>
</div>
</div>
)}
</div>
);
};
@@ -0,0 +1 @@
export { SecretVersionDiffView } from "./SecretVersionDiffView";
@@ -0,0 +1 @@
export { CommitDetailsPage } from "./CommitDetailsPage";
@@ -0,0 +1,76 @@
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { SecretDashboardPathBreadcrumb } from "@app/components/navigation/SecretDashboardPathBreadcrumb";
import { BreadcrumbTypes } from "@app/components/v2";
import { CommitDetailsPage } from "./CommitDetailsPage";
const CommitDetailsPageQueryParamsSchema = z.object({
secretPath: z.string().catch("/")
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/"
)({
component: CommitDetailsPage,
validateSearch: zodValidator(CommitDetailsPageQueryParamsSchema),
search: {
middlewares: [stripSearchParams({ secretPath: "/" })]
},
beforeLoad: ({ context, params, search }) => {
const secretPathSegments = search.secretPath.split("/").filter(Boolean);
return {
breadcrumbs: [
...context.breadcrumbs,
{
type: BreadcrumbTypes.Dropdown,
label:
context.project.environments.find((el) => el.slug === params.environment)?.name || "",
dropdownTitle: "Environments",
links: context.project.environments.map((el) => ({
label: el.name,
link: linkOptions({
to: "/secret-manager/$projectId/secrets/$envSlug",
params: {
projectId: params.projectId,
envSlug: el.slug
}
})
}))
},
...secretPathSegments.map((_, index) => ({
type: BreadcrumbTypes.Component,
component: () => (
<SecretDashboardPathBreadcrumb
secretPathSegments={secretPathSegments}
selectedPathSegmentIndex={index}
environmentSlug={params.environment}
projectId={params.projectId}
disableCopy
/>
)
})),
{
label: "Commits",
link: linkOptions({
to: "/secret-manager/$projectId/commits/$environment/$folderId",
params: {
projectId: params.projectId,
environment: params.environment,
folderId: params.folderId
},
search: {
secretPath: search.secretPath
}
})
},
{
label: params.commitId
}
]
};
}
});
@@ -0,0 +1,65 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { PageHeader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useWorkspace } from "@app/context";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { CommitHistoryTab } from "./components/CommitHistoryTab";
export const CommitsPage = () => {
const { t } = useTranslation();
const envSlug = useParams({
from: ROUTE_PATHS.SecretManager.CommitsPage.id,
select: (el) => el.environment
});
const { currentWorkspace } = useWorkspace();
const navigate = useNavigate();
const folderId = useParams({
from: ROUTE_PATHS.SecretManager.CommitsPage.id,
select: (el) => el.folderId
});
const routerQueryParams: { secretPath?: string } = useSearch({
from: ROUTE_PATHS.SecretManager.CommitsPage.id
});
const secretPath = routerQueryParams?.secretPath || "/";
const handleSelectCommit = (commitId: string) => {
navigate({
to: `/${ProjectType.SecretManager}/$projectId/commits/$environment/$folderId/$commitId` as const,
params: {
projectId: currentWorkspace.id,
folderId,
environment: envSlug,
commitId
},
search: (query) => ({
...query,
secretPath
})
});
};
return (
<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]">
<PageHeader
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."
/>
<CommitHistoryTab
onSelectCommit={handleSelectCommit}
projectId={currentWorkspace.id}
environment={envSlug}
secretPath={secretPath}
/>
</div>
</div>
);
};
@@ -0,0 +1,281 @@
import { useCallback, useMemo, useState } from "react";
import {
faArrowDownWideShort,
faArrowUpWideShort,
faCopy,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format, formatDistanceToNow } from "date-fns";
import { Button, Input, Spinner } from "@app/components/v2";
import { CopyButton } from "@app/components/v2/CopyButton";
import { useGetFolderCommitHistory } from "@app/hooks/api/folderCommits";
interface CommitActorMetadata {
email?: string;
name?: string;
}
interface Commit {
id: string;
message: string;
createdAt: string;
actorType: string;
actorMetadata?: CommitActorMetadata;
}
const formatTimeAgo = (timestamp: string): string => {
return formatDistanceToNow(new Date(timestamp), { addSuffix: true });
};
/**
* Commit Item component for displaying a single commit
*/
const CommitItem = ({
commit,
onSelectCommit
}: {
commit: Commit;
onSelectCommit: (commitId: string, tab: string) => void;
}) => {
return (
<div className="border-b border-zinc-800 last:border-b-0">
<div className="px-4 py-4 transition-colors duration-200 hover:bg-zinc-800">
<div className="flex flex-col sm:flex-row sm:justify-between">
<div className="flex-1">
<div className="flex items-center">
<Button
variant="link"
className="text-white hover:underline"
onClick={(e) => {
e.stopPropagation();
onSelectCommit(commit.id, "tab-commit-details");
}}
>
{commit.message}
</Button>
</div>
<p className="text-white-400 mt-2 flex flex-wrap items-center gap-4 text-sm">
<span className="flex items-center">
{commit.actorMetadata?.email || commit.actorMetadata?.name || commit.actorType}
<p className="ml-1 mr-1">committed</p>
<time dateTime={commit.createdAt}>{formatTimeAgo(commit.createdAt)}</time>
</span>
</p>
</div>
<div className="mt-2 flex items-center sm:mt-0">
<div className="flex items-center space-x-1">
<Button
variant="link"
className="text-white hover:underline"
onClick={(e) => {
e.stopPropagation();
onSelectCommit(commit.id, "tab-commit-details");
}}
>
<code className="text-white-400 px-3 py-1 font-mono text-sm">
{commit.id?.substring(0, 11)}
</code>
</Button>
<CopyButton
value={commit.id}
name={commit.id}
size="sm"
variant="plain"
color="text-mineshaft-400"
icon={faCopy}
/>
</div>
</div>
</div>
</div>
</div>
);
};
/**
* Date Group component for displaying commits grouped by date
*/
const DateGroup = ({
date,
commits,
onSelectCommit
}: {
date: string;
commits: Commit[];
onSelectCommit: (commitId: string, tab: string) => void;
}) => {
return (
<div className="mb-8 last:mb-0 last:pb-2">
<div className="mb-4 flex items-center">
<div className="relative mr-3 flex h-6 w-6 items-center justify-center">
<div className="z-10 h-3 w-3 rounded-full border-2 border-mineshaft-600 bg-bunker-800" />
<div className="absolute left-0 right-0 top-1/2 h-0.5 -translate-y-1/2 bg-mineshaft-600" />
</div>
<h2 className="text-sm text-white">Commits on {date}</h2>
</div>
<div className="relative">
<div className="absolute bottom-0 left-3 top-0 w-0.5 bg-mineshaft-600" />
<div className="ml-10">
{commits.map((commit) => (
<div key={commit.id} className="relative mb-3 pb-1">
<div className="overflow-hidden rounded-md border border-solid border-mineshaft-600">
<CommitItem commit={commit} onSelectCommit={onSelectCommit} />
</div>
</div>
))}
</div>
</div>
</div>
);
};
export const CommitHistoryTab = ({
onSelectCommit,
projectId,
environment,
secretPath
}: {
onSelectCommit: (commitId: string, tab: string) => void;
projectId: string;
environment: string;
secretPath: string;
}) => {
const [searchTerm, setSearchTerm] = useState("");
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
const [visibleCommits, setVisibleCommits] = useState(10);
const { data: commits, isLoading } = useGetFolderCommitHistory({
workspaceId: projectId,
environment,
directory: secretPath
});
const filteredCommits = useMemo(() => {
if (!commits?.length) return [];
return commits.filter(
(commit) =>
commit.id?.includes(searchTerm) ||
commit.message?.toLowerCase().includes(searchTerm.toLowerCase()) ||
commit.actorMetadata?.name?.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [commits, searchTerm]);
const sortedCommits = useMemo(() => {
if (!filteredCommits?.length) return [];
return [...filteredCommits].sort((a, b) => {
const dateA = new Date(a.createdAt).getTime();
const dateB = new Date(b.createdAt).getTime();
return sortDirection === "desc" ? dateB - dateA : dateA - dateB;
});
}, [filteredCommits, sortDirection]);
const displayedCommits = useMemo(() => {
return sortedCommits.slice(0, visibleCommits);
}, [sortedCommits, visibleCommits]);
const groupedCommits = useMemo(() => {
return displayedCommits.reduce(
(acc, commit) => {
const date = format(new Date(commit.createdAt), "MMM d, yyyy");
if (!acc[date]) {
acc[date] = [];
}
acc[date].push(commit);
return acc;
},
{} as Record<string, Commit[]>
);
}, [displayedCommits]);
const handleSort = useCallback(() => {
setSortDirection((prev) => (prev === "desc" ? "asc" : "desc"));
}, []);
const loadMoreCommits = useCallback(() => {
setVisibleCommits((prev) => prev + 10);
}, []);
if (isLoading) {
return (
<div className="flex h-64 items-center justify-center">
<Spinner size="lg" aria-label="Loading commits" />
</div>
);
}
const hasMoreCommits = sortedCommits.length > visibleCommits;
return (
<div className="w-full">
<div className="mb-4 flex flex-col sm:flex-row sm:justify-end">
<div className="flex w-full flex-wrap items-center gap-2">
<div className="relative flex-grow">
<Input
placeholder="Search commits..."
className="h-10 w-full rounded-md border-transparent bg-zinc-800 pl-9 pr-3 text-sm text-white placeholder-gray-400 focus:border-gray-600 focus:ring-primary-500/20"
onChange={(e) => setSearchTerm(e.target.value)}
aria-label="Search commits"
/>
<div className="absolute left-3 top-1/2 -translate-y-1/2 transform text-gray-400">
<FontAwesomeIcon icon={faSearch} aria-hidden="true" />
</div>
</div>
<Button
variant="outline_bg"
size="md"
className="flex h-10 items-center justify-center gap-2 rounded-md bg-zinc-800 px-4 py-2 text-sm text-white transition-colors duration-200 hover:bg-zinc-700 focus:outline-none focus:ring-2 focus:ring-primary-500"
onClick={handleSort}
aria-label={`Sort by date ${sortDirection === "desc" ? "ascending" : "descending"}`}
>
<FontAwesomeIcon
icon={sortDirection === "desc" ? faArrowDownWideShort : faArrowUpWideShort}
aria-hidden="true"
/>
</Button>
</div>
</div>
<div className="space-y-8">
{Object.keys(groupedCommits).length > 0 ? (
<>
{Object.entries(groupedCommits).map(([date, dateCommits]) => (
<DateGroup
key={date}
date={date}
commits={dateCommits}
onSelectCommit={onSelectCommit}
/>
))}
</>
) : (
<div className="text-white-400 flex min-h-40 flex-col items-center justify-center rounded-lg bg-zinc-900 py-8 text-center">
<FontAwesomeIcon
icon={faSearch}
className="text-white-500 mb-3 text-3xl"
aria-hidden="true"
/>
<p>No matching commits found. Try a different search term.</p>
</div>
)}
{hasMoreCommits && (
<div className="flex justify-center pb-2">
<Button
variant="outline_bg"
size="md"
className="rounded-md bg-zinc-900 px-6 py-2 text-sm font-medium text-white transition-colors duration-200 hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-primary-500"
onClick={loadMoreCommits}
aria-label="Load more commits"
>
Load more commits
</Button>
</div>
)}
</div>
</div>
);
};
@@ -0,0 +1 @@
export { CommitHistoryTab } from "./CommitHistoryTab";
@@ -0,0 +1 @@
export { CommitsPage } from "./CommitsPage";
@@ -0,0 +1,71 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { SecretDashboardPathBreadcrumb } from "@app/components/navigation/SecretDashboardPathBreadcrumb";
import { BreadcrumbTypes } from "@app/components/v2";
import { CommitsPage } from "./CommitsPage";
const CommitsPageQueryParamsSchema = z.object({
secretPath: z.string().catch("/")
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/"
)({
component: CommitsPage,
validateSearch: zodValidator(CommitsPageQueryParamsSchema),
search: {
middlewares: [stripSearchParams({ secretPath: "/" })]
},
beforeLoad: ({ context, params, search }) => {
const secretPathSegments = search.secretPath.split("/").filter(Boolean);
return {
breadcrumbs: [
...context.breadcrumbs,
{
type: BreadcrumbTypes.Dropdown,
label:
context.project.environments.find((el) => el.slug === params.environment)?.name || "",
dropdownTitle: "Environments",
links: context.project.environments.map((el) => ({
label: el.name,
link: linkOptions({
to: "/secret-manager/$projectId/secrets/$envSlug",
params: {
projectId: params.projectId,
envSlug: el.slug
}
})
}))
},
...secretPathSegments.map((_, index) => ({
type: BreadcrumbTypes.Component,
component: () => (
<SecretDashboardPathBreadcrumb
secretPathSegments={secretPathSegments}
selectedPathSegmentIndex={index}
environmentSlug={params.environment}
projectId={params.projectId}
disableCopy
/>
)
})),
{
label: "Commits",
link: linkOptions({
to: "/secret-manager/$projectId/commits/$environment/$folderId",
params: {
projectId: params.projectId,
environment: params.environment,
folderId: params.folderId
}
})
}
]
};
}
});
@@ -39,6 +39,7 @@ import {
} from "@app/hooks/api";
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 { ProjectType } from "@app/hooks/api/workspace/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
@@ -265,6 +266,17 @@ const Page = () => {
limit: 10
});
const {
data: { count: folderCommitsCount, folderId } = { count: 0, folderId: "" },
isPending: isFolderCommitsCountLoading,
isFetching: isFolderCommitsCountFetching
} = useGetFolderCommitsCount({
directory: secretPath,
workspaceId,
environment,
isPaused: !canDoReadRollback
});
const {
data: snapshotCount,
isPending: isSnapshotCountLoading,
@@ -276,6 +288,39 @@ const Page = () => {
isPaused: !canDoReadRollback
});
const isPITEnabled = true;
const changesCount = useMemo(() => {
return isPITEnabled ? folderCommitsCount : snapshotCount;
}, [folderCommitsCount, snapshotCount]);
const isChangesCountPending = useMemo(() => {
return isPITEnabled ? isFolderCommitsCountLoading || isSnapshotCountLoading : false;
}, [isFolderCommitsCountLoading, isSnapshotCountLoading]);
const isChangesCountFetching = useMemo(() => {
return isPITEnabled ? isFolderCommitsCountFetching || isSnapshotCountFetching : false;
}, [isFolderCommitsCountFetching, isSnapshotCountFetching]);
const handleOnClickRollbackMode = () => {
if (isPITEnabled) {
navigate({
to: `/${ProjectType.SecretManager}/$projectId/commits/$environment/$folderId` as const,
params: {
projectId: workspaceId,
folderId,
environment
},
search: (query) => ({
...query,
secretPath
})
});
} else {
handlePopUpToggle("snapshots", true);
}
};
const noAccessSecretCount = Math.max(
(page * perPage > totalCount ? totalCount % perPage : perPage) -
(imports?.length || 0) -
@@ -436,13 +481,14 @@ const Page = () => {
onVisibilityToggle={handleToggleVisibility}
onSearchChange={handleSearchChange}
onToggleTagFilter={handleTagToggle}
snapshotCount={snapshotCount || 0}
isSnapshotCountLoading={isSnapshotCountLoading && isSnapshotCountFetching}
snapshotCount={changesCount || 0}
isSnapshotCountLoading={isChangesCountPending && isChangesCountFetching}
onToggleRowType={handleToggleRowType}
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
onClickRollbackMode={handleOnClickRollbackMode}
protectedBranchPolicyName={boardPolicy?.name}
importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
isPITEnabled={isPITEnabled}
/>
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
<div className="flex flex-col" id="dashboard">
@@ -606,7 +652,7 @@ const Page = () => {
secretPath={secretPath}
secrets={secrets}
folders={folders}
snapshotCount={snapshotCount}
snapshotCount={changesCount}
onGoBack={handleResetSnapshot}
onClickListSnapshot={() => handlePopUpToggle("snapshots", true)}
/>
@@ -123,6 +123,7 @@ type Props = {
isImported: boolean;
}[];
}[];
isPITEnabled: boolean;
};
export const ActionBar = ({
@@ -142,6 +143,7 @@ export const ActionBar = ({
onToggleRowType,
protectedBranchPolicyName,
importedBy,
isPITEnabled = false,
usedBySecretSyncs
}: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
@@ -796,7 +798,7 @@ export const ActionBar = ({
className="h-10"
isDisabled={!isAllowed}
>
{`${snapshotCount} ${snapshotCount === 1 ? "Snapshot" : "Snapshots"}`}
{`${snapshotCount} ${isPITEnabled ? "Commit" : "Snapshot"}${snapshotCount === 1 ? "" : "s"}`}
</Button>
)}
</ProjectPermissionCan>
@@ -9,6 +9,7 @@ import { usePopUp } from "@app/hooks";
import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
import { commitKeys } from "@app/hooks/api/folderCommits/queries";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
@@ -264,6 +265,12 @@ export const SecretListView = ({
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: secretApprovalRequestKeys.count({ workspaceId })
});
@@ -315,6 +322,12 @@ export const SecretListView = ({
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: secretApprovalRequestKeys.count({ workspaceId })
});
+183 -1
View File
@@ -191,6 +191,9 @@ import { Route as secretManagerIntegrationsGcpSecretManagerOauthCallbackPageRout
import { Route as secretManagerIntegrationsBitbucketOauthCallbackPageRouteImport } from './pages/secret-manager/integrations/BitbucketOauthCallbackPage/route'
import { Route as secretManagerIntegrationsAzureKeyVaultOauthCallbackPageRouteImport } from './pages/secret-manager/integrations/AzureKeyVaultOauthCallbackPage/route'
import { Route as secretManagerIntegrationsAzureAppConfigurationOauthCallbackPageRouteImport } from './pages/secret-manager/integrations/AzureAppConfigurationOauthCallbackPage/route'
import { Route as secretManagerCommitsPageRouteImport } from './pages/secret-manager/CommitsPage/route'
import { Route as secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteImport } from './pages/secret-manager/CommitDetailsPage/components/RollbackPreviewTab/route'
import { Route as secretManagerCommitDetailsPageRouteImport } from './pages/secret-manager/CommitDetailsPage/route'
// Create Virtual Routes
@@ -246,6 +249,14 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLa
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations',
)()
const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId',
)()
const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId',
)()
// Create/Update Routes
@@ -1082,6 +1093,15 @@ const secretManagerIntegrationsListPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRoute =
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdImport.update(
{
id: '/commits/$environment/$folderId',
path: '/commits/$environment/$folderId',
getParentRoute: () => secretManagerLayoutRoute,
} as any,
)
const secretManagerIntegrationsWindmillConfigurePageRouteRoute =
secretManagerIntegrationsWindmillConfigurePageRouteImport.update({
id: '/windmill/create',
@@ -1608,6 +1628,16 @@ const secretManagerIntegrationsAwsParameterStoreAuthorizePageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRoute =
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdImport.update(
{
id: '/$commitId',
path: '/$commitId',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRoute,
} as any,
)
const secretManagerIntegrationsVercelOauthCallbackPageRouteRoute =
secretManagerIntegrationsVercelOauthCallbackPageRouteImport.update({
id: '/vercel/oauth2/callback',
@@ -1690,6 +1720,30 @@ const secretManagerIntegrationsAzureAppConfigurationOauthCallbackPageRouteRoute
} as any,
)
const secretManagerCommitsPageRouteRoute =
secretManagerCommitsPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRoute,
} as any)
const secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteRoute =
secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteImport.update({
id: '/restore',
path: '/restore',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRoute,
} as any)
const secretManagerCommitDetailsPageRouteRoute =
secretManagerCommitDetailsPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
@@ -2975,6 +3029,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof secretManagerIntegrationsWindmillConfigurePageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsImport
}
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId'
path: '/commits/$environment/$folderId'
fullPath: '/secret-manager/$projectId/commits/$environment/$folderId'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdImport
parentRoute: typeof secretManagerLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/'
path: '/'
fullPath: '/secret-manager/$projectId/commits/$environment/$folderId/'
preLoaderRoute: typeof secretManagerCommitsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdImport
}
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback'
path: '/azure-app-configuration/oauth2/callback'
@@ -3045,6 +3113,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof secretManagerIntegrationsVercelOauthCallbackPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsImport
}
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId'
path: '/$commitId'
fullPath: '/secret-manager/$projectId/commits/$environment/$folderId/$commitId'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdImport
}
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/'
path: '/'
fullPath: '/secret-manager/$projectId/commits/$environment/$folderId/$commitId/'
preLoaderRoute: typeof secretManagerCommitDetailsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdImport
}
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore'
path: '/restore'
fullPath: '/secret-manager/$projectId/commits/$environment/$folderId/$commitId/restore'
preLoaderRoute: typeof secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdImport
}
}
}
@@ -3553,6 +3642,41 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLa
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRouteChildren {
secretManagerCommitDetailsPageRouteRoute: typeof secretManagerCommitDetailsPageRouteRoute
secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteRoute: typeof secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRouteChildren =
{
secretManagerCommitDetailsPageRouteRoute:
secretManagerCommitDetailsPageRouteRoute,
secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteRoute:
secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRouteChildren {
secretManagerCommitsPageRouteRoute: typeof secretManagerCommitsPageRouteRoute
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRouteWithChildren
}
const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRouteChildren =
{
secretManagerCommitsPageRouteRoute: secretManagerCommitsPageRouteRoute,
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRoute:
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRouteWithChildren,
}
const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRouteChildren,
)
interface secretManagerLayoutRouteChildren {
secretManagerIPAllowlistPageRouteRoute: typeof secretManagerIPAllowlistPageRouteRoute
secretManagerSecretApprovalsPageRouteRoute: typeof secretManagerSecretApprovalsPageRouteRoute
@@ -3565,6 +3689,7 @@ interface secretManagerLayoutRouteChildren {
projectIdentityDetailsByIDPageRouteSecretManagerRoute: typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
projectMemberDetailsByIDPageRouteSecretManagerRoute: typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
projectRoleDetailsBySlugPageRouteSecretManagerRoute: typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRouteWithChildren
}
const secretManagerLayoutRouteChildren: secretManagerLayoutRouteChildren = {
@@ -3588,6 +3713,8 @@ const secretManagerLayoutRouteChildren: secretManagerLayoutRouteChildren = {
projectMemberDetailsByIDPageRouteSecretManagerRoute,
projectRoleDetailsBySlugPageRouteSecretManagerRoute:
projectRoleDetailsBySlugPageRouteSecretManagerRoute,
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRoute:
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRouteWithChildren,
}
const secretManagerLayoutRouteWithChildren =
@@ -4018,6 +4145,8 @@ export interface FileRoutesByFullPath {
'/secret-manager/$projectId/integrations/vercel/create': typeof secretManagerIntegrationsVercelConfigurePageRouteRoute
'/secret-manager/$projectId/integrations/windmill/authorize': typeof secretManagerIntegrationsWindmillAuthorizePageRouteRoute
'/secret-manager/$projectId/integrations/windmill/create': typeof secretManagerIntegrationsWindmillConfigurePageRouteRoute
'/secret-manager/$projectId/commits/$environment/$folderId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRouteWithChildren
'/secret-manager/$projectId/commits/$environment/$folderId/': typeof secretManagerCommitsPageRouteRoute
'/secret-manager/$projectId/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsAzureAppConfigurationOauthCallbackPageRouteRoute
'/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsAzureKeyVaultOauthCallbackPageRouteRoute
'/secret-manager/$projectId/integrations/bitbucket/oauth2/callback': typeof secretManagerIntegrationsBitbucketOauthCallbackPageRouteRoute
@@ -4028,6 +4157,9 @@ export interface FileRoutesByFullPath {
'/secret-manager/$projectId/integrations/netlify/oauth2/callback': typeof secretManagerIntegrationsNetlifyOauthCallbackPageRouteRoute
'/secret-manager/$projectId/integrations/secret-syncs/$destination/$syncId': typeof secretManagerSecretSyncDetailsByIDPageRouteRoute
'/secret-manager/$projectId/integrations/vercel/oauth2/callback': typeof secretManagerIntegrationsVercelOauthCallbackPageRouteRoute
'/secret-manager/$projectId/commits/$environment/$folderId/$commitId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRouteWithChildren
'/secret-manager/$projectId/commits/$environment/$folderId/$commitId/': typeof secretManagerCommitDetailsPageRouteRoute
'/secret-manager/$projectId/commits/$environment/$folderId/$commitId/restore': typeof secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteRoute
}
export interface FileRoutesByTo {
@@ -4196,6 +4328,7 @@ export interface FileRoutesByTo {
'/secret-manager/$projectId/integrations/vercel/create': typeof secretManagerIntegrationsVercelConfigurePageRouteRoute
'/secret-manager/$projectId/integrations/windmill/authorize': typeof secretManagerIntegrationsWindmillAuthorizePageRouteRoute
'/secret-manager/$projectId/integrations/windmill/create': typeof secretManagerIntegrationsWindmillConfigurePageRouteRoute
'/secret-manager/$projectId/commits/$environment/$folderId': typeof secretManagerCommitsPageRouteRoute
'/secret-manager/$projectId/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsAzureAppConfigurationOauthCallbackPageRouteRoute
'/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsAzureKeyVaultOauthCallbackPageRouteRoute
'/secret-manager/$projectId/integrations/bitbucket/oauth2/callback': typeof secretManagerIntegrationsBitbucketOauthCallbackPageRouteRoute
@@ -4206,6 +4339,8 @@ export interface FileRoutesByTo {
'/secret-manager/$projectId/integrations/netlify/oauth2/callback': typeof secretManagerIntegrationsNetlifyOauthCallbackPageRouteRoute
'/secret-manager/$projectId/integrations/secret-syncs/$destination/$syncId': typeof secretManagerSecretSyncDetailsByIDPageRouteRoute
'/secret-manager/$projectId/integrations/vercel/oauth2/callback': typeof secretManagerIntegrationsVercelOauthCallbackPageRouteRoute
'/secret-manager/$projectId/commits/$environment/$folderId/$commitId': typeof secretManagerCommitDetailsPageRouteRoute
'/secret-manager/$projectId/commits/$environment/$folderId/$commitId/restore': typeof secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteRoute
}
export interface FileRoutesById {
@@ -4393,6 +4528,8 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/create': typeof secretManagerIntegrationsVercelConfigurePageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/authorize': typeof secretManagerIntegrationsWindmillAuthorizePageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/create': typeof secretManagerIntegrationsWindmillConfigurePageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/': typeof secretManagerCommitsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsAzureAppConfigurationOauthCallbackPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsAzureKeyVaultOauthCallbackPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback': typeof secretManagerIntegrationsBitbucketOauthCallbackPageRouteRoute
@@ -4403,6 +4540,9 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback': typeof secretManagerIntegrationsNetlifyOauthCallbackPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId': typeof secretManagerSecretSyncDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback': typeof secretManagerIntegrationsVercelOauthCallbackPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/': typeof secretManagerCommitDetailsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore': typeof secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteRoute
}
export interface FileRouteTypes {
@@ -4582,6 +4722,8 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId/integrations/vercel/create'
| '/secret-manager/$projectId/integrations/windmill/authorize'
| '/secret-manager/$projectId/integrations/windmill/create'
| '/secret-manager/$projectId/commits/$environment/$folderId'
| '/secret-manager/$projectId/commits/$environment/$folderId/'
| '/secret-manager/$projectId/integrations/azure-app-configuration/oauth2/callback'
| '/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback'
| '/secret-manager/$projectId/integrations/bitbucket/oauth2/callback'
@@ -4592,6 +4734,9 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId/integrations/netlify/oauth2/callback'
| '/secret-manager/$projectId/integrations/secret-syncs/$destination/$syncId'
| '/secret-manager/$projectId/integrations/vercel/oauth2/callback'
| '/secret-manager/$projectId/commits/$environment/$folderId/$commitId'
| '/secret-manager/$projectId/commits/$environment/$folderId/$commitId/'
| '/secret-manager/$projectId/commits/$environment/$folderId/$commitId/restore'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
@@ -4759,6 +4904,7 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId/integrations/vercel/create'
| '/secret-manager/$projectId/integrations/windmill/authorize'
| '/secret-manager/$projectId/integrations/windmill/create'
| '/secret-manager/$projectId/commits/$environment/$folderId'
| '/secret-manager/$projectId/integrations/azure-app-configuration/oauth2/callback'
| '/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback'
| '/secret-manager/$projectId/integrations/bitbucket/oauth2/callback'
@@ -4769,6 +4915,8 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId/integrations/netlify/oauth2/callback'
| '/secret-manager/$projectId/integrations/secret-syncs/$destination/$syncId'
| '/secret-manager/$projectId/integrations/vercel/oauth2/callback'
| '/secret-manager/$projectId/commits/$environment/$folderId/$commitId'
| '/secret-manager/$projectId/commits/$environment/$folderId/$commitId/restore'
id:
| '__root__'
| '/'
@@ -4954,6 +5102,8 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/create'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/authorize'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/create'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback'
@@ -4964,6 +5114,9 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore'
fileRoutesById: FileRoutesById
}
@@ -5403,7 +5556,8 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/members/$membershipId",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug"
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId"
]
},
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout": {
@@ -5970,6 +6124,18 @@ export const routeTree = rootRoute
"filePath": "secret-manager/integrations/WindmillConfigurePage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations"
},
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout",
"children": [
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId"
]
},
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/": {
"filePath": "secret-manager/CommitsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId"
},
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback": {
"filePath": "secret-manager/integrations/AzureAppConfigurationOauthCallbackPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations"
@@ -6009,6 +6175,22 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback": {
"filePath": "secret-manager/integrations/VercelOauthCallbackPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations"
},
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId",
"children": [
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/",
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
]
},
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/": {
"filePath": "secret-manager/CommitDetailsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId"
},
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore": {
"filePath": "secret-manager/CommitDetailsPage/components/RollbackPreviewTab/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId"
}
}
}
+10
View File
@@ -47,6 +47,16 @@ const secretManagerRoutes = route("/secret-manager/$projectId", [
route("/approval", "secret-manager/SecretApprovalsPage/route.tsx"),
route("/secret-rotation", "secret-manager/SecretRotationPage/route.tsx"),
route("/settings", "secret-manager/SettingsPage/route.tsx"),
route("/commits/$environment/$folderId", [
index("secret-manager/CommitsPage/route.tsx"),
route("/$commitId", [
index("secret-manager/CommitDetailsPage/route.tsx"),
route(
"/restore",
"secret-manager/CommitDetailsPage/components/RollbackPreviewTab/route.tsx"
)
])
]),
route("/integrations", [
index("secret-manager/IntegrationsListPage/route.tsx"),
route("/$integrationId", "secret-manager/IntegrationsDetailsByIDPage/route.tsx"),