mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
PIT: UI views
This commit is contained in:
@@ -29,6 +29,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
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");
|
||||
|
||||
@@ -11,6 +11,7 @@ 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(),
|
||||
|
||||
@@ -1,144 +1,612 @@
|
||||
/* 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 all commits for a folder
|
||||
// Get commits count for a folder
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/commits/:folderId",
|
||||
url: "/commits/count/:workspaceId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
folderId: z.string().trim()
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
environment: z.string().trim(),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash)
|
||||
}),
|
||||
response: {
|
||||
200: z.any()
|
||||
200: z.object({
|
||||
count: z.number(),
|
||||
folderId: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const commits = await server.services.folderCommit.getCommitsByFolderId(req.params.folderId);
|
||||
return commits;
|
||||
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/:commitId/changes",
|
||||
url: "/commits/:workspaceId/:commitId/changes",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.any()
|
||||
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(req.params.commitId);
|
||||
return changes;
|
||||
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()
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Compare folder states between commits
|
||||
// Retrieve rollback changes for a commit
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/compare",
|
||||
url: "/commits/:workspaceId/:commitId/compare",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
fromCommit: z.string().trim(),
|
||||
toCommit: z.string().trim()
|
||||
folderId: z.string().trim(),
|
||||
envId: z.string().trim(),
|
||||
deepRollback: booleanSchema.default(false),
|
||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash)
|
||||
}),
|
||||
response: {
|
||||
200: z.any()
|
||||
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 diff = await server.services.folderCommit.compareFolderStates({
|
||||
currentCommitId: req.query.fromCommit,
|
||||
targetCommitId: req.query.toCommit
|
||||
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 diff;
|
||||
return diffs;
|
||||
}
|
||||
});
|
||||
|
||||
// Rollback to a previous commit
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/rollback",
|
||||
url: "/commits/:workspaceId/:commitId/rollback",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
fromCommit: z.string().trim(),
|
||||
toCommit: z.string().trim(),
|
||||
folderId: z.string().trim(),
|
||||
projectId: z.string().trim(),
|
||||
reconstructNewFolders: z.boolean().default(false)
|
||||
deepRollback: z.boolean().default(false),
|
||||
message: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.any()
|
||||
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(
|
||||
latestCommit.id,
|
||||
req.body.folderId,
|
||||
req.permission.id,
|
||||
req.permission.type,
|
||||
req.params.workspaceId
|
||||
);
|
||||
return { success: true };
|
||||
}
|
||||
const diff = await server.services.folderCommit.compareFolderStates({
|
||||
currentCommitId: req.body.fromCommit,
|
||||
targetCommitId: req.body.toCommit
|
||||
currentCommitId: latestCommit.id,
|
||||
targetCommitId: req.params.commitId
|
||||
});
|
||||
|
||||
const response = await server.services.folderCommit.applyFolderStateDifferences({
|
||||
differences: diff,
|
||||
actorInfo: {
|
||||
actorType: req.permission?.type || "PLATFORM",
|
||||
actorId: req.permission?.id,
|
||||
message: "Rollback to previous commit"
|
||||
actorType: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
message: req.body.message || "Rollback to previous commit"
|
||||
},
|
||||
folderId: req.body.folderId,
|
||||
projectId: req.body.projectId,
|
||||
reconstructNewFolders: req.body.reconstructNewFolders
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
// Deep rollback to a specific commit
|
||||
// Folder state at commit
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/deep-rollback",
|
||||
method: "GET",
|
||||
url: "/commits/:workspaceId/:commitId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
commitId: z.string().trim(),
|
||||
envId: z.string().trim(),
|
||||
projectId: z.string().trim()
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
folderId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.any()
|
||||
200: folderStateSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
await server.services.folderCommit.deepRollbackFolder(
|
||||
req.body.commitId,
|
||||
req.body.envId,
|
||||
req.permission?.id || "PLATFORM",
|
||||
req.permission?.type || "PLATFORM",
|
||||
req.body.projectId
|
||||
);
|
||||
const response = await server.services.folderCommit.reconstructFolderState(req.params.commitId);
|
||||
|
||||
return { success: true };
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -351,6 +351,7 @@ export const secretRotationQueueFactory = ({
|
||||
folderId: secretVersions[0].folderId,
|
||||
changes: secretVersions.map((sv) => ({
|
||||
type: "add",
|
||||
isUpdate: true,
|
||||
secretVersionId: sv.id
|
||||
}))
|
||||
},
|
||||
|
||||
@@ -587,7 +587,8 @@ export const registerRoutes = async (
|
||||
folderCheckpointResourcesDAL,
|
||||
secretV2BridgeDAL,
|
||||
folderTreeCheckpointResourcesDAL,
|
||||
folderCommitQueueService
|
||||
folderCommitQueueService,
|
||||
permissionService
|
||||
});
|
||||
const scimService = scimServiceFactory({
|
||||
licenseService,
|
||||
|
||||
@@ -27,6 +27,10 @@ export const folderCheckpointResourcesDALFactory = (db: TDbClient) => {
|
||||
(TFolderCheckpointResources & {
|
||||
referencedSecretId?: string;
|
||||
referencedFolderId?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
})[]
|
||||
> => {
|
||||
try {
|
||||
@@ -45,9 +49,17 @@ export const folderCheckpointResourcesDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.FolderCheckpointResources))
|
||||
.select(
|
||||
db.ref("secretId").withSchema(TableName.SecretVersionV2).as("referencedSecretId"),
|
||||
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("referencedFolderId")
|
||||
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;
|
||||
return docs.map((doc) => ({
|
||||
...doc,
|
||||
folderVersion: doc.folderVersion?.toString(),
|
||||
secretVersion: doc.secretVersion?.toString()
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByCheckpointId" });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TFolderCommitChanges, TFolderCommits } from "@app/db/schemas";
|
||||
import {
|
||||
TableName,
|
||||
TFolderCommitChanges,
|
||||
TFolderCommits,
|
||||
TSecretFolderVersions,
|
||||
TSecretVersionsV2
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
@@ -12,17 +18,67 @@ type CommitChangeWithCommitInfo = TFolderCommitChanges & {
|
||||
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<TFolderCommitChanges[]> => {
|
||||
const findByCommitId = async (folderCommitId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())<TFolderCommitChanges>(TableName.FolderCommitChanges)
|
||||
.where({ folderCommitId })
|
||||
.select(selectAllTableCols(TableName.FolderCommitChanges));
|
||||
return docs;
|
||||
.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" });
|
||||
}
|
||||
|
||||
@@ -19,11 +19,36 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findByFolderId = async (folderId: string, tx?: Knex): Promise<TFolderCommits[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
const trx = tx || db.replicaNode();
|
||||
|
||||
// First, get all folder commits
|
||||
const folderCommits = await trx(TableName.FolderCommit)
|
||||
.where({ folderId })
|
||||
.select(selectAllTableCols(TableName.FolderCommit))
|
||||
.select("*")
|
||||
.orderBy("createdAt", "desc");
|
||||
return docs;
|
||||
|
||||
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" });
|
||||
}
|
||||
@@ -158,7 +183,14 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
tx?: Knex
|
||||
): Promise<
|
||||
(TFolderCommits & {
|
||||
changes: (TFolderCommitChanges & { referencedSecretId?: string; referencedFolderId?: string })[];
|
||||
changes: (TFolderCommitChanges & {
|
||||
referencedSecretId?: string;
|
||||
referencedFolderId?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
})[];
|
||||
})[]
|
||||
> => {
|
||||
try {
|
||||
@@ -195,7 +227,11 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
||||
.select(
|
||||
db.ref("secretId").withSchema(TableName.SecretVersionV2).as("referencedSecretId"),
|
||||
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("referencedFolderId")
|
||||
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
|
||||
@@ -280,6 +316,34 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
@@ -291,6 +355,7 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
findLatestCommitBetween,
|
||||
findLatestEnvCommit,
|
||||
getEnvNumberOfCommitsSince,
|
||||
findLatestCommitByFolderIds
|
||||
findLatestCommitByFolderIds,
|
||||
findAllFolderCommitsAfter
|
||||
};
|
||||
};
|
||||
|
||||
@@ -113,7 +113,8 @@ describe("folderCommitServiceFactory", () => {
|
||||
create: vi.fn().mockResolvedValue({}),
|
||||
updateById: vi.fn().mockResolvedValue({}),
|
||||
find: vi.fn().mockResolvedValue([]),
|
||||
findByIdsWithLatestVersion: vi.fn().mockResolvedValue({})
|
||||
findByIdsWithLatestVersion: vi.fn().mockResolvedValue({}),
|
||||
findLatestVersionMany: vi.fn().mockResolvedValue({})
|
||||
};
|
||||
|
||||
const mockSecretV2BridgeDAL = {
|
||||
@@ -133,26 +134,44 @@ describe("folderCommitServiceFactory", () => {
|
||||
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
|
||||
folderCommitQueueService: mockFolderCommitQueueService,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
permissionService: mockPermissionService
|
||||
});
|
||||
});
|
||||
|
||||
@@ -532,13 +551,31 @@ describe("folderCommitServiceFactory", () => {
|
||||
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]);
|
||||
|
||||
// These mocks need to return objects with an id field
|
||||
mockSecretVersionV2BridgeDAL.findByIdsWithLatestVersion.mockResolvedValue(secretVersions);
|
||||
mockFolderVersionDAL.findByIdsWithLatestVersion.mockResolvedValue(folderVersions);
|
||||
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));
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TSecretFolders, TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas";
|
||||
import { ActionProjectType, TSecretFolders, TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionCommitsActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
import { TFolderCheckpointDALFactory } from "../folder-checkpoint/folder-checkpoint-dal";
|
||||
import { TFolderCheckpointResourcesDALFactory } from "../folder-checkpoint-resources/folder-checkpoint-resources-dal";
|
||||
import { TFolderCommitChangesDALFactory } from "../folder-commit-changes/folder-commit-changes-dal";
|
||||
@@ -22,7 +25,6 @@ import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TFolderCommitDALFactory } from "./folder-commit-dal";
|
||||
import { TFolderCommitQueueServiceFactory } from "./folder-commit-queue";
|
||||
|
||||
// Define enums for better type safety
|
||||
export enum ChangeType {
|
||||
ADD = "add",
|
||||
DELETE = "delete",
|
||||
@@ -35,7 +37,6 @@ enum ResourceType {
|
||||
FOLDER = "folder"
|
||||
}
|
||||
|
||||
// Improved types for DTO objects
|
||||
type TCreateCommitDTO = {
|
||||
actor: {
|
||||
type: string;
|
||||
@@ -50,6 +51,7 @@ type TCreateCommitDTO = {
|
||||
type: string;
|
||||
secretVersionId?: string;
|
||||
folderVersionId?: string;
|
||||
isUpdate?: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -69,6 +71,23 @@ export type ResourceChange = {
|
||||
commitId: number;
|
||||
createdAt?: Date;
|
||||
parentId?: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
fromVersion?: 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;
|
||||
}[];
|
||||
};
|
||||
|
||||
type ActorInfo = {
|
||||
@@ -84,77 +103,21 @@ type StateChangeResult = {
|
||||
};
|
||||
|
||||
type TFolderCommitServiceFactoryDep = {
|
||||
folderCommitDAL: Pick<
|
||||
TFolderCommitDALFactory,
|
||||
| "create"
|
||||
| "findById"
|
||||
| "findByFolderId"
|
||||
| "findLatestCommit"
|
||||
| "transaction"
|
||||
| "getNumberOfCommitsSince"
|
||||
| "getEnvNumberOfCommitsSince"
|
||||
| "findCommitsToRecreate"
|
||||
| "findMultipleLatestCommits"
|
||||
| "findLatestCommitBetween"
|
||||
| "findAllCommitsBetween"
|
||||
| "findLatestEnvCommit"
|
||||
| "findLatestCommitByFolderIds"
|
||||
>;
|
||||
folderCommitChangesDAL: Pick<TFolderCommitChangesDALFactory, "create" | "findByCommitId" | "insertMany">;
|
||||
folderCheckpointDAL: Pick<
|
||||
TFolderCheckpointDALFactory,
|
||||
"create" | "findByFolderId" | "findLatestByFolderId" | "findNearestCheckpoint"
|
||||
>;
|
||||
folderCheckpointResourcesDAL: Pick<TFolderCheckpointResourcesDALFactory, "insertMany" | "findByCheckpointId">;
|
||||
folderTreeCheckpointDAL: Pick<
|
||||
TFolderTreeCheckpointDALFactory,
|
||||
"create" | "findNearestCheckpoint" | "findLatestByEnvId"
|
||||
>;
|
||||
folderTreeCheckpointResourcesDAL: Pick<
|
||||
TFolderTreeCheckpointResourcesDALFactory,
|
||||
"insertMany" | "findByTreeCheckpointId"
|
||||
>;
|
||||
userDAL: Pick<TUserDALFactory, "findById">;
|
||||
identityDAL: Pick<TIdentityDALFactory, "findById">;
|
||||
folderDAL: Pick<
|
||||
TSecretFolderDALFactory,
|
||||
| "findByParentId"
|
||||
| "findByProjectId"
|
||||
| "deleteById"
|
||||
| "create"
|
||||
| "updateById"
|
||||
| "update"
|
||||
| "find"
|
||||
| "findById"
|
||||
| "findByEnvId"
|
||||
| "findFoldersByRootAndIds"
|
||||
>;
|
||||
folderVersionDAL: Pick<
|
||||
TSecretFolderVersionDALFactory,
|
||||
| "findLatestFolderVersions"
|
||||
| "findById"
|
||||
| "deleteById"
|
||||
| "create"
|
||||
| "updateById"
|
||||
| "find"
|
||||
| "findByIdsWithLatestVersion"
|
||||
>;
|
||||
secretVersionV2BridgeDAL: Pick<
|
||||
TSecretVersionV2DALFactory,
|
||||
| "findLatestVersionByFolderId"
|
||||
| "findById"
|
||||
| "deleteById"
|
||||
| "create"
|
||||
| "updateById"
|
||||
| "find"
|
||||
| "findByIdsWithLatestVersion"
|
||||
>;
|
||||
secretV2BridgeDAL: Pick<
|
||||
secretV2BridgeDal.TSecretV2BridgeDALFactory,
|
||||
"deleteById" | "create" | "updateById" | "update" | "insertMany" | "invalidateSecretCacheByProjectId"
|
||||
>;
|
||||
folderCommitDAL: TFolderCommitDALFactory;
|
||||
folderCommitChangesDAL: TFolderCommitChangesDALFactory;
|
||||
folderCheckpointDAL: TFolderCheckpointDALFactory;
|
||||
folderCheckpointResourcesDAL: TFolderCheckpointResourcesDALFactory;
|
||||
folderTreeCheckpointDAL: TFolderTreeCheckpointDALFactory;
|
||||
folderTreeCheckpointResourcesDAL: TFolderTreeCheckpointResourcesDALFactory;
|
||||
userDAL: TUserDALFactory;
|
||||
identityDAL: TIdentityDALFactory;
|
||||
folderDAL: TSecretFolderDALFactory;
|
||||
folderVersionDAL: TSecretFolderVersionDALFactory;
|
||||
secretVersionV2BridgeDAL: TSecretVersionV2DALFactory;
|
||||
secretV2BridgeDAL: secretV2BridgeDal.TSecretV2BridgeDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
folderCommitQueueService?: Pick<TFolderCommitQueueServiceFactory, "scheduleTreeCheckpoint">;
|
||||
permissionService?: TPermissionServiceFactory;
|
||||
};
|
||||
|
||||
export const folderCommitServiceFactory = ({
|
||||
@@ -171,10 +134,39 @@ export const folderCommitServiceFactory = ({
|
||||
projectDAL,
|
||||
secretV2BridgeDAL,
|
||||
folderTreeCheckpointResourcesDAL,
|
||||
folderCommitQueueService
|
||||
folderCommitQueueService,
|
||||
permissionService
|
||||
}: TFolderCommitServiceFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const checkProjectPermission = async ({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
projectId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
}) => {
|
||||
if (!permissionService) {
|
||||
throw new Error("Permission service not initialized");
|
||||
}
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches all resources within a folder
|
||||
*/
|
||||
@@ -255,7 +247,17 @@ export const folderCommitServiceFactory = ({
|
||||
const reconstructFolderState = async (
|
||||
folderCommitId: string,
|
||||
tx?: Knex
|
||||
): Promise<{ type: string; id: string; versionId: string }[]> => {
|
||||
): Promise<
|
||||
{
|
||||
type: string;
|
||||
id: string;
|
||||
versionId: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
}[]
|
||||
> => {
|
||||
const targetCommit = await folderCommitDAL.findById(folderCommitId, tx);
|
||||
if (!targetCommit) {
|
||||
throw new NotFoundError({ message: `Commit with ID ${folderCommitId} not found` });
|
||||
@@ -268,7 +270,18 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
const checkpointResources = await folderCheckpointResourcesDAL.findByCheckpointId(nearestCheckpoint.id, tx);
|
||||
|
||||
const folderState: Record<string, { type: string; id: string; versionId: string }> = {};
|
||||
const folderState: Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
id: string;
|
||||
versionId: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
}
|
||||
> = {};
|
||||
|
||||
// Add all checkpoint resources to initial state
|
||||
checkpointResources.forEach((resource) => {
|
||||
@@ -276,13 +289,17 @@ export const folderCommitServiceFactory = ({
|
||||
folderState[`secret-${resource.referencedSecretId}`] = {
|
||||
type: "secret",
|
||||
id: resource.referencedSecretId,
|
||||
versionId: resource.secretVersionId
|
||||
versionId: resource.secretVersionId,
|
||||
secretKey: resource.secretKey,
|
||||
secretVersion: resource.secretVersion
|
||||
};
|
||||
} else if (resource.folderVersionId && resource.referencedFolderId) {
|
||||
folderState[`folder-${resource.referencedFolderId}`] = {
|
||||
type: "folder",
|
||||
id: resource.referencedFolderId,
|
||||
versionId: resource.folderVersionId
|
||||
versionId: resource.folderVersionId,
|
||||
folderName: resource.folderName,
|
||||
folderVersion: resource.folderVersion
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -307,7 +324,9 @@ export const folderCommitServiceFactory = ({
|
||||
folderState[key] = {
|
||||
type: "secret",
|
||||
id: change.referencedSecretId,
|
||||
versionId: change.secretVersionId
|
||||
versionId: change.secretVersionId,
|
||||
secretKey: change.secretKey,
|
||||
secretVersion: change.secretVersion
|
||||
};
|
||||
} else if (change.changeType.toLowerCase() === "delete") {
|
||||
delete folderState[key];
|
||||
@@ -319,7 +338,9 @@ export const folderCommitServiceFactory = ({
|
||||
folderState[key] = {
|
||||
type: "folder",
|
||||
id: change.referencedFolderId,
|
||||
versionId: change.folderVersionId
|
||||
versionId: change.folderVersionId,
|
||||
folderName: change.folderName,
|
||||
folderVersion: change.folderVersion
|
||||
};
|
||||
} else if (change.changeType.toLowerCase() === "delete") {
|
||||
delete folderState[key];
|
||||
@@ -356,7 +377,11 @@ export const folderCommitServiceFactory = ({
|
||||
id: resource.id,
|
||||
versionId: resource.versionId,
|
||||
changeType: "create",
|
||||
commitId: targetCommit.commitId
|
||||
commitId: targetCommit.commitId,
|
||||
secretKey: resource.secretKey,
|
||||
secretVersion: resource.secretVersion,
|
||||
folderName: resource.folderName,
|
||||
folderVersion: resource.folderVersion
|
||||
})) as ResourceChange[];
|
||||
}
|
||||
|
||||
@@ -365,8 +390,32 @@ export const folderCommitServiceFactory = ({
|
||||
const targetState = await reconstructFolderState(targetCommitId, tx);
|
||||
|
||||
// Create lookup maps for easier comparison
|
||||
const currentMap: Record<string, { type: string; id: string; versionId: string }> = {};
|
||||
const targetMap: Record<string, { type: string; id: string; versionId: string }> = {};
|
||||
const currentMap: Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
id: string;
|
||||
versionId: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
fromVersion?: string;
|
||||
}
|
||||
> = {};
|
||||
const targetMap: Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
id: string;
|
||||
versionId: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
fromVersion?: string;
|
||||
}
|
||||
> = {};
|
||||
|
||||
// Build lookup maps
|
||||
currentState.forEach((resource) => {
|
||||
@@ -393,7 +442,12 @@ export const folderCommitServiceFactory = ({
|
||||
id: currentResource.id,
|
||||
versionId: currentResource.versionId,
|
||||
changeType: ChangeType.DELETE,
|
||||
commitId: targetCommit.commitId
|
||||
commitId: targetCommit.commitId,
|
||||
secretKey: currentResource.secretKey,
|
||||
secretVersion: currentResource.secretVersion,
|
||||
folderName: currentResource.folderName,
|
||||
folderVersion: currentResource.folderVersion,
|
||||
fromVersion: currentResource.versionId
|
||||
});
|
||||
} else if (currentResource.versionId !== targetResource.versionId) {
|
||||
differences.push({
|
||||
@@ -401,12 +455,16 @@ export const folderCommitServiceFactory = ({
|
||||
id: targetResource.id,
|
||||
versionId: targetResource.versionId,
|
||||
changeType: ChangeType.UPDATE,
|
||||
commitId: targetCommit.commitId
|
||||
commitId: targetCommit.commitId,
|
||||
secretKey: targetResource.secretKey,
|
||||
secretVersion: targetResource.secretVersion,
|
||||
folderName: targetResource.folderName,
|
||||
folderVersion: targetResource.folderVersion,
|
||||
fromVersion: currentResource.folderVersion || currentResource.secretVersion
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Find creates
|
||||
Object.keys(targetMap).forEach((key) => {
|
||||
if (!currentMap[key]) {
|
||||
const targetResource = targetMap[key];
|
||||
@@ -416,7 +474,11 @@ export const folderCommitServiceFactory = ({
|
||||
versionId: targetResource.versionId,
|
||||
changeType: ChangeType.CREATE,
|
||||
commitId: targetCommit.commitId,
|
||||
createdAt: targetCommit.createdAt
|
||||
createdAt: targetCommit.createdAt,
|
||||
secretKey: targetResource.secretKey,
|
||||
secretVersion: targetResource.secretVersion,
|
||||
folderName: targetResource.folderName,
|
||||
folderVersion: targetResource.folderVersion
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -485,7 +547,8 @@ export const folderCommitServiceFactory = ({
|
||||
folderCommitId: newCommit.id,
|
||||
changeType: change.type,
|
||||
secretVersionId: change.secretVersionId,
|
||||
folderVersionId: change.folderVersionId
|
||||
folderVersionId: change.folderVersionId,
|
||||
isUpdate: change.isUpdate || false
|
||||
})),
|
||||
tx
|
||||
);
|
||||
@@ -515,11 +578,21 @@ export const folderCommitServiceFactory = ({
|
||||
) => {
|
||||
const commitChanges = [];
|
||||
|
||||
// Collect all secretIds for batch lookup
|
||||
const secretIds = changes.map((change) => secretVersions[change.id]?.secretId).filter(Boolean);
|
||||
|
||||
// Fetch all latest versions in one call
|
||||
const latestVersionsMap = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, secretIds, tx);
|
||||
|
||||
for (const change of changes) {
|
||||
const secretVersion = secretVersions[change.id];
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!secretVersion) continue;
|
||||
|
||||
// Get the latest version from our batch result
|
||||
const latestVersion = latestVersionsMap[secretVersion.secretId];
|
||||
const nextVersion = latestVersion ? latestVersion.version + 1 : 1;
|
||||
|
||||
switch (change.changeType) {
|
||||
case "create":
|
||||
{
|
||||
@@ -527,7 +600,7 @@ export const folderCommitServiceFactory = ({
|
||||
{
|
||||
id: change.id,
|
||||
skipMultilineEncoding: secretVersion.skipMultilineEncoding,
|
||||
version: secretVersion.version + 1,
|
||||
version: nextVersion,
|
||||
type: secretVersion.type,
|
||||
key: secretVersion.key,
|
||||
reminderNote: secretVersion.reminderNote,
|
||||
@@ -545,7 +618,7 @@ export const folderCommitServiceFactory = ({
|
||||
{
|
||||
folderId,
|
||||
secretId: secretVersion.secretId,
|
||||
version: secretVersion.version + 1,
|
||||
version: nextVersion,
|
||||
encryptedValue: secretVersion.encryptedValue,
|
||||
key: secretVersion.key,
|
||||
encryptedComment: secretVersion.encryptedComment,
|
||||
@@ -575,7 +648,7 @@ export const folderCommitServiceFactory = ({
|
||||
change.id,
|
||||
{
|
||||
skipMultilineEncoding: secretVersion?.skipMultilineEncoding,
|
||||
version: secretVersion?.version,
|
||||
version: nextVersion,
|
||||
type: secretVersion?.type,
|
||||
key: secretVersion?.key,
|
||||
reminderNote: secretVersion?.reminderNote,
|
||||
@@ -590,7 +663,7 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
const newVersion = await secretVersionV2BridgeDAL.create(
|
||||
{
|
||||
version: secretVersion.version + 1,
|
||||
version: nextVersion,
|
||||
encryptedValue: secretVersion.encryptedValue,
|
||||
key: secretVersion.key,
|
||||
encryptedComment: secretVersion.encryptedComment,
|
||||
@@ -611,14 +684,15 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
commitChanges.push({
|
||||
type: ChangeType.ADD,
|
||||
isUpdate: true,
|
||||
secretVersionId: newVersion.id
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
// Delete case remains unchanged
|
||||
case "delete":
|
||||
await secretV2BridgeDAL.deleteById(change.id, tx);
|
||||
|
||||
commitChanges.push({
|
||||
type: ChangeType.DELETE,
|
||||
secretVersionId: change.versionId
|
||||
@@ -724,8 +798,13 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
case "update":
|
||||
if (change.versionId) {
|
||||
const versionDetails = await folderVersionDAL.findById(change.versionId, tx);
|
||||
if (versionDetails) {
|
||||
const latestVersionDetails = await folderVersionDAL.findByIdsWithLatestVersion(
|
||||
[change.versionId],
|
||||
undefined,
|
||||
tx
|
||||
);
|
||||
if (latestVersionDetails && Object.keys(latestVersionDetails).length > 0) {
|
||||
const versionDetails = latestVersionDetails[0];
|
||||
await folderDAL.updateById(
|
||||
change.id,
|
||||
{
|
||||
@@ -749,6 +828,7 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
commitChanges.push({
|
||||
type: ChangeType.ADD,
|
||||
isUpdate: true,
|
||||
folderVersionId: newFolderVersion.id
|
||||
});
|
||||
}
|
||||
@@ -855,11 +935,103 @@ export const folderCommitServiceFactory = ({
|
||||
return folderCommitDAL.findByFolderId(folderId, tx);
|
||||
};
|
||||
|
||||
const getCommitsForFolder = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
environment,
|
||||
path
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
path: string;
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId
|
||||
});
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) {
|
||||
throw new NotFoundError({
|
||||
message: `Folder not found for project ID ${projectId}, environment ${environment}, path ${path}`
|
||||
});
|
||||
}
|
||||
const folderCommits = await folderCommitDAL.findByFolderId(folder.id);
|
||||
return folderCommits;
|
||||
};
|
||||
|
||||
const getCommitsCount = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
environment,
|
||||
path
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
path: string;
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId
|
||||
});
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) {
|
||||
throw new NotFoundError({
|
||||
message: `Folder not found for project ID ${projectId}, environment ${environment}, path ${path}`
|
||||
});
|
||||
}
|
||||
const folderCommits = await folderCommitDAL.findByFolderId(folder.id);
|
||||
return { count: folderCommits.length, folderId: folder.id };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get changes for a commit
|
||||
*/
|
||||
const getCommitChanges = async (commitId: string, tx?: Knex) => {
|
||||
return folderCommitChangesDAL.findByCommitId(commitId, tx);
|
||||
const getCommitChanges = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
commitId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
commitId: string;
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId
|
||||
});
|
||||
const changes = await folderCommitChangesDAL.findByCommitId(commitId);
|
||||
const commit = await folderCommitDAL.findById(commitId);
|
||||
return { ...commit, changes };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1021,6 +1193,100 @@ export const folderCommitServiceFactory = ({
|
||||
);
|
||||
};
|
||||
|
||||
const deepCompareFolder = async ({
|
||||
targetCommitId,
|
||||
envId,
|
||||
projectId,
|
||||
tx
|
||||
}: {
|
||||
targetCommitId: string;
|
||||
envId: string;
|
||||
actorId: string;
|
||||
actorType: ActorType;
|
||||
projectId: string;
|
||||
tx?: Knex;
|
||||
}) => {
|
||||
const targetCommit = await folderCommitDAL.findById(targetCommitId, tx);
|
||||
if (!targetCommit) {
|
||||
throw new NotFoundError({ message: `No commit found for commit ID ${targetCommitId}` });
|
||||
}
|
||||
|
||||
const checkpoint = await folderTreeCheckpointDAL.findNearestCheckpoint(targetCommitId, envId, tx);
|
||||
if (!checkpoint) {
|
||||
throw new NotFoundError({ message: `No checkpoint found for commit ID ${targetCommitId}` });
|
||||
}
|
||||
|
||||
const folderCheckpointCommits = await folderTreeCheckpointResourcesDAL.findByTreeCheckpointId(checkpoint.id, tx);
|
||||
const folderCommits = await folderCommitDAL.findAllCommitsBetween({
|
||||
envId,
|
||||
endCommitId: targetCommit.commitId.toString(),
|
||||
startCommitId: checkpoint.commitId.toString(),
|
||||
tx
|
||||
});
|
||||
|
||||
// Group commits by folderId and keep only the latest
|
||||
const folderGroups = new Map<string, { createdAt: Date; id: string }>();
|
||||
|
||||
if (folderCheckpointCommits && folderCheckpointCommits.length > 0) {
|
||||
for (const commit of folderCheckpointCommits) {
|
||||
folderGroups.set(commit.folderId, {
|
||||
createdAt: commit.createdAt,
|
||||
id: commit.folderCommitId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (folderCommits && folderCommits.length > 0) {
|
||||
for (const commit of folderCommits) {
|
||||
const { folderId, createdAt, id } = commit;
|
||||
const existingCommit = folderGroups.get(folderId);
|
||||
|
||||
if (!existingCommit || createdAt.getTime() > existingCommit.createdAt.getTime()) {
|
||||
folderGroups.set(folderId, { createdAt, id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const folderDiffs = new Map<string, ResourceChange[]>();
|
||||
|
||||
// Process each folder to determine differences
|
||||
await Promise.all(
|
||||
Array.from(folderGroups.entries()).map(async ([folderId, commit]) => {
|
||||
const latestFolderCommit = await folderCommitDAL.findLatestCommit(folderId, tx);
|
||||
if (latestFolderCommit && latestFolderCommit.id !== commit.id) {
|
||||
const diff = await compareFolderStates({
|
||||
currentCommitId: latestFolderCommit.id,
|
||||
targetCommitId: commit.id,
|
||||
tx
|
||||
});
|
||||
if (diff?.length > 0) {
|
||||
folderDiffs.set(folderId, diff);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Apply changes in hierarchical order
|
||||
const folderIds = Array.from(folderDiffs.keys());
|
||||
const folders = await folderDAL.findFoldersByRootAndIds({ rootId: targetCommit.folderId, folderIds }, tx);
|
||||
const sortedFolders = sortFoldersByHierarchy(folders);
|
||||
|
||||
const response = [];
|
||||
for (const folder of sortedFolders) {
|
||||
const diff = folderDiffs.get(folder.id);
|
||||
if (diff) {
|
||||
const folderPath = await folderDAL.findSecretPathByFolderIds(projectId, [folder.id]);
|
||||
response.push({
|
||||
folderId: folder.id,
|
||||
folderName: folder.name,
|
||||
changes: diff,
|
||||
folderPath: folderPath?.[0]?.path
|
||||
});
|
||||
}
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Roll back a folder tree to a specific commit
|
||||
*/
|
||||
@@ -1117,7 +1383,139 @@ export const folderCommitServiceFactory = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Return the public interface
|
||||
const getLatestCommit = async ({
|
||||
folderId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId
|
||||
}: {
|
||||
folderId: string;
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId
|
||||
});
|
||||
return folderCommitDAL.findLatestCommit(folderId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Revert changes made in a specific commit
|
||||
*/
|
||||
const revertCommitChanges = async ({
|
||||
commitId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
message = "Revert commit changes"
|
||||
}: {
|
||||
commitId: string;
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
message?: string;
|
||||
}) => {
|
||||
// Check permissions first
|
||||
await checkProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
|
||||
// Get the commit to revert
|
||||
const commitToRevert = await folderCommitDAL.findById(commitId);
|
||||
if (!commitToRevert) {
|
||||
throw new NotFoundError({ message: `Commit with ID ${commitId} not found` });
|
||||
}
|
||||
|
||||
// Get all commits for this folder
|
||||
const allCommits = await folderCommitDAL.findByFolderId(commitToRevert.folderId);
|
||||
if (!allCommits || allCommits.length === 0) {
|
||||
throw new NotFoundError({ message: `No commits found for folder ${commitToRevert.folderId}` });
|
||||
}
|
||||
|
||||
// Sort commits by commitId (which appears to be numeric)
|
||||
const sortedCommits = allCommits.sort((a, b) => a.commitId - b.commitId);
|
||||
|
||||
// Find the index of the commit to revert
|
||||
const commitIndex = sortedCommits.findIndex((c) => c.id === commitId);
|
||||
if (commitIndex === -1) {
|
||||
throw new NotFoundError({ message: `Commit ${commitId} not found in the commit history` });
|
||||
}
|
||||
|
||||
// If it's the first commit, we can't revert it (nothing before it)
|
||||
if (commitIndex === 0) {
|
||||
throw new BadRequestError({ message: "Cannot revert the first commit" });
|
||||
}
|
||||
|
||||
// Get the commit just before the one we want to revert
|
||||
const previousCommit = sortedCommits[commitIndex - 1];
|
||||
|
||||
// Calculate the changes needed to go from current commit back to the previous one
|
||||
const inverseChanges = await compareFolderStates({
|
||||
currentCommitId: commitToRevert.id,
|
||||
targetCommitId: previousCommit.id
|
||||
});
|
||||
|
||||
const latestCommit = await folderCommitDAL.findLatestCommit(commitToRevert.folderId);
|
||||
if (!latestCommit) {
|
||||
throw new NotFoundError({ message: `Latest commit not found for folder ${commitToRevert.folderId}` });
|
||||
}
|
||||
const currentState = await reconstructFolderState(latestCommit.id);
|
||||
|
||||
const filteredChanges = inverseChanges.filter(
|
||||
(change) =>
|
||||
((change.changeType === ChangeType.DELETE || change.changeType === ChangeType.UPDATE) &&
|
||||
(currentState.some((c) => c.id === change.id) || currentState.some((c) => c.id === change.id))) ||
|
||||
(change.changeType === ChangeType.CREATE &&
|
||||
(currentState.every((c) => c.id !== change.id) || currentState.every((c) => c.id !== change.id)))
|
||||
);
|
||||
|
||||
if (!filteredChanges || filteredChanges.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: "No changes to revert",
|
||||
originalCommitId: commitId
|
||||
};
|
||||
}
|
||||
|
||||
// Apply the changes to revert the commit
|
||||
const revertResult = await applyFolderStateDifferences({
|
||||
differences: filteredChanges,
|
||||
actorInfo: {
|
||||
actorType: actor,
|
||||
actorId,
|
||||
message: message || `Reverted changes from commit ${commitId}`
|
||||
},
|
||||
folderId: commitToRevert.folderId,
|
||||
projectId,
|
||||
reconstructNewFolders: false
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Changes reverted successfully",
|
||||
originalCommitId: commitId,
|
||||
revertCommitId: latestCommit?.id,
|
||||
changesReverted: revertResult.totalChanges
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createCommit,
|
||||
addCommitChange,
|
||||
@@ -1132,7 +1530,13 @@ export const folderCommitServiceFactory = ({
|
||||
compareFolderStates,
|
||||
applyFolderStateDifferences,
|
||||
createFolderTreeCheckpoint,
|
||||
deepRollbackFolder
|
||||
deepRollbackFolder,
|
||||
getCommitsCount,
|
||||
getLatestCommit,
|
||||
deepCompareFolder,
|
||||
reconstructFolderState,
|
||||
getCommitsForFolder,
|
||||
revertCommitChanges
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ type TSecretFolderServiceFactoryDep = {
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
folderDAL: TSecretFolderDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
|
||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestFolderVersions" | "create" | "insertMany">;
|
||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestFolderVersions" | "create" | "insertMany" | "find">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
};
|
||||
@@ -286,6 +286,7 @@ export const secretFolderServiceFactory = ({
|
||||
changes: [
|
||||
{
|
||||
type: "add",
|
||||
isUpdate: true,
|
||||
folderVersionId: folderVersion.id
|
||||
}
|
||||
]
|
||||
@@ -401,6 +402,7 @@ export const secretFolderServiceFactory = ({
|
||||
changes: [
|
||||
{
|
||||
type: "add",
|
||||
isUpdate: true,
|
||||
folderVersionId: folderVersion.id
|
||||
}
|
||||
]
|
||||
@@ -765,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,
|
||||
@@ -775,6 +793,7 @@ export const secretFolderServiceFactory = ({
|
||||
getProjectFolderCount,
|
||||
getFoldersMultiEnv,
|
||||
getFoldersDeepByEnvs,
|
||||
getProjectEnvironmentsFolders
|
||||
getProjectEnvironmentsFolders,
|
||||
getFolderVersionsByIds
|
||||
};
|
||||
};
|
||||
|
||||
@@ -148,7 +148,7 @@ export const fnSecretBulkInsert = async ({
|
||||
id: actor?.actorId
|
||||
}
|
||||
},
|
||||
message: "Secret Creation",
|
||||
message: "Secret Created",
|
||||
folderId,
|
||||
changes: commitChanges
|
||||
},
|
||||
@@ -290,6 +290,7 @@ export const fnSecretBulkUpdate = async ({
|
||||
.filter(({ type }) => type === SecretType.Shared)
|
||||
.map((sv) => ({
|
||||
type: "add",
|
||||
isUpdate: true,
|
||||
secretVersionId: sv.id
|
||||
}));
|
||||
if (commitChanges.length > 0) {
|
||||
@@ -301,7 +302,7 @@ export const fnSecretBulkUpdate = async ({
|
||||
id: actor?.actorId
|
||||
}
|
||||
},
|
||||
message: "Secret Update",
|
||||
message: "Secret Updated",
|
||||
folderId,
|
||||
changes: commitChanges
|
||||
},
|
||||
@@ -432,7 +433,7 @@ export const fnSecretBulkDelete = async ({
|
||||
id: actorId
|
||||
}
|
||||
},
|
||||
message: "Secret Delete",
|
||||
message: "Secret Deleted",
|
||||
folderId,
|
||||
changes: commitChanges
|
||||
},
|
||||
|
||||
@@ -2178,10 +2178,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: folder.projectId
|
||||
});
|
||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors(secretId, folder.projectId, {
|
||||
offset,
|
||||
limit,
|
||||
sort: [["createdAt", "desc"]]
|
||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
|
||||
secretId,
|
||||
projectId: folder.projectId,
|
||||
findOpt: {
|
||||
offset,
|
||||
limit,
|
||||
sort: [["createdAt", "desc"]]
|
||||
}
|
||||
});
|
||||
return secretVersions.map((el) => {
|
||||
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||
@@ -2856,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,
|
||||
@@ -2874,6 +2945,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
getSecretReferenceTree,
|
||||
getSecretsByFolderMappings,
|
||||
getSecretById,
|
||||
getAccessibleSecrets
|
||||
getAccessibleSecrets,
|
||||
getSecretVersionsByIds
|
||||
};
|
||||
};
|
||||
|
||||
@@ -173,13 +173,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(
|
||||
@@ -202,10 +210,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),
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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>;
|
||||
|
||||
2
frontend/src/hooks/api/folderCommits/index.tsx
Normal file
2
frontend/src/hooks/api/folderCommits/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useGetFolderCommitHistory, useGetFolderCommitsCount } from "./queries";
|
||||
export * from "./types";
|
||||
247
frontend/src/hooks/api/folderCommits/queries.tsx
Normal file
247
frontend/src/hooks/api/folderCommits/queries.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
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
|
||||
) => {
|
||||
const { data } = await apiRequest.post<{ success: boolean }>(
|
||||
`/api/v1/pit/commits/${projectId}/${commitId}/rollback`,
|
||||
{
|
||||
folderId,
|
||||
deepRollback,
|
||||
message
|
||||
}
|
||||
);
|
||||
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
|
||||
}: {
|
||||
workspaceId: string;
|
||||
commitId: string;
|
||||
folderId: string;
|
||||
deepRollback: boolean;
|
||||
environment: string;
|
||||
directory: string;
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (message: string) =>
|
||||
fetchRollback(folderId, commitId, workspaceId, deepRollback, message),
|
||||
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)
|
||||
});
|
||||
};
|
||||
63
frontend/src/hooks/api/folderCommits/types.ts
Normal file
63
frontend/src/hooks/api/folderCommits/types.ts
Normal file
@@ -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,376 @@
|
||||
/* 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
|
||||
});
|
||||
|
||||
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";
|
||||
1
frontend/src/pages/secret-manager/CommitsPage/index.tsx
Normal file
1
frontend/src/pages/secret-manager/CommitsPage/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { CommitsPage } from "./CommitsPage";
|
||||
71
frontend/src/pages/secret-manager/CommitsPage/route.tsx
Normal file
71
frontend/src/pages/secret-manager/CommitsPage/route.tsx
Normal file
@@ -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 })
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user