mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 13:28:27 +00:00
PIT: general improvements and fixes
This commit is contained in:
Vendored
+2
@@ -26,6 +26,7 @@ import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-con
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { TPitServiceFactory } from "@app/ee/services/pit/pit-service";
|
||||||
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
||||||
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
||||||
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
|
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
|
||||||
@@ -270,6 +271,7 @@ declare module "fastify" {
|
|||||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||||
folderCommit: TFolderCommitServiceFactory;
|
folderCommit: TFolderCommitServiceFactory;
|
||||||
|
pit: TPitServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-base-to-string */
|
/* eslint-disable @typescript-eslint/no-base-to-string */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ProjectPermissionCommitsActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
|
||||||
import { NotFoundError } from "@app/lib/errors";
|
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { booleanSchema } from "@app/server/routes/sanitizedSchemas";
|
import { booleanSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { ActorAuthMethod, ActorType, AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { ChangeType } from "@app/services/folder-commit/folder-commit-service";
|
import { commitChangesResponseSchema, resourceChangeSchema } from "@app/services/folder-commit/folder-commit-schemas";
|
||||||
import { commitChangesResponseSchema } from "@app/services/folder-commit/folder-commit-types";
|
|
||||||
|
|
||||||
const commitHistoryItemSchema = z.object({
|
const commitHistoryItemSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -49,7 +44,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -60,30 +55,30 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const res = await server.services.folderCommit.getCommitsCount({
|
const result = await server.services.pit.getCommitsCount({
|
||||||
actor: req.permission?.type,
|
actor: req.permission?.type,
|
||||||
actorId: req.permission?.id,
|
actorId: req.permission?.id,
|
||||||
actorOrgId: req.permission?.orgId,
|
actorOrgId: req.permission?.orgId,
|
||||||
actorAuthMethod: req.permission?.authMethod,
|
actorAuthMethod: req.permission?.authMethod,
|
||||||
projectId: req.query.workspaceId,
|
projectId: req.query.projectId,
|
||||||
environment: req.query.environment,
|
environment: req.query.environment,
|
||||||
path: req.query.path
|
path: req.query.path
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.query.workspaceId,
|
projectId: req.query.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_PROJECT_PIT_COMMIT_COUNT,
|
type: EventType.GET_PROJECT_PIT_COMMIT_COUNT,
|
||||||
metadata: {
|
metadata: {
|
||||||
environment: req.query.environment,
|
environment: req.query.environment,
|
||||||
path: req.query.path,
|
path: req.query.path,
|
||||||
commitCount: res.count.toString()
|
commitCount: result.count.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return res;
|
return result;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,7 +93,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
workspaceId: z.string().trim(),
|
projectId: z.string().trim(),
|
||||||
offset: z.coerce.number().min(0).default(0),
|
offset: z.coerce.number().min(0).default(0),
|
||||||
limit: z.coerce.number().min(1).max(100).default(20),
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
search: z.string().trim().optional(),
|
search: z.string().trim().optional(),
|
||||||
@@ -114,12 +109,12 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const result = await server.services.folderCommit.getCommitsForFolder({
|
const result = await server.services.pit.getCommitsForFolder({
|
||||||
actor: req.permission?.type,
|
actor: req.permission?.type,
|
||||||
actorId: req.permission?.id,
|
actorId: req.permission?.id,
|
||||||
actorOrgId: req.permission?.orgId,
|
actorOrgId: req.permission?.orgId,
|
||||||
actorAuthMethod: req.permission?.authMethod,
|
actorAuthMethod: req.permission?.authMethod,
|
||||||
projectId: req.query.workspaceId,
|
projectId: req.query.projectId,
|
||||||
environment: req.query.environment,
|
environment: req.query.environment,
|
||||||
path: req.query.path,
|
path: req.query.path,
|
||||||
offset: req.query.offset,
|
offset: req.query.offset,
|
||||||
@@ -130,7 +125,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.query.workspaceId,
|
projectId: req.query.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_PROJECT_PIT_COMMITS,
|
type: EventType.GET_PROJECT_PIT_COMMITS,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -145,87 +140,10 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return result;
|
||||||
commits: result.commits.map((commit) => ({
|
|
||||||
...commit,
|
|
||||||
commitId: commit.commitId.toString()
|
|
||||||
})),
|
|
||||||
total: result.total,
|
|
||||||
hasMore: result.hasMore
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const getChangeVersions = async (
|
|
||||||
change: {
|
|
||||||
secretVersion?: string;
|
|
||||||
secretId?: string;
|
|
||||||
id?: string;
|
|
||||||
isUpdate?: boolean;
|
|
||||||
changeType?: string;
|
|
||||||
},
|
|
||||||
previousVersion: string,
|
|
||||||
actorId: string,
|
|
||||||
actor: ActorType,
|
|
||||||
actorOrgId: string,
|
|
||||||
actorAuthMethod: ActorAuthMethod,
|
|
||||||
folderId: string
|
|
||||||
) => {
|
|
||||||
if (change.secretVersion) {
|
|
||||||
const currentVersion = change.secretVersion || "1";
|
|
||||||
const secretId = change.secretId ? change.secretId : change.id;
|
|
||||||
if (!secretId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const versions = await server.services.secret.getSecretVersionsV2ByIds({
|
|
||||||
actorId,
|
|
||||||
actor,
|
|
||||||
actorOrgId,
|
|
||||||
actorAuthMethod,
|
|
||||||
secretId,
|
|
||||||
// if it's update add also the previous secretversionid
|
|
||||||
secretVersions:
|
|
||||||
change.isUpdate || change.changeType === ChangeType.UPDATE
|
|
||||||
? [currentVersion, previousVersion]
|
|
||||||
: [currentVersion],
|
|
||||||
folderId
|
|
||||||
});
|
|
||||||
return 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
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFolderVersions = async (
|
|
||||||
change: {
|
|
||||||
folderVersion?: string;
|
|
||||||
isUpdate?: boolean;
|
|
||||||
changeType?: string;
|
|
||||||
},
|
|
||||||
fromVersion: string,
|
|
||||||
folderId: string
|
|
||||||
) => {
|
|
||||||
const currentVersion = change.folderVersion || "1";
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const versions = await server.services.folder.getFolderVersionsByIds({
|
|
||||||
folderId,
|
|
||||||
folderVersions:
|
|
||||||
change.isUpdate || change.changeType === ChangeType.UPDATE ? [currentVersion, fromVersion] : [currentVersion]
|
|
||||||
});
|
|
||||||
return versions.map((v) => ({
|
|
||||||
name: v.name
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get commit changes for a specific commit
|
// Get commit changes for a specific commit
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -238,7 +156,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
commitId: z.string().trim()
|
commitId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: commitChangesResponseSchema
|
200: commitChangesResponseSchema
|
||||||
@@ -246,54 +164,28 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const changes = await server.services.folderCommit.getCommitChanges({
|
const result = await server.services.pit.getCommitChanges({
|
||||||
actor: req.permission?.type,
|
actor: req.permission?.type,
|
||||||
actorId: req.permission?.id,
|
actorId: req.permission?.id,
|
||||||
actorOrgId: req.permission?.orgId,
|
actorOrgId: req.permission?.orgId,
|
||||||
actorAuthMethod: req.permission?.authMethod,
|
actorAuthMethod: req.permission?.authMethod,
|
||||||
projectId: req.query.workspaceId,
|
projectId: req.query.projectId,
|
||||||
commitId: req.params.commitId
|
commitId: req.params.commitId
|
||||||
});
|
});
|
||||||
for (const change of changes.changes) {
|
|
||||||
if (change.secretVersionId && change.secretVersion) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
change.versions = await getChangeVersions(
|
|
||||||
change,
|
|
||||||
(Number.parseInt(change.secretVersion, 10) - 1).toString(),
|
|
||||||
req.permission.id,
|
|
||||||
req.permission.type,
|
|
||||||
req.permission.orgId,
|
|
||||||
req.permission.authMethod,
|
|
||||||
change.folderId
|
|
||||||
);
|
|
||||||
} else if (change.folderVersionId && change.folderChangeId && change.folderVersion) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
change.versions = await getFolderVersions(
|
|
||||||
change,
|
|
||||||
(Number.parseInt(change.folderVersion, 10) - 1).toString(),
|
|
||||||
change.folderChangeId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.query.workspaceId,
|
projectId: req.query.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_PROJECT_PIT_COMMIT_CHANGES,
|
type: EventType.GET_PROJECT_PIT_COMMIT_CHANGES,
|
||||||
metadata: {
|
metadata: {
|
||||||
commitId: req.params.commitId,
|
commitId: req.params.commitId,
|
||||||
changesCount: (changes.changes?.length || 0).toString()
|
changesCount: (result.changes.changes?.length || 0).toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return result;
|
||||||
changes: {
|
|
||||||
...changes,
|
|
||||||
commitId: changes.commitId.toString()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -313,7 +205,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
envId: z.string().trim(),
|
envId: z.string().trim(),
|
||||||
deepRollback: booleanSchema.default(false),
|
deepRollback: booleanSchema.default(false),
|
||||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.array(
|
200: z.array(
|
||||||
@@ -321,89 +213,43 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
folderId: z.string(),
|
folderId: z.string(),
|
||||||
folderName: z.string(),
|
folderName: z.string(),
|
||||||
folderPath: z.string().optional(),
|
folderPath: z.string().optional(),
|
||||||
changes: z.any()
|
changes: z.array(resourceChangeSchema)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const latestCommit = await server.services.folderCommit.getLatestCommit({
|
const result = await server.services.pit.compareCommitChanges({
|
||||||
folderId: req.query.folderId,
|
|
||||||
actor: req.permission?.type,
|
actor: req.permission?.type,
|
||||||
actorId: req.permission?.id,
|
actorId: req.permission?.id,
|
||||||
actorOrgId: req.permission?.orgId,
|
actorOrgId: req.permission?.orgId,
|
||||||
actorAuthMethod: req.permission?.authMethod,
|
actorAuthMethod: req.permission?.authMethod,
|
||||||
projectId: req.query.workspaceId
|
projectId: req.query.projectId,
|
||||||
|
commitId: req.params.commitId,
|
||||||
|
folderId: req.query.folderId,
|
||||||
|
envId: req.query.envId,
|
||||||
|
deepRollback: req.query.deepRollback,
|
||||||
|
secretPath: req.query.secretPath
|
||||||
});
|
});
|
||||||
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,
|
|
||||||
projectId: req.query.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) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
change.versions = await getChangeVersions(
|
|
||||||
change,
|
|
||||||
change.fromVersion || "1",
|
|
||||||
req.permission.id,
|
|
||||||
req.permission.type,
|
|
||||||
req.permission.orgId,
|
|
||||||
req.permission.authMethod,
|
|
||||||
diff.folderId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (change.folderVersion) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
change.versions = await getFolderVersions(change, change.fromVersion || "1", change.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.query.workspaceId,
|
projectId: req.query.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.PIT_COMPARE_FOLDER_STATES,
|
type: EventType.PIT_COMPARE_FOLDER_STATES,
|
||||||
metadata: {
|
metadata: {
|
||||||
targetCommitId: req.params.commitId,
|
targetCommitId: req.params.commitId,
|
||||||
folderId: req.query.folderId,
|
folderId: req.query.folderId,
|
||||||
deepRollback: req.query.deepRollback,
|
deepRollback: req.query.deepRollback,
|
||||||
diffsCount: diffs.length.toString()
|
diffsCount: result.length.toString(),
|
||||||
|
env: req.query.envId,
|
||||||
|
folderPath: req.query.secretPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return diffs;
|
return result;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -423,7 +269,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
deepRollback: z.boolean().default(false),
|
deepRollback: z.boolean().default(false),
|
||||||
message: z.string().trim().optional(),
|
message: z.string().trim().optional(),
|
||||||
envId: z.string().trim(),
|
envId: z.string().trim(),
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -436,79 +282,36 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { permission } = await server.services.permission.getProjectPermission({
|
const result = await server.services.pit.rollbackToCommit({
|
||||||
actor: req.permission?.type,
|
|
||||||
actorId: req.permission?.id,
|
|
||||||
projectId: req.body.workspaceId,
|
|
||||||
actorAuthMethod: req.permission?.authMethod,
|
|
||||||
actorOrgId: req.permission?.orgId,
|
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionCommitsActions.PerformRollback,
|
|
||||||
ProjectPermissionSub.Commits
|
|
||||||
);
|
|
||||||
const latestCommit = await server.services.folderCommit.getLatestCommit({
|
|
||||||
folderId: req.body.folderId,
|
|
||||||
actor: req.permission?.type,
|
actor: req.permission?.type,
|
||||||
actorId: req.permission?.id,
|
actorId: req.permission?.id,
|
||||||
actorOrgId: req.permission?.orgId,
|
actorOrgId: req.permission?.orgId,
|
||||||
actorAuthMethod: req.permission?.authMethod,
|
actorAuthMethod: req.permission?.authMethod,
|
||||||
projectId: req.body.workspaceId
|
projectId: req.body.projectId,
|
||||||
});
|
commitId: req.params.commitId,
|
||||||
if (!latestCommit) {
|
|
||||||
throw new NotFoundError({ message: "Latest commit not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.body.deepRollback) {
|
|
||||||
await server.services.folderCommit.deepRollbackFolder(
|
|
||||||
req.params.commitId,
|
|
||||||
req.body.envId,
|
|
||||||
req.permission.id,
|
|
||||||
req.permission.type,
|
|
||||||
req.body.workspaceId
|
|
||||||
);
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
const diff = await server.services.folderCommit.compareFolderStates({
|
|
||||||
currentCommitId: latestCommit.id,
|
|
||||||
targetCommitId: req.params.commitId
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await server.services.folderCommit.applyFolderStateDifferences({
|
|
||||||
differences: diff,
|
|
||||||
actorInfo: {
|
|
||||||
actorType: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
message: req.body.message || "Rollback to previous commit"
|
|
||||||
},
|
|
||||||
folderId: req.body.folderId,
|
folderId: req.body.folderId,
|
||||||
projectId: req.body.workspaceId,
|
deepRollback: req.body.deepRollback,
|
||||||
reconstructNewFolders: req.body.deepRollback
|
message: req.body.message,
|
||||||
|
envId: req.body.envId
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.body.workspaceId,
|
projectId: req.body.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.PIT_ROLLBACK_COMMIT,
|
type: EventType.PIT_ROLLBACK_COMMIT,
|
||||||
metadata: {
|
metadata: {
|
||||||
targetCommitId: req.params.commitId,
|
targetCommitId: req.params.commitId,
|
||||||
|
envId: req.body.envId,
|
||||||
folderId: req.body.folderId,
|
folderId: req.body.folderId,
|
||||||
deepRollback: req.body.deepRollback,
|
deepRollback: req.body.deepRollback,
|
||||||
message: req.body.message || "Rollback to previous commit",
|
message: req.body.message || "Rollback to previous commit",
|
||||||
totalChanges: response.totalChanges.toString()
|
totalChanges: result.totalChanges?.toString() || "0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return result;
|
||||||
success: true,
|
|
||||||
secretChangesCount: response.secretChangesCount,
|
|
||||||
folderChangesCount: response.folderChangesCount,
|
|
||||||
totalChanges: response.totalChanges
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -523,8 +326,8 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
commitId: z.string().trim()
|
commitId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
body: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -538,29 +341,29 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const response = await server.services.folderCommit.revertCommitChanges({
|
const result = await server.services.pit.revertCommit({
|
||||||
commitId: req.params.commitId,
|
|
||||||
actor: req.permission?.type,
|
actor: req.permission?.type,
|
||||||
actorId: req.permission?.id,
|
actorId: req.permission?.id,
|
||||||
actorAuthMethod: req.permission?.authMethod,
|
|
||||||
actorOrgId: req.permission?.orgId,
|
actorOrgId: req.permission?.orgId,
|
||||||
projectId: req.query.workspaceId
|
actorAuthMethod: req.permission?.authMethod,
|
||||||
|
projectId: req.body.projectId,
|
||||||
|
commitId: req.params.commitId
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.query.workspaceId,
|
projectId: req.body.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.PIT_REVERT_COMMIT,
|
type: EventType.PIT_REVERT_COMMIT,
|
||||||
metadata: {
|
metadata: {
|
||||||
commitId: req.params.commitId,
|
commitId: req.params.commitId,
|
||||||
revertCommitId: response.revertCommitId,
|
revertCommitId: result.revertCommitId,
|
||||||
changesReverted: response.changesReverted?.toString()
|
changesReverted: result.changesReverted?.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return result;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -577,7 +380,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
folderId: z.string().trim(),
|
folderId: z.string().trim(),
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: folderStateSchema
|
200: folderStateSchema
|
||||||
@@ -585,39 +388,29 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { permission } = await server.services.permission.getProjectPermission({
|
const result = await server.services.pit.getFolderStateAtCommit({
|
||||||
actor: req.permission?.type,
|
actor: req.permission?.type,
|
||||||
actorId: req.permission?.id,
|
actorId: req.permission?.id,
|
||||||
projectId: req.query.workspaceId,
|
|
||||||
actorAuthMethod: req.permission?.authMethod,
|
|
||||||
actorOrgId: req.permission?.orgId,
|
actorOrgId: req.permission?.orgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actorAuthMethod: req.permission?.authMethod,
|
||||||
|
projectId: req.query.projectId,
|
||||||
|
commitId: req.params.commitId
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionCommitsActions.Read,
|
|
||||||
ProjectPermissionSub.Commits
|
|
||||||
);
|
|
||||||
const response = await server.services.folderCommit.reconstructFolderState(req.params.commitId);
|
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.query.workspaceId,
|
projectId: req.query.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.PIT_GET_FOLDER_STATE,
|
type: EventType.PIT_GET_FOLDER_STATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
commitId: req.params.commitId,
|
commitId: req.params.commitId,
|
||||||
folderId: req.query.folderId,
|
folderId: req.query.folderId,
|
||||||
resourceCount: response.length.toString()
|
resourceCount: result.length.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.map((item) => ({
|
return result;
|
||||||
...item,
|
|
||||||
secretVersion: item.secretVersion ? Number(item.secretVersion) : undefined,
|
|
||||||
folderVersion: item.folderVersion ? Number(item.folderVersion) : undefined
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2959,6 +2959,7 @@ interface PitRollbackCommitEvent {
|
|||||||
deepRollback: boolean;
|
deepRollback: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
totalChanges: string;
|
totalChanges: string;
|
||||||
|
envId: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2987,6 +2988,8 @@ interface PitCompareFolderStatesEvent {
|
|||||||
folderId: string;
|
folderId: string;
|
||||||
deepRollback: boolean;
|
deepRollback: boolean;
|
||||||
diffsCount: string;
|
diffsCount: string;
|
||||||
|
env: string;
|
||||||
|
folderPath: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,465 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
|
import { ProjectPermissionCommitsActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { ResourceType, TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||||
|
import {
|
||||||
|
isFolderCommitChange,
|
||||||
|
isSecretCommitChange
|
||||||
|
} from "@app/services/folder-commit-changes/folder-commit-changes-dal";
|
||||||
|
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
|
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
|
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
|
||||||
|
type TPitServiceFactoryDep = {
|
||||||
|
folderCommitService: TFolderCommitServiceFactory;
|
||||||
|
secretService: Pick<TSecretServiceFactory, "getSecretVersionsV2ByIds" | "getChangeVersions">;
|
||||||
|
folderService: Pick<TSecretFolderServiceFactory, "getFolderById" | "getFolderVersions">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPitServiceFactory = ReturnType<typeof pitServiceFactory>;
|
||||||
|
|
||||||
|
export const pitServiceFactory = ({
|
||||||
|
folderCommitService,
|
||||||
|
secretService,
|
||||||
|
folderService,
|
||||||
|
permissionService
|
||||||
|
}: TPitServiceFactoryDep) => {
|
||||||
|
const getCommitsCount = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
path
|
||||||
|
}: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
}) => {
|
||||||
|
const result = await folderCommitService.getCommitsCount({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
path
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCommitsForFolder = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
path,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
search,
|
||||||
|
sort
|
||||||
|
}: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
search?: string;
|
||||||
|
sort: "asc" | "desc";
|
||||||
|
}) => {
|
||||||
|
const result = await folderCommitService.getCommitsForFolder({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
path,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
search,
|
||||||
|
sort
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
commits: result.commits.map((commit) => ({
|
||||||
|
...commit,
|
||||||
|
commitId: commit.commitId.toString()
|
||||||
|
})),
|
||||||
|
total: result.total,
|
||||||
|
hasMore: result.hasMore
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCommitChanges = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
commitId
|
||||||
|
}: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
projectId: string;
|
||||||
|
commitId: string;
|
||||||
|
}) => {
|
||||||
|
const changes = await folderCommitService.getCommitChanges({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
commitId
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const change of changes.changes) {
|
||||||
|
if (isSecretCommitChange(change)) {
|
||||||
|
change.versions = await secretService.getChangeVersions(
|
||||||
|
{
|
||||||
|
secretVersion: change.secretVersion,
|
||||||
|
secretId: change.secretId,
|
||||||
|
id: change.id,
|
||||||
|
isUpdate: change.isUpdate,
|
||||||
|
changeType: change.changeType
|
||||||
|
},
|
||||||
|
(Number.parseInt(change.secretVersion, 10) - 1).toString(),
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
change.folderId
|
||||||
|
);
|
||||||
|
} else if (isFolderCommitChange(change)) {
|
||||||
|
change.versions = await folderService.getFolderVersions(
|
||||||
|
change,
|
||||||
|
(Number.parseInt(change.folderVersion, 10) - 1).toString(),
|
||||||
|
change.folderChangeId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
changes: {
|
||||||
|
...changes,
|
||||||
|
commitId: changes.commitId.toString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const compareCommitChanges = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
commitId,
|
||||||
|
folderId,
|
||||||
|
envId,
|
||||||
|
deepRollback,
|
||||||
|
secretPath
|
||||||
|
}: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
projectId: string;
|
||||||
|
commitId: string;
|
||||||
|
folderId: string;
|
||||||
|
envId: string;
|
||||||
|
deepRollback: boolean;
|
||||||
|
secretPath: string;
|
||||||
|
}) => {
|
||||||
|
const latestCommit = await folderCommitService.getLatestCommit({
|
||||||
|
folderId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetCommit = await folderCommitService.getCommitById({
|
||||||
|
commitId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!latestCommit) {
|
||||||
|
throw new NotFoundError({ message: "Latest commit not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let diffs;
|
||||||
|
if (deepRollback) {
|
||||||
|
diffs = await folderCommitService.deepCompareFolder({
|
||||||
|
targetCommitId: targetCommit.id,
|
||||||
|
envId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const folderData = await folderService.getFolderById({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
id: folderId
|
||||||
|
});
|
||||||
|
|
||||||
|
diffs = [
|
||||||
|
{
|
||||||
|
folderId: folderData.id,
|
||||||
|
folderName: folderData.name,
|
||||||
|
folderPath: secretPath,
|
||||||
|
changes: await folderCommitService.compareFolderStates({
|
||||||
|
targetCommitId: commitId,
|
||||||
|
currentCommitId: latestCommit.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const diff of diffs) {
|
||||||
|
for (const change of diff.changes) {
|
||||||
|
// Use discriminated union type checking
|
||||||
|
if (change.type === ResourceType.SECRET) {
|
||||||
|
// TypeScript now knows this is a SecretChange
|
||||||
|
if (change.secretKey && change.secretVersion && change.secretId) {
|
||||||
|
change.versions = await secretService.getChangeVersions(
|
||||||
|
{
|
||||||
|
secretVersion: change.secretVersion,
|
||||||
|
secretId: change.secretId,
|
||||||
|
id: change.id,
|
||||||
|
isUpdate: change.isUpdate,
|
||||||
|
changeType: change.changeType
|
||||||
|
},
|
||||||
|
change.fromVersion || "1",
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
diff.folderId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (change.type === ResourceType.FOLDER) {
|
||||||
|
// TypeScript now knows this is a FolderChange
|
||||||
|
if (change.folderVersion) {
|
||||||
|
change.versions = await folderService.getFolderVersions(change, change.fromVersion || "1", change.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return diffs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rollbackToCommit = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
commitId,
|
||||||
|
folderId,
|
||||||
|
deepRollback,
|
||||||
|
message,
|
||||||
|
envId
|
||||||
|
}: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
projectId: string;
|
||||||
|
commitId: string;
|
||||||
|
folderId: string;
|
||||||
|
deepRollback: boolean;
|
||||||
|
message?: string;
|
||||||
|
envId: string;
|
||||||
|
}) => {
|
||||||
|
const { permission: userPermission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(userPermission).throwUnlessCan(
|
||||||
|
ProjectPermissionCommitsActions.PerformRollback,
|
||||||
|
ProjectPermissionSub.Commits
|
||||||
|
);
|
||||||
|
|
||||||
|
const latestCommit = await folderCommitService.getLatestCommit({
|
||||||
|
folderId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!latestCommit) {
|
||||||
|
throw new NotFoundError({ message: "Latest commit not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`PIT - Attempting to rollback folder ${folderId} from commit ${latestCommit.id} to commit ${commitId}`);
|
||||||
|
|
||||||
|
const targetCommit = await folderCommitService.getCommitById({
|
||||||
|
commitId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetCommit || targetCommit.folderId !== folderId || targetCommit.envId !== envId) {
|
||||||
|
throw new NotFoundError({ message: "Target commit not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!latestCommit || latestCommit.envId !== envId) {
|
||||||
|
throw new NotFoundError({ message: "Latest commit not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deepRollback) {
|
||||||
|
await folderCommitService.deepRollbackFolder(commitId, envId, actorId, actor, projectId, message);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = await folderCommitService.compareFolderStates({
|
||||||
|
currentCommitId: latestCommit.id,
|
||||||
|
targetCommitId: commitId
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await folderCommitService.applyFolderStateDifferences({
|
||||||
|
differences: diff,
|
||||||
|
actorInfo: {
|
||||||
|
actorType: actor,
|
||||||
|
actorId,
|
||||||
|
message: message || "Rollback to previous commit"
|
||||||
|
},
|
||||||
|
folderId,
|
||||||
|
projectId,
|
||||||
|
reconstructNewFolders: deepRollback
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
secretChangesCount: response.secretChangesCount,
|
||||||
|
folderChangesCount: response.folderChangesCount,
|
||||||
|
totalChanges: response.totalChanges
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const revertCommit = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
commitId
|
||||||
|
}: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
projectId: string;
|
||||||
|
commitId: string;
|
||||||
|
}) => {
|
||||||
|
const response = await folderCommitService.revertCommitChanges({
|
||||||
|
commitId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFolderStateAtCommit = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
commitId
|
||||||
|
}: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
projectId: string;
|
||||||
|
commitId: string;
|
||||||
|
}) => {
|
||||||
|
const { permission: userPermission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(userPermission).throwUnlessCan(
|
||||||
|
ProjectPermissionCommitsActions.Read,
|
||||||
|
ProjectPermissionSub.Commits
|
||||||
|
);
|
||||||
|
|
||||||
|
const commit = await folderCommitService.getCommitById({
|
||||||
|
commitId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!commit) {
|
||||||
|
throw new NotFoundError({ message: `Commit with ID ${commitId} not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await folderCommitService.reconstructFolderState(commitId);
|
||||||
|
|
||||||
|
return response.map((item) => ({
|
||||||
|
...item,
|
||||||
|
secretVersion: item.secretVersion ? Number(item.secretVersion) : undefined,
|
||||||
|
folderVersion: item.folderVersion ? Number(item.folderVersion) : undefined
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCommitsCount,
|
||||||
|
getCommitsForFolder,
|
||||||
|
getCommitChanges,
|
||||||
|
compareCommitChanges,
|
||||||
|
rollbackToCommit,
|
||||||
|
revertCommit,
|
||||||
|
getFolderStateAtCommit
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@ import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
import { CommitType, TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -584,7 +584,7 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
// Secret was deleted and re-added - this is an update only if versions are different
|
// Secret was deleted and re-added - this is an update only if versions are different
|
||||||
if (deletedInfo.versionId !== addedSecret.id) {
|
if (deletedInfo.versionId !== addedSecret.id) {
|
||||||
commitChanges.push({
|
commitChanges.push({
|
||||||
type: "add", // In the commit system, updates are tracked as "add" with isUpdate=true
|
type: CommitType.ADD, // In the commit system, updates are tracked as "add" with isUpdate=true
|
||||||
secretVersionId: addedSecret.id,
|
secretVersionId: addedSecret.id,
|
||||||
isUpdate: true
|
isUpdate: true
|
||||||
});
|
});
|
||||||
@@ -594,7 +594,7 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
} else if (deletedInfo.versionId) {
|
} else if (deletedInfo.versionId) {
|
||||||
// Secret was only deleted
|
// Secret was only deleted
|
||||||
commitChanges.push({
|
commitChanges.push({
|
||||||
type: "delete",
|
type: CommitType.DELETE,
|
||||||
secretVersionId: deletedInfo.versionId
|
secretVersionId: deletedInfo.versionId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -602,7 +602,7 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
// Add remaining new secrets (not updates)
|
// Add remaining new secrets (not updates)
|
||||||
addedSecretsChanges.forEach((addedSecret) => {
|
addedSecretsChanges.forEach((addedSecret) => {
|
||||||
commitChanges.push({
|
commitChanges.push({
|
||||||
type: "add",
|
type: CommitType.ADD,
|
||||||
secretVersionId: addedSecret.id
|
secretVersionId: addedSecret.id
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -614,7 +614,7 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
// Folder was deleted and re-added - this is an update only if versions are different
|
// Folder was deleted and re-added - this is an update only if versions are different
|
||||||
if (deletedInfo.versionId !== addedFolder.id) {
|
if (deletedInfo.versionId !== addedFolder.id) {
|
||||||
commitChanges.push({
|
commitChanges.push({
|
||||||
type: "add",
|
type: CommitType.ADD,
|
||||||
folderVersionId: addedFolder.id,
|
folderVersionId: addedFolder.id,
|
||||||
isUpdate: true
|
isUpdate: true
|
||||||
});
|
});
|
||||||
@@ -624,7 +624,7 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
} else if (deletedInfo.versionId) {
|
} else if (deletedInfo.versionId) {
|
||||||
// Folder was only deleted
|
// Folder was only deleted
|
||||||
commitChanges.push({
|
commitChanges.push({
|
||||||
type: "delete",
|
type: CommitType.DELETE,
|
||||||
folderVersionId: deletedInfo.versionId
|
folderVersionId: deletedInfo.versionId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -633,7 +633,7 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
// Add remaining new folders (not updates)
|
// Add remaining new folders (not updates)
|
||||||
addedFoldersChanges.forEach((addedFolder) => {
|
addedFoldersChanges.forEach((addedFolder) => {
|
||||||
commitChanges.push({
|
commitChanges.push({
|
||||||
type: "add",
|
type: CommitType.ADD,
|
||||||
folderVersionId: addedFolder.id
|
folderVersionId: addedFolder.id
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
|||||||
import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||||
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
|
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
|
||||||
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { pitServiceFactory } from "@app/ee/services/pit/pit-service";
|
||||||
import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal";
|
import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal";
|
||||||
import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
||||||
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
@@ -1520,6 +1521,13 @@ export const registerRoutes = async (
|
|||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pitService = pitServiceFactory({
|
||||||
|
folderCommitService,
|
||||||
|
secretService,
|
||||||
|
folderService,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
const identityOidcAuthService = identityOidcAuthServiceFactory({
|
const identityOidcAuthService = identityOidcAuthServiceFactory({
|
||||||
identityOidcAuthDAL,
|
identityOidcAuthDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
@@ -1824,6 +1832,7 @@ export const registerRoutes = async (
|
|||||||
certificateTemplate: certificateTemplateService,
|
certificateTemplate: certificateTemplateService,
|
||||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
certificateEst: certificateEstService,
|
certificateEst: certificateEstService,
|
||||||
|
pit: pitService,
|
||||||
pkiAlert: pkiAlertService,
|
pkiAlert: pkiAlertService,
|
||||||
pkiCollection: pkiCollectionService,
|
pkiCollection: pkiCollectionService,
|
||||||
pkiSubscriber: pkiSubscriberService,
|
pkiSubscriber: pkiSubscriberService,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
@@ -14,32 +15,57 @@ import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
|||||||
|
|
||||||
export type TFolderCommitChangesDALFactory = ReturnType<typeof folderCommitChangesDALFactory>;
|
export type TFolderCommitChangesDALFactory = ReturnType<typeof folderCommitChangesDALFactory>;
|
||||||
|
|
||||||
export type CommitChangeWithCommitInfo = TFolderCommitChanges & {
|
// Base type with common fields
|
||||||
|
type BaseCommitChangeInfo = TFolderCommitChanges & {
|
||||||
actorMetadata: unknown;
|
actorMetadata: unknown;
|
||||||
actorType: string;
|
actorType: string;
|
||||||
message?: string | null;
|
message?: string | null;
|
||||||
folderId: string;
|
folderId: string;
|
||||||
folderName?: string;
|
createdAt: Date;
|
||||||
folderVersion?: string;
|
};
|
||||||
secretKey?: string;
|
|
||||||
secretVersion?: string;
|
// Secret-specific change
|
||||||
secretId?: string;
|
export type SecretCommitChange = BaseCommitChangeInfo & {
|
||||||
folderChangeId?: string;
|
resourceType: "secret";
|
||||||
objectType?: string;
|
secretKey: string;
|
||||||
|
changeType: string;
|
||||||
|
secretVersionId?: string | null;
|
||||||
|
secretVersion: string;
|
||||||
|
secretId: string;
|
||||||
versions?: {
|
versions?: {
|
||||||
secretKey?: string;
|
secretKey: string;
|
||||||
secretComment?: string;
|
secretComment: string;
|
||||||
skipMultilineEncoding?: boolean | null;
|
skipMultilineEncoding?: boolean | null;
|
||||||
secretReminderRepeatDays?: number | null;
|
secretReminderRepeatDays?: number | null;
|
||||||
secretReminderNote?: string | null;
|
secretReminderNote?: string | null;
|
||||||
metadata?: unknown;
|
metadata?: unknown;
|
||||||
tags?: string[] | null;
|
tags?: string[] | null;
|
||||||
secretReminderRecipients?: string[] | null;
|
secretReminderRecipients?: string[] | null;
|
||||||
secretValue?: string;
|
secretValue: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Folder-specific change
|
||||||
|
export type FolderCommitChange = BaseCommitChangeInfo & {
|
||||||
|
resourceType: "folder";
|
||||||
|
folderName: string;
|
||||||
|
folderVersion: string;
|
||||||
|
folderChangeId: string;
|
||||||
|
versions?: {
|
||||||
name?: string;
|
name?: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Discriminated union
|
||||||
|
export type CommitChangeWithCommitInfo = SecretCommitChange | FolderCommitChange;
|
||||||
|
|
||||||
|
// Type guards
|
||||||
|
export const isSecretCommitChange = (change: CommitChangeWithCommitInfo): change is SecretCommitChange =>
|
||||||
|
change.resourceType === "secret";
|
||||||
|
|
||||||
|
export const isFolderCommitChange = (change: CommitChangeWithCommitInfo): change is FolderCommitChange =>
|
||||||
|
change.resourceType === "folder";
|
||||||
|
|
||||||
export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
||||||
const folderCommitChangesOrm = ormify(db, TableName.FolderCommitChanges);
|
const folderCommitChangesOrm = ormify(db, TableName.FolderCommitChanges);
|
||||||
|
|
||||||
@@ -50,7 +76,6 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
|||||||
): Promise<CommitChangeWithCommitInfo[]> => {
|
): Promise<CommitChangeWithCommitInfo[]> => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db.replicaNode())<TFolderCommitChanges>(TableName.FolderCommitChanges)
|
const docs = await (tx || db.replicaNode())<TFolderCommitChanges>(TableName.FolderCommitChanges)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
||||||
.where(buildFindFilter({ folderCommitId }, TableName.FolderCommitChanges))
|
.where(buildFindFilter({ folderCommitId }, TableName.FolderCommitChanges))
|
||||||
.leftJoin<TFolderCommits>(
|
.leftJoin<TFolderCommits>(
|
||||||
TableName.FolderCommit,
|
TableName.FolderCommit,
|
||||||
@@ -91,57 +116,108 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("createdAt").withSchema(TableName.FolderCommit),
|
db.ref("createdAt").withSchema(TableName.FolderCommit),
|
||||||
db.ref("folderId").withSchema(TableName.FolderCommit)
|
db.ref("folderId").withSchema(TableName.FolderCommit)
|
||||||
);
|
);
|
||||||
return docs.map((doc) => ({
|
|
||||||
...doc,
|
return docs.map((doc) => {
|
||||||
folderVersion: doc.folderVersion?.toString(),
|
// Determine if this is a secret or folder change based on populated fields
|
||||||
secretVersion: doc.secretVersion?.toString()
|
if (doc.secretKey && doc.secretVersion && doc.secretId) {
|
||||||
}));
|
return {
|
||||||
|
...doc,
|
||||||
|
resourceType: "secret",
|
||||||
|
secretKey: doc.secretKey,
|
||||||
|
secretVersion: doc.secretVersion.toString(),
|
||||||
|
secretId: doc.secretId
|
||||||
|
} as SecretCommitChange;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
resourceType: "folder",
|
||||||
|
folderName: doc.folderName,
|
||||||
|
folderVersion: doc.folderVersion.toString(),
|
||||||
|
folderChangeId: doc.folderChangeId
|
||||||
|
} as FolderCommitChange;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindByCommitId" });
|
throw new DatabaseError({ error, name: "FindByCommitId" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findBySecretVersionId = async (secretVersionId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
|
const findBySecretVersionId = async (secretVersionId: string, tx?: Knex): Promise<SecretCommitChange[]> => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db.replicaNode())<
|
const docs = await (tx || db.replicaNode())<
|
||||||
TFolderCommitChanges &
|
TFolderCommitChanges &
|
||||||
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
|
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
|
||||||
>(TableName.FolderCommitChanges)
|
>(TableName.FolderCommitChanges)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
||||||
.where(buildFindFilter({ secretVersionId }, TableName.FolderCommitChanges))
|
.where(buildFindFilter({ secretVersionId }, TableName.FolderCommitChanges))
|
||||||
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
||||||
.join(TableName.FolderCommit, `${TableName.FolderCommitChanges}.folderCommitId`, `${TableName.FolderCommit}.id`)
|
.join(TableName.FolderCommit, `${TableName.FolderCommitChanges}.folderCommitId`, `${TableName.FolderCommit}.id`)
|
||||||
|
.leftJoin<TSecretVersionsV2>(
|
||||||
|
TableName.SecretVersionV2,
|
||||||
|
`${TableName.FolderCommitChanges}.secretVersionId`,
|
||||||
|
`${TableName.SecretVersionV2}.id`
|
||||||
|
)
|
||||||
.select(
|
.select(
|
||||||
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
||||||
db.ref("actorType").withSchema(TableName.FolderCommit),
|
db.ref("actorType").withSchema(TableName.FolderCommit),
|
||||||
db.ref("message").withSchema(TableName.FolderCommit),
|
db.ref("message").withSchema(TableName.FolderCommit),
|
||||||
db.ref("createdAt").withSchema(TableName.FolderCommit),
|
db.ref("createdAt").withSchema(TableName.FolderCommit),
|
||||||
db.ref("folderId").withSchema(TableName.FolderCommit)
|
db.ref("folderId").withSchema(TableName.FolderCommit),
|
||||||
|
db.ref("key").withSchema(TableName.SecretVersionV2).as("secretKey"),
|
||||||
|
db.ref("version").withSchema(TableName.SecretVersionV2).as("secretVersion"),
|
||||||
|
db.ref("secretId").withSchema(TableName.SecretVersionV2)
|
||||||
|
);
|
||||||
|
|
||||||
|
return docs
|
||||||
|
.filter((doc) => doc.secretKey && doc.secretVersion && doc.secretId)
|
||||||
|
.map(
|
||||||
|
(doc): SecretCommitChange => ({
|
||||||
|
...doc,
|
||||||
|
resourceType: "secret",
|
||||||
|
secretKey: doc.secretKey,
|
||||||
|
secretVersion: doc.secretVersion.toString(),
|
||||||
|
secretId: doc.secretId
|
||||||
|
})
|
||||||
);
|
);
|
||||||
return docs;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindBySecretVersionId" });
|
throw new DatabaseError({ error, name: "FindBySecretVersionId" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByFolderVersionId = async (folderVersionId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
|
const findByFolderVersionId = async (folderVersionId: string, tx?: Knex): Promise<FolderCommitChange[]> => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db.replicaNode())<
|
const docs = await (tx || db.replicaNode())<
|
||||||
TFolderCommitChanges &
|
TFolderCommitChanges &
|
||||||
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
|
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
|
||||||
>(TableName.FolderCommitChanges)
|
>(TableName.FolderCommitChanges)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
||||||
.where(buildFindFilter({ folderVersionId }, TableName.FolderCommitChanges))
|
.where(buildFindFilter({ folderVersionId }, TableName.FolderCommitChanges))
|
||||||
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
||||||
.join(TableName.FolderCommit, `${TableName.FolderCommitChanges}.folderCommitId`, `${TableName.FolderCommit}.id`)
|
.join(TableName.FolderCommit, `${TableName.FolderCommitChanges}.folderCommitId`, `${TableName.FolderCommit}.id`)
|
||||||
|
.leftJoin<TSecretFolderVersions>(
|
||||||
|
TableName.SecretFolderVersion,
|
||||||
|
`${TableName.FolderCommitChanges}.folderVersionId`,
|
||||||
|
`${TableName.SecretFolderVersion}.id`
|
||||||
|
)
|
||||||
.select(
|
.select(
|
||||||
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
||||||
db.ref("actorType").withSchema(TableName.FolderCommit),
|
db.ref("actorType").withSchema(TableName.FolderCommit),
|
||||||
db.ref("message").withSchema(TableName.FolderCommit),
|
db.ref("message").withSchema(TableName.FolderCommit),
|
||||||
db.ref("createdAt").withSchema(TableName.FolderCommit),
|
db.ref("createdAt").withSchema(TableName.FolderCommit),
|
||||||
db.ref("folderId").withSchema(TableName.FolderCommit)
|
db.ref("folderId").withSchema(TableName.FolderCommit),
|
||||||
|
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderName"),
|
||||||
|
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderChangeId"),
|
||||||
|
db.ref("version").withSchema(TableName.SecretFolderVersion).as("folderVersion")
|
||||||
|
);
|
||||||
|
|
||||||
|
return docs
|
||||||
|
.filter((doc) => doc.folderName && doc.folderVersion && doc.folderChangeId)
|
||||||
|
.map(
|
||||||
|
(doc): FolderCommitChange => ({
|
||||||
|
...doc,
|
||||||
|
resourceType: "folder",
|
||||||
|
folderName: doc.folderName,
|
||||||
|
folderVersion: doc.folderVersion!.toString(),
|
||||||
|
folderChangeId: doc.folderChangeId
|
||||||
|
})
|
||||||
);
|
);
|
||||||
return docs;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindByFolderVersionId" });
|
throw new DatabaseError({ error, name: "FindByFolderVersionId" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -474,6 +474,24 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findCommitBefore = async (
|
||||||
|
folderId: string,
|
||||||
|
commitId: bigint,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TFolderCommits | undefined> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where({ folderId })
|
||||||
|
.where("commitId", "<", commitId.toString())
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
.orderBy("commitId", "desc")
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindCommitBefore" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...restOfOrm,
|
...restOfOrm,
|
||||||
findByFolderId,
|
findByFolderId,
|
||||||
@@ -489,6 +507,7 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
|||||||
findAllFolderCommitsAfter,
|
findAllFolderCommitsAfter,
|
||||||
findPreviousCommitTo,
|
findPreviousCommitTo,
|
||||||
findById,
|
findById,
|
||||||
findByFolderIdPaginated
|
findByFolderIdPaginated,
|
||||||
|
findCommitBefore
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Base schema shared by both secret and folder changes
|
||||||
|
const baseChangeSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
folderCommitId: z.string(),
|
||||||
|
changeType: z.string(),
|
||||||
|
isUpdate: z.boolean().optional(),
|
||||||
|
createdAt: z.union([z.string(), z.date()]),
|
||||||
|
updatedAt: z.union([z.string(), z.date()]),
|
||||||
|
actorMetadata: z
|
||||||
|
.union([
|
||||||
|
z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
name: z.string().optional()
|
||||||
|
}),
|
||||||
|
z.unknown()
|
||||||
|
])
|
||||||
|
.optional(),
|
||||||
|
actorType: z.string(),
|
||||||
|
message: z.string().nullable().optional(),
|
||||||
|
folderId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Secret-specific versions schema
|
||||||
|
const secretVersionSchema = z.object({
|
||||||
|
secretKey: z.string(),
|
||||||
|
secretComment: z.string(),
|
||||||
|
skipMultilineEncoding: z.boolean().nullable().optional(),
|
||||||
|
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||||
|
tags: z.array(z.string()).nullable().optional(),
|
||||||
|
metadata: z.unknown().nullable().optional(),
|
||||||
|
secretReminderNote: z.string().nullable().optional(),
|
||||||
|
secretValue: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Folder-specific versions schema
|
||||||
|
const folderVersionSchema = z.object({
|
||||||
|
name: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Secret commit change schema
|
||||||
|
const secretCommitChangeSchema = baseChangeSchema.extend({
|
||||||
|
resourceType: z.literal("secret"),
|
||||||
|
secretVersionId: z.string().optional().nullable(),
|
||||||
|
secretKey: z.string(),
|
||||||
|
secretVersion: z.union([z.string(), z.number()]),
|
||||||
|
secretId: z.string(),
|
||||||
|
versions: z.array(secretVersionSchema).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Folder commit change schema
|
||||||
|
const folderCommitChangeSchema = baseChangeSchema.extend({
|
||||||
|
resourceType: z.literal("folder"),
|
||||||
|
folderVersionId: z.string().optional().nullable(),
|
||||||
|
folderName: z.string(),
|
||||||
|
folderChangeId: z.string(),
|
||||||
|
folderVersion: z.union([z.string(), z.number()]),
|
||||||
|
versions: z.array(folderVersionSchema).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Discriminated union for commit changes
|
||||||
|
export const commitChangeSchema = z.discriminatedUnion("resourceType", [
|
||||||
|
secretCommitChangeSchema,
|
||||||
|
folderCommitChangeSchema
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Commit schema
|
||||||
|
const commitSchema = 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().nullable().optional(),
|
||||||
|
folderId: z.string(),
|
||||||
|
envId: z.string(),
|
||||||
|
createdAt: z.union([z.string(), z.date()]),
|
||||||
|
updatedAt: z.union([z.string(), z.date()]),
|
||||||
|
changes: z.array(commitChangeSchema).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response schema
|
||||||
|
export const commitChangesResponseSchema = z.object({
|
||||||
|
changes: commitSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
// Base resource change schema for comparison results
|
||||||
|
const baseResourceChangeSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
versionId: z.string(),
|
||||||
|
oldVersionId: z.string().optional(),
|
||||||
|
changeType: z.enum(["add", "delete", "update", "create"]),
|
||||||
|
commitId: z.union([z.string(), z.bigint()]),
|
||||||
|
createdAt: z.union([z.string(), z.date()]).optional(),
|
||||||
|
parentId: z.string().optional(),
|
||||||
|
isUpdate: z.boolean().optional(),
|
||||||
|
fromVersion: z.union([z.string(), z.number()]).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Secret resource change schema
|
||||||
|
const secretResourceChangeSchema = baseResourceChangeSchema.extend({
|
||||||
|
type: z.literal("secret"),
|
||||||
|
secretKey: z.string().optional(),
|
||||||
|
secretVersion: z.union([z.string(), z.number()]).optional(),
|
||||||
|
secretId: z.string().optional(),
|
||||||
|
versions: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
secretKey: z.string().optional(),
|
||||||
|
secretComment: z.string().optional(),
|
||||||
|
skipMultilineEncoding: z.boolean().nullable().optional(),
|
||||||
|
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||||
|
tags: z.array(z.string()).nullable().optional(),
|
||||||
|
metadata: z.unknown().nullable().optional(),
|
||||||
|
secretReminderNote: z.string().nullable().optional(),
|
||||||
|
secretValue: z.string().optional()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Folder resource change schema
|
||||||
|
const folderResourceChangeSchema = baseResourceChangeSchema.extend({
|
||||||
|
type: z.literal("folder"),
|
||||||
|
folderName: z.string().optional(),
|
||||||
|
folderVersion: z.union([z.string(), z.number()]).optional(),
|
||||||
|
versions: z.array(folderVersionSchema).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Discriminated union for resource changes
|
||||||
|
export const resourceChangeSchema = z.discriminatedUnion("type", [
|
||||||
|
secretResourceChangeSchema,
|
||||||
|
folderResourceChangeSchema
|
||||||
|
]);
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/return-await */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
@@ -9,6 +12,7 @@ import {
|
|||||||
ChangeType,
|
ChangeType,
|
||||||
CommitType,
|
CommitType,
|
||||||
folderCommitServiceFactory,
|
folderCommitServiceFactory,
|
||||||
|
ResourceType,
|
||||||
TFolderCommitServiceFactory
|
TFolderCommitServiceFactory
|
||||||
} from "./folder-commit-service";
|
} from "./folder-commit-service";
|
||||||
|
|
||||||
@@ -518,8 +522,20 @@ describe("folderCommitServiceFactory", () => {
|
|||||||
const actorType = ActorType.USER;
|
const actorType = ActorType.USER;
|
||||||
|
|
||||||
const differences = [
|
const differences = [
|
||||||
{ type: "secret", id: "secret-1", versionId: "v1", changeType: ChangeType.CREATE, commitId: BigInt(1) },
|
{
|
||||||
{ type: "folder", id: "folder-1", versionId: "v2", changeType: ChangeType.UPDATE, commitId: BigInt(1) }
|
type: ResourceType.SECRET,
|
||||||
|
id: "secret-1",
|
||||||
|
versionId: "v1",
|
||||||
|
changeType: ChangeType.CREATE,
|
||||||
|
commitId: BigInt(1)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ResourceType.FOLDER,
|
||||||
|
id: "folder-1",
|
||||||
|
versionId: "v2",
|
||||||
|
changeType: ChangeType.UPDATE,
|
||||||
|
commitId: BigInt(1)
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const secretVersions = {
|
const secretVersions = {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export enum CommitType {
|
|||||||
DELETE = "delete"
|
DELETE = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ResourceType {
|
export enum ResourceType {
|
||||||
SECRET = "secret",
|
SECRET = "secret",
|
||||||
FOLDER = "folder"
|
FOLDER = "folder"
|
||||||
}
|
}
|
||||||
@@ -68,8 +68,7 @@ type TCommitChangeDTO = {
|
|||||||
folderVersionId?: string;
|
folderVersionId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ResourceChange = {
|
type BaseChange = {
|
||||||
type: string;
|
|
||||||
id: string;
|
id: string;
|
||||||
versionId: string;
|
versionId: string;
|
||||||
oldVersionId?: string;
|
oldVersionId?: string;
|
||||||
@@ -78,12 +77,14 @@ export type ResourceChange = {
|
|||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
isUpdate?: boolean;
|
isUpdate?: boolean;
|
||||||
|
fromVersion?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SecretChange = {
|
||||||
|
type: ResourceType.SECRET;
|
||||||
secretKey?: string;
|
secretKey?: string;
|
||||||
secretVersion?: string;
|
secretVersion?: string;
|
||||||
secretId?: string;
|
secretId?: string;
|
||||||
folderName?: string;
|
|
||||||
folderVersion?: string;
|
|
||||||
fromVersion?: string;
|
|
||||||
versions?: {
|
versions?: {
|
||||||
secretKey?: string;
|
secretKey?: string;
|
||||||
secretComment?: string;
|
secretComment?: string;
|
||||||
@@ -94,10 +95,20 @@ export type ResourceChange = {
|
|||||||
tags?: string[] | null;
|
tags?: string[] | null;
|
||||||
secretReminderRecipients?: string[] | null;
|
secretReminderRecipients?: string[] | null;
|
||||||
secretValue?: string;
|
secretValue?: string;
|
||||||
name?: string;
|
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FolderChange = {
|
||||||
|
type: ResourceType.FOLDER;
|
||||||
|
folderName?: string;
|
||||||
|
folderVersion?: string;
|
||||||
|
versions?: {
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResourceChange = BaseChange & (SecretChange | FolderChange);
|
||||||
|
|
||||||
type ActorInfo = {
|
type ActorInfo = {
|
||||||
actorType: string;
|
actorType: string;
|
||||||
actorId?: string;
|
actorId?: string;
|
||||||
@@ -150,7 +161,7 @@ export const folderCommitServiceFactory = ({
|
|||||||
}: TFolderCommitServiceFactoryDep) => {
|
}: TFolderCommitServiceFactoryDep) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const checkProjectPermission = async ({
|
const checkProjectCommitReadPermission = async ({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -377,7 +388,7 @@ export const folderCommitServiceFactory = ({
|
|||||||
targetCommitId: string;
|
targetCommitId: string;
|
||||||
defaultOperation?: "create" | "update" | "delete";
|
defaultOperation?: "create" | "update" | "delete";
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
}) => {
|
}): Promise<ResourceChange[]> => {
|
||||||
const targetCommit = await folderCommitDAL.findById(targetCommitId, tx);
|
const targetCommit = await folderCommitDAL.findById(targetCommitId, tx);
|
||||||
if (!targetCommit) {
|
if (!targetCommit) {
|
||||||
throw new NotFoundError({ message: `Commit with ID ${targetCommitId} not found` });
|
throw new NotFoundError({ message: `Commit with ID ${targetCommitId} not found` });
|
||||||
@@ -387,17 +398,34 @@ export const folderCommitServiceFactory = ({
|
|||||||
if (!currentCommitId) {
|
if (!currentCommitId) {
|
||||||
const targetState = await reconstructFolderState(targetCommitId, tx);
|
const targetState = await reconstructFolderState(targetCommitId, tx);
|
||||||
|
|
||||||
return targetState.map((resource) => ({
|
return targetState
|
||||||
type: resource.type,
|
.map((resource): ResourceChange | null => {
|
||||||
id: resource.id,
|
if (resource.type === ResourceType.SECRET) {
|
||||||
versionId: resource.versionId,
|
return {
|
||||||
changeType: defaultOperation,
|
type: ResourceType.SECRET,
|
||||||
commitId: targetCommit.commitId,
|
id: resource.id,
|
||||||
secretKey: resource.secretKey,
|
versionId: resource.versionId,
|
||||||
secretVersion: resource.secretVersion,
|
changeType: defaultOperation as ChangeType,
|
||||||
folderName: resource.folderName,
|
commitId: targetCommit.commitId,
|
||||||
folderVersion: resource.folderVersion
|
secretKey: resource.secretKey,
|
||||||
})) as ResourceChange[];
|
secretVersion: resource.secretVersion,
|
||||||
|
secretId: resource.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (resource.type === ResourceType.FOLDER) {
|
||||||
|
return {
|
||||||
|
type: ResourceType.FOLDER,
|
||||||
|
id: resource.id,
|
||||||
|
versionId: resource.versionId,
|
||||||
|
changeType: defaultOperation as ChangeType,
|
||||||
|
commitId: targetCommit.commitId,
|
||||||
|
folderName: resource.folderName,
|
||||||
|
folderVersion: resource.folderVersion
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((change): change is ResourceChange => !!change);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Original logic for when currentCommitId is provided
|
// Original logic for when currentCommitId is provided
|
||||||
@@ -452,49 +480,88 @@ export const folderCommitServiceFactory = ({
|
|||||||
const targetResource = targetMap[key];
|
const targetResource = targetMap[key];
|
||||||
|
|
||||||
if (!targetResource) {
|
if (!targetResource) {
|
||||||
differences.push({
|
// Resource was deleted
|
||||||
type: currentResource.type,
|
if (currentResource.type === ResourceType.SECRET) {
|
||||||
id: currentResource.id,
|
differences.push({
|
||||||
versionId: currentResource.versionId,
|
type: ResourceType.SECRET,
|
||||||
changeType: ChangeType.DELETE,
|
id: currentResource.id,
|
||||||
commitId: targetCommit.commitId,
|
versionId: currentResource.versionId,
|
||||||
secretKey: currentResource.secretKey,
|
changeType: ChangeType.DELETE,
|
||||||
secretVersion: currentResource.secretVersion,
|
commitId: targetCommit.commitId,
|
||||||
folderName: currentResource.folderName,
|
secretKey: currentResource.secretKey,
|
||||||
folderVersion: currentResource.folderVersion,
|
secretVersion: currentResource.secretVersion,
|
||||||
fromVersion: currentResource.versionId
|
secretId: currentResource.id,
|
||||||
});
|
fromVersion: currentResource.versionId
|
||||||
|
});
|
||||||
|
} else if (currentResource.type === ResourceType.FOLDER) {
|
||||||
|
differences.push({
|
||||||
|
type: ResourceType.FOLDER,
|
||||||
|
id: currentResource.id,
|
||||||
|
versionId: currentResource.versionId,
|
||||||
|
changeType: ChangeType.DELETE,
|
||||||
|
commitId: targetCommit.commitId,
|
||||||
|
folderName: currentResource.folderName,
|
||||||
|
folderVersion: currentResource.folderVersion,
|
||||||
|
fromVersion: currentResource.versionId
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (currentResource.versionId !== targetResource.versionId) {
|
} else if (currentResource.versionId !== targetResource.versionId) {
|
||||||
differences.push({
|
// Resource was updated
|
||||||
type: targetResource.type,
|
if (targetResource.type === ResourceType.SECRET) {
|
||||||
id: targetResource.id,
|
differences.push({
|
||||||
versionId: targetResource.versionId,
|
type: ResourceType.SECRET,
|
||||||
changeType: ChangeType.UPDATE,
|
id: targetResource.id,
|
||||||
commitId: targetCommit.commitId,
|
versionId: targetResource.versionId,
|
||||||
secretKey: targetResource.secretKey,
|
changeType: ChangeType.UPDATE,
|
||||||
secretVersion: targetResource.secretVersion,
|
commitId: targetCommit.commitId,
|
||||||
folderName: targetResource.folderName,
|
secretKey: targetResource.secretKey,
|
||||||
folderVersion: targetResource.folderVersion,
|
secretVersion: targetResource.secretVersion,
|
||||||
fromVersion: currentResource.folderVersion || currentResource.secretVersion
|
secretId: targetResource.id,
|
||||||
});
|
fromVersion: currentResource.secretVersion
|
||||||
|
});
|
||||||
|
} else if (targetResource.type === ResourceType.FOLDER) {
|
||||||
|
differences.push({
|
||||||
|
type: ResourceType.FOLDER,
|
||||||
|
id: targetResource.id,
|
||||||
|
versionId: targetResource.versionId,
|
||||||
|
changeType: ChangeType.UPDATE,
|
||||||
|
commitId: targetCommit.commitId,
|
||||||
|
folderName: targetResource.folderName,
|
||||||
|
folderVersion: targetResource.folderVersion,
|
||||||
|
fromVersion: currentResource.folderVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Find new resources
|
||||||
Object.keys(targetMap).forEach((key) => {
|
Object.keys(targetMap).forEach((key) => {
|
||||||
if (!currentMap[key]) {
|
if (!currentMap[key]) {
|
||||||
const targetResource = targetMap[key];
|
const targetResource = targetMap[key];
|
||||||
differences.push({
|
if (targetResource.type === ResourceType.SECRET) {
|
||||||
type: targetResource.type,
|
differences.push({
|
||||||
id: targetResource.id,
|
type: ResourceType.SECRET,
|
||||||
versionId: targetResource.versionId,
|
id: targetResource.id,
|
||||||
changeType: ChangeType.CREATE,
|
versionId: targetResource.versionId,
|
||||||
commitId: targetCommit.commitId,
|
changeType: ChangeType.CREATE,
|
||||||
createdAt: targetCommit.createdAt,
|
commitId: targetCommit.commitId,
|
||||||
secretKey: targetResource.secretKey,
|
createdAt: targetCommit.createdAt,
|
||||||
secretVersion: targetResource.secretVersion,
|
secretKey: targetResource.secretKey,
|
||||||
folderName: targetResource.folderName,
|
secretVersion: targetResource.secretVersion,
|
||||||
folderVersion: targetResource.folderVersion
|
secretId: targetResource.id
|
||||||
});
|
});
|
||||||
|
} else if (targetResource.type === ResourceType.FOLDER) {
|
||||||
|
differences.push({
|
||||||
|
type: ResourceType.FOLDER,
|
||||||
|
id: targetResource.id,
|
||||||
|
versionId: targetResource.versionId,
|
||||||
|
changeType: ChangeType.CREATE,
|
||||||
|
commitId: targetCommit.commitId,
|
||||||
|
createdAt: targetCommit.createdAt,
|
||||||
|
folderName: targetResource.folderName,
|
||||||
|
folderVersion: targetResource.folderVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -640,13 +707,18 @@ export const folderCommitServiceFactory = ({
|
|||||||
) => {
|
) => {
|
||||||
const commitChanges = [];
|
const commitChanges = [];
|
||||||
|
|
||||||
|
// Filter only secret changes using discriminated union
|
||||||
|
const secretChanges = changes.filter(
|
||||||
|
(change): change is ResourceChange & SecretChange => change.type === ResourceType.SECRET
|
||||||
|
);
|
||||||
|
|
||||||
// Collect all secretIds for batch lookup
|
// Collect all secretIds for batch lookup
|
||||||
const secretIds = changes.map((change) => secretVersions[change.id]?.secretId).filter(Boolean);
|
const secretIds = secretChanges.map((change) => secretVersions[change.id]?.secretId).filter(Boolean);
|
||||||
|
|
||||||
// Fetch all latest versions in one call
|
// Fetch all latest versions in one call
|
||||||
const latestVersionsMap = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, secretIds, tx);
|
const latestVersionsMap = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, secretIds, tx);
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of secretChanges) {
|
||||||
const secretVersion = secretVersions[change.id];
|
const secretVersion = secretVersions[change.id];
|
||||||
// eslint-disable-next-line no-continue
|
// eslint-disable-next-line no-continue
|
||||||
if (!secretVersion) continue;
|
if (!secretVersion) continue;
|
||||||
@@ -800,7 +872,12 @@ export const folderCommitServiceFactory = ({
|
|||||||
) => {
|
) => {
|
||||||
const commitChanges = [];
|
const commitChanges = [];
|
||||||
|
|
||||||
for (const change of changes) {
|
// Filter only folder changes using discriminated union
|
||||||
|
const folderChanges = changes.filter(
|
||||||
|
(change): change is ResourceChange & FolderChange => change.type === ResourceType.FOLDER
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const change of folderChanges) {
|
||||||
const folderVersion = folderVersions[change.id];
|
const folderVersion = folderVersions[change.id];
|
||||||
|
|
||||||
switch (change.changeType) {
|
switch (change.changeType) {
|
||||||
@@ -913,9 +990,14 @@ export const folderCommitServiceFactory = ({
|
|||||||
|
|
||||||
return commitChanges;
|
return commitChanges;
|
||||||
};
|
};
|
||||||
// Group differences by type for more efficient processing
|
|
||||||
const secretChanges = differences.filter((diff) => diff.type === ResourceType.SECRET);
|
// Group differences by type for more efficient processing using discriminated unions
|
||||||
const folderChanges = differences.filter((diff) => diff.type === ResourceType.FOLDER);
|
const secretChanges = differences.filter(
|
||||||
|
(diff): diff is ResourceChange & SecretChange => diff.type === ResourceType.SECRET
|
||||||
|
);
|
||||||
|
const folderChanges = differences.filter(
|
||||||
|
(diff): diff is ResourceChange & FolderChange => diff.type === ResourceType.FOLDER
|
||||||
|
);
|
||||||
|
|
||||||
// Batch fetch necessary data
|
// Batch fetch necessary data
|
||||||
const secretVersions = await secretVersionV2BridgeDAL.findByIdsWithLatestVersion(
|
const secretVersions = await secretVersionV2BridgeDAL.findByIdsWithLatestVersion(
|
||||||
@@ -933,8 +1015,8 @@ export const folderCommitServiceFactory = ({
|
|||||||
|
|
||||||
// Process changes in parallel
|
// Process changes in parallel
|
||||||
const [secretCommitChanges, folderCommitChanges] = await Promise.all([
|
const [secretCommitChanges, folderCommitChanges] = await Promise.all([
|
||||||
processSecretChanges(secretChanges, secretVersions, actorInfo, folderId, tx),
|
processSecretChanges(differences, secretVersions, actorInfo, folderId, tx),
|
||||||
processFolderChanges(folderChanges, folderVersions)
|
processFolderChanges(differences, folderVersions)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Combine all changes
|
// Combine all changes
|
||||||
@@ -988,8 +1070,31 @@ export const folderCommitServiceFactory = ({
|
|||||||
/**
|
/**
|
||||||
* Retrieve a commit by ID
|
* Retrieve a commit by ID
|
||||||
*/
|
*/
|
||||||
const getCommitById = async (id: string, tx?: Knex) => {
|
const getCommitById = async ({
|
||||||
return folderCommitDAL.findById(id, tx);
|
commitId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId,
|
||||||
|
tx
|
||||||
|
}: {
|
||||||
|
commitId: string;
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
actorOrgId: string;
|
||||||
|
projectId: string;
|
||||||
|
tx?: Knex;
|
||||||
|
}) => {
|
||||||
|
await checkProjectCommitReadPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
return folderCommitDAL.findById(commitId, tx);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1024,7 +1129,7 @@ export const folderCommitServiceFactory = ({
|
|||||||
search?: string;
|
search?: string;
|
||||||
sort: "asc" | "desc";
|
sort: "asc" | "desc";
|
||||||
}) => {
|
}) => {
|
||||||
await checkProjectPermission({
|
await checkProjectCommitReadPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
@@ -1063,7 +1168,7 @@ export const folderCommitServiceFactory = ({
|
|||||||
environment: string;
|
environment: string;
|
||||||
path: string;
|
path: string;
|
||||||
}) => {
|
}) => {
|
||||||
await checkProjectPermission({
|
await checkProjectCommitReadPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
@@ -1099,7 +1204,7 @@ export const folderCommitServiceFactory = ({
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
commitId: string;
|
commitId: string;
|
||||||
}) => {
|
}) => {
|
||||||
await checkProjectPermission({
|
await checkProjectCommitReadPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
@@ -1382,7 +1487,8 @@ export const folderCommitServiceFactory = ({
|
|||||||
envId: string,
|
envId: string,
|
||||||
actorId: string,
|
actorId: string,
|
||||||
actorType: ActorType,
|
actorType: ActorType,
|
||||||
projectId: string
|
projectId: string,
|
||||||
|
message?: string
|
||||||
) => {
|
) => {
|
||||||
await folderCommitDAL.transaction(async (tx) => {
|
await folderCommitDAL.transaction(async (tx) => {
|
||||||
const targetCommit = await folderCommitDAL.findById(targetCommitId, tx);
|
const targetCommit = await folderCommitDAL.findById(targetCommitId, tx);
|
||||||
@@ -1480,7 +1586,7 @@ export const folderCommitServiceFactory = ({
|
|||||||
actorInfo: {
|
actorInfo: {
|
||||||
actorType,
|
actorType,
|
||||||
actorId,
|
actorId,
|
||||||
message: "Deep rollback"
|
message: message || "Deep rollback"
|
||||||
},
|
},
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -1508,7 +1614,7 @@ export const folderCommitServiceFactory = ({
|
|||||||
actorOrgId: string;
|
actorOrgId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
}) => {
|
}) => {
|
||||||
await checkProjectPermission({
|
await checkProjectCommitReadPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
@@ -1555,7 +1661,7 @@ export const folderCommitServiceFactory = ({
|
|||||||
ProjectPermissionSub.Commits
|
ProjectPermissionSub.Commits
|
||||||
);
|
);
|
||||||
// Check permissions first
|
// Check permissions first
|
||||||
await checkProjectPermission({
|
await checkProjectCommitReadPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -1569,32 +1675,12 @@ export const folderCommitServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `Commit with ID ${commitId} not found` });
|
throw new NotFoundError({ message: `Commit with ID ${commitId} not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all commits for this folder
|
const previousCommit = await folderCommitDAL.findCommitBefore(commitToRevert.folderId, commitToRevert.commitId);
|
||||||
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)
|
if (!previousCommit) {
|
||||||
const sortedCommits = allCommits.sort((a, b) => {
|
|
||||||
if (a.commitId < b.commitId) return -1;
|
|
||||||
if (a.commitId > b.commitId) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
// 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" });
|
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
|
// Calculate the changes needed to go from current commit back to the previous one
|
||||||
const inverseChanges = await compareFolderStates({
|
const inverseChanges = await compareFolderStates({
|
||||||
currentCommitId: commitToRevert.id,
|
currentCommitId: commitToRevert.id,
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const baseChangeSchema = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
folderCommitId: z.string(),
|
|
||||||
changeType: z.string(),
|
|
||||||
isUpdate: z.boolean().optional(),
|
|
||||||
createdAt: z.union([z.string(), z.date()]),
|
|
||||||
updatedAt: z.union([z.string(), z.date()]),
|
|
||||||
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()
|
|
||||||
});
|
|
||||||
|
|
||||||
const commitChangeSchema = baseChangeSchema.extend({
|
|
||||||
secretVersionId: z.string().optional().nullable(),
|
|
||||||
folderVersionId: z.string().optional().nullable(),
|
|
||||||
folderName: z.string().optional().nullable(),
|
|
||||||
folderChangeId: z.string().optional().nullable(),
|
|
||||||
secretKey: z.string().optional().nullable(),
|
|
||||||
secretVersion: z.union([z.string(), z.number()]).optional().nullable(),
|
|
||||||
secretId: z.string().optional().nullable(),
|
|
||||||
folderVersion: z.union([z.string(), z.number()]).optional().nullable(),
|
|
||||||
versions: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
secretKey: z.string().optional().nullable(),
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
const commitSchema = 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().nullable().optional(),
|
|
||||||
folderId: z.string(),
|
|
||||||
envId: z.string(),
|
|
||||||
createdAt: z.union([z.string(), z.date()]),
|
|
||||||
updatedAt: z.union([z.string(), z.date()]),
|
|
||||||
changes: z.array(commitChangeSchema).optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const commitChangesResponseSchema = z.object({
|
|
||||||
changes: commitSchema
|
|
||||||
});
|
|
||||||
@@ -10,7 +10,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||||
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
|
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
|
||||||
|
|
||||||
import { CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
import { ChangeType, CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
||||||
@@ -783,6 +783,27 @@ export const secretFolderServiceFactory = ({
|
|||||||
return versions;
|
return versions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFolderVersions = async (
|
||||||
|
change: {
|
||||||
|
folderVersion?: string;
|
||||||
|
isUpdate?: boolean;
|
||||||
|
changeType?: string;
|
||||||
|
},
|
||||||
|
fromVersion: string,
|
||||||
|
folderId: string
|
||||||
|
) => {
|
||||||
|
const currentVersion = change.folderVersion || "1";
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const versions = await getFolderVersionsByIds({
|
||||||
|
folderId,
|
||||||
|
folderVersions:
|
||||||
|
change.isUpdate || change.changeType === ChangeType.UPDATE ? [currentVersion, fromVersion] : [currentVersion]
|
||||||
|
});
|
||||||
|
return versions.map((v) => ({
|
||||||
|
name: v.name
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createFolder,
|
createFolder,
|
||||||
updateFolder,
|
updateFolder,
|
||||||
@@ -794,6 +815,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
getFoldersMultiEnv,
|
getFoldersMultiEnv,
|
||||||
getFoldersDeepByEnvs,
|
getFoldersDeepByEnvs,
|
||||||
getProjectEnvironmentsFolders,
|
getProjectEnvironmentsFolders,
|
||||||
getFolderVersionsByIds
|
getFolderVersionsByIds,
|
||||||
|
getFolderVersions
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export const fnSecretBulkInsert = async ({
|
|||||||
userActorId,
|
userActorId,
|
||||||
identityActorId,
|
identityActorId,
|
||||||
actorType,
|
actorType,
|
||||||
|
metadata: el.metadata ? JSON.stringify(el.metadata) : null,
|
||||||
secretId: newSecretGroupedByKeyName[el.key][0].id
|
secretId: newSecretGroupedByKeyName[el.key][0].id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
@@ -273,7 +274,7 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
userId,
|
userId,
|
||||||
encryptedComment,
|
encryptedComment,
|
||||||
version,
|
version,
|
||||||
metadata,
|
metadata: metadata ? JSON.stringify(metadata) : null,
|
||||||
reminderNote,
|
reminderNote,
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
reminderRepeatDays,
|
reminderRepeatDays,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "@app/ee/services/permission/permission-fns";
|
} from "@app/ee/services/permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
ProjectPermissionCommitsActions,
|
ProjectPermissionCommitsActions,
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSet,
|
ProjectPermissionSet,
|
||||||
@@ -528,6 +529,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
skipMultilineEncoding: inputSecret.skipMultilineEncoding,
|
skipMultilineEncoding: inputSecret.skipMultilineEncoding,
|
||||||
key: inputSecret.newSecretName || secretName,
|
key: inputSecret.newSecretName || secretName,
|
||||||
tags: inputSecret.tagIds,
|
tags: inputSecret.tagIds,
|
||||||
|
metadata: JSON.stringify(secretMetadata),
|
||||||
secretMetadata,
|
secretMetadata,
|
||||||
...encryptedValue
|
...encryptedValue
|
||||||
}
|
}
|
||||||
@@ -2173,7 +2175,13 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
|
||||||
|
const canRead =
|
||||||
|
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback) ||
|
||||||
|
permission.can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
||||||
|
|
||||||
|
if (!canRead) throw new ForbiddenRequestError({ message: "You do not have permission to read secret versions" });
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId: folder.projectId
|
projectId: folder.projectId
|
||||||
@@ -2889,7 +2897,13 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
|
||||||
|
const canRead =
|
||||||
|
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback) ||
|
||||||
|
permission.can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
||||||
|
|
||||||
|
if (!canRead) throw new ForbiddenRequestError({ message: "You do not have permission to read secret versions" });
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId: folder.projectId
|
projectId: folder.projectId
|
||||||
|
|||||||
@@ -199,13 +199,13 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
|
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
|
||||||
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
|
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.SecretV2JnTag,
|
TableName.SecretVersionV2Tag,
|
||||||
`${TableName.SecretV2}.id`,
|
`${TableName.SecretVersionV2}.id`,
|
||||||
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`
|
||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.SecretTag,
|
TableName.SecretTag,
|
||||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
|
||||||
`${TableName.SecretTag}.id`
|
`${TableName.SecretTag}.id`
|
||||||
)
|
)
|
||||||
.where((qb) => {
|
.where((qb) => {
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ import {
|
|||||||
TGetSecretsRawByFolderMappingsDTO
|
TGetSecretsRawByFolderMappingsDTO
|
||||||
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
import { ChangeType } from "../folder-commit/folder-commit-service";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
@@ -3303,6 +3304,51 @@ export const secretServiceFactory = ({
|
|||||||
return secrets;
|
return secrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getChangeVersions = async (
|
||||||
|
change: {
|
||||||
|
secretVersion: string;
|
||||||
|
secretId?: string;
|
||||||
|
id?: string;
|
||||||
|
isUpdate?: boolean;
|
||||||
|
changeType?: string;
|
||||||
|
},
|
||||||
|
previousVersion: string,
|
||||||
|
actorId: string,
|
||||||
|
actor: ActorType,
|
||||||
|
actorOrgId: string,
|
||||||
|
actorAuthMethod: ActorAuthMethod,
|
||||||
|
folderId: string
|
||||||
|
) => {
|
||||||
|
const currentVersion = change.secretVersion;
|
||||||
|
const secretId = change.secretId ? change.secretId : change.id;
|
||||||
|
if (!secretId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const versions = await getSecretVersionsV2ByIds({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
secretId,
|
||||||
|
// if it's update add also the previous secretversionid
|
||||||
|
secretVersions:
|
||||||
|
change.isUpdate || change.changeType === ChangeType.UPDATE
|
||||||
|
? [currentVersion, previousVersion]
|
||||||
|
: [currentVersion],
|
||||||
|
folderId
|
||||||
|
});
|
||||||
|
return versions?.map((v) => ({
|
||||||
|
secretKey: v.secretKey,
|
||||||
|
secretComment: v.secretComment,
|
||||||
|
skipMultilineEncoding: v.skipMultilineEncoding,
|
||||||
|
secretReminderRepeatDays: v.secretReminderRepeatDays,
|
||||||
|
tags: v.tags?.map((tag) => tag.slug),
|
||||||
|
metadata: v.metadata,
|
||||||
|
secretReminderNote: v.secretReminderNote,
|
||||||
|
secretValue: v.secretValue
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attachTags,
|
attachTags,
|
||||||
detachTags,
|
detachTags,
|
||||||
@@ -3334,6 +3380,7 @@ export const secretServiceFactory = ({
|
|||||||
getSecretAccessList,
|
getSecretAccessList,
|
||||||
getSecretByIdRaw,
|
getSecretByIdRaw,
|
||||||
getAccessibleSecrets,
|
getAccessibleSecrets,
|
||||||
getSecretVersionsV2ByIds
|
getSecretVersionsV2ByIds,
|
||||||
|
getChangeVersions
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const fetchFolderCommitsCount = async ({
|
|||||||
params: {
|
params: {
|
||||||
environment,
|
environment,
|
||||||
path: directory,
|
path: directory,
|
||||||
workspaceId
|
projectId: workspaceId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -86,7 +86,7 @@ const fetchFolderCommitHistory = async (
|
|||||||
params: {
|
params: {
|
||||||
environment,
|
environment,
|
||||||
path: directory,
|
path: directory,
|
||||||
workspaceId,
|
projectId: workspaceId,
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
search,
|
search,
|
||||||
@@ -101,7 +101,7 @@ export const fetchCommitDetails = async (workspaceId: string, commitId: string)
|
|||||||
`/api/v1/pit/commits/${commitId}/changes`,
|
`/api/v1/pit/commits/${commitId}/changes`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
workspaceId
|
projectId: workspaceId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -124,7 +124,7 @@ export const fetchRollbackPreview = async (
|
|||||||
envId,
|
envId,
|
||||||
deepRollback,
|
deepRollback,
|
||||||
secretPath,
|
secretPath,
|
||||||
workspaceId
|
projectId: workspaceId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -146,7 +146,7 @@ const fetchRollback = async (
|
|||||||
deepRollback,
|
deepRollback,
|
||||||
message,
|
message,
|
||||||
envId,
|
envId,
|
||||||
workspaceId
|
projectId: workspaceId
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -156,7 +156,7 @@ const fetchRevert = async (commitId: string, workspaceId: string) => {
|
|||||||
const { data } = await apiRequest.post<{ success: boolean; message: string }>(
|
const { data } = await apiRequest.post<{ success: boolean; message: string }>(
|
||||||
`/api/v1/pit/commits/${commitId}/revert`,
|
`/api/v1/pit/commits/${commitId}/revert`,
|
||||||
{
|
{
|
||||||
workspaceId
|
projectId: workspaceId
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
+2
-2
@@ -361,8 +361,8 @@ export const RollbackPreviewTab = (): JSX.Element => {
|
|||||||
subTitle={`
|
subTitle={`
|
||||||
${
|
${
|
||||||
deepRollback
|
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."
|
||||||
: "This will revert this folder and all child folders to how they appeared at the point in time of this commit."
|
: "This will revert all changes to how they appeared at the point in time of this commit."
|
||||||
}
|
}
|
||||||
Any changes made after this commit will be permanently removed.
|
Any changes made after this commit will be permanently removed.
|
||||||
`}
|
`}
|
||||||
|
|||||||
+471
-401
@@ -1,22 +1,18 @@
|
|||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable no-nested-ternary */
|
||||||
import { useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
export interface Version {
|
export interface Version {
|
||||||
id?: string;
|
id?: string;
|
||||||
version: number;
|
version: number;
|
||||||
// Secret-specific fields
|
|
||||||
secretKey?: string;
|
|
||||||
secretValue?: string;
|
|
||||||
secretComment?: string;
|
|
||||||
skipMultilineEncoding?: boolean;
|
|
||||||
// Folder-specific fields
|
|
||||||
name?: string;
|
|
||||||
// Allow other properties
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
||||||
|
type JsonObject = { [key: string]: JsonValue };
|
||||||
|
type JsonArray = JsonValue[];
|
||||||
|
|
||||||
export interface DiffViewItem {
|
export interface DiffViewItem {
|
||||||
type: "secret" | "folder";
|
type: "secret" | "folder";
|
||||||
isAdded?: boolean;
|
isAdded?: boolean;
|
||||||
@@ -35,244 +31,423 @@ interface SecretVersionDiffViewProps {
|
|||||||
onToggleCollapse?: (id: string) => void;
|
onToggleCollapse?: (id: string) => void;
|
||||||
showHeader?: boolean;
|
showHeader?: boolean;
|
||||||
customHeader?: JSX.Element;
|
customHeader?: JSX.Element;
|
||||||
|
excludedFieldsHighlight?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const highlightChangedFields = (
|
const isObject = (obj: JsonValue): obj is JsonObject => {
|
||||||
json: any,
|
return obj !== null && typeof obj === "object" && !Array.isArray(obj);
|
||||||
changedFields: Set<string>,
|
};
|
||||||
isOldVersion: boolean
|
|
||||||
|
const isArray = (obj: JsonValue): obj is JsonArray => {
|
||||||
|
return Array.isArray(obj);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deepEqual = (a: JsonValue, b: JsonValue): boolean => {
|
||||||
|
if (a === b) return true;
|
||||||
|
if (a == null || b == null) return false;
|
||||||
|
if (typeof a !== typeof b) return false;
|
||||||
|
|
||||||
|
if (isArray(a) && isArray(b)) {
|
||||||
|
if (a.length !== b.length) return false;
|
||||||
|
return a.every((item: JsonValue, index: number) => deepEqual(item, b[index]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject(a) && isObject(b)) {
|
||||||
|
const keysA = Object.keys(a);
|
||||||
|
const keysB = Object.keys(b);
|
||||||
|
if (keysA.length !== keysB.length) return false;
|
||||||
|
return keysA.every((key) => keysB.includes(key) && deepEqual(a[key], b[key]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDiffPaths = (oldObj: JsonValue, newObj: JsonValue, path: string = ""): Set<string> => {
|
||||||
|
const diffPaths = new Set<string>();
|
||||||
|
|
||||||
|
if (oldObj === newObj) return diffPaths;
|
||||||
|
|
||||||
|
if (oldObj == null || newObj == null) {
|
||||||
|
diffPaths.add(path || "root");
|
||||||
|
return diffPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof oldObj !== typeof newObj) {
|
||||||
|
diffPaths.add(path || "root");
|
||||||
|
return diffPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray(oldObj) && isArray(newObj)) {
|
||||||
|
return diffPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject(oldObj) && isObject(newObj)) {
|
||||||
|
const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
|
||||||
|
|
||||||
|
allKeys.forEach((key) => {
|
||||||
|
const currentPath = path ? `${path}.${key}` : key;
|
||||||
|
|
||||||
|
if (path.includes("[")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(key in oldObj) || !(key in newObj) || !deepEqual(oldObj[key], newObj[key])) {
|
||||||
|
diffPaths.add(currentPath);
|
||||||
|
|
||||||
|
if (
|
||||||
|
key in oldObj &&
|
||||||
|
key in newObj &&
|
||||||
|
(isObject(oldObj[key]) || isArray(oldObj[key])) &&
|
||||||
|
(isObject(newObj[key]) || isArray(newObj[key]))
|
||||||
|
) {
|
||||||
|
const nestedDiffs = getDiffPaths(oldObj[key], newObj[key], currentPath);
|
||||||
|
nestedDiffs.forEach((p) => diffPaths.add(p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return diffPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldObj !== newObj) {
|
||||||
|
diffPaths.add(path || "root");
|
||||||
|
}
|
||||||
|
|
||||||
|
return diffPaths;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNestedValue = (obj: JsonValue, path: string): JsonValue => {
|
||||||
|
if (!path) return obj;
|
||||||
|
|
||||||
|
const parts = path.split(/[.[\]]+/).filter(Boolean);
|
||||||
|
let current: JsonValue = obj;
|
||||||
|
|
||||||
|
parts.forEach((part) => {
|
||||||
|
if (current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isObject(current)) {
|
||||||
|
current = current[part];
|
||||||
|
} else if (isArray(current)) {
|
||||||
|
const index = parseInt(part, 10);
|
||||||
|
if (!Number.isNaN(index)) {
|
||||||
|
current = current[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return current;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPathDifferent = (jsonPath: string, diffPaths: Set<string>): boolean => {
|
||||||
|
if (diffPaths.has(jsonPath)) return true;
|
||||||
|
|
||||||
|
const diffPathsArray = Array.from(diffPaths);
|
||||||
|
return diffPathsArray.some((diffPath) => {
|
||||||
|
return (
|
||||||
|
jsonPath.startsWith(`${diffPath}.`) ||
|
||||||
|
jsonPath.startsWith(`${diffPath}[`) ||
|
||||||
|
diffPath.startsWith(`${jsonPath}.`) ||
|
||||||
|
diffPath.startsWith(`${jsonPath}[`)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isContainerActuallyChanged = (
|
||||||
|
path: string,
|
||||||
|
diffPaths: Set<string>,
|
||||||
|
oldObj: JsonValue,
|
||||||
|
newObj: JsonValue
|
||||||
|
): boolean => {
|
||||||
|
if (diffPaths.has(path)) {
|
||||||
|
if (oldObj == null || newObj == null) return true;
|
||||||
|
if (typeof oldObj !== typeof newObj) return true;
|
||||||
|
if (isArray(oldObj) !== isArray(newObj)) return true;
|
||||||
|
if (isObject(oldObj) !== isObject(newObj)) return true;
|
||||||
|
|
||||||
|
if (!isObject(oldObj) && !isArray(oldObj)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray(oldObj) && isArray(newObj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject(oldObj) && isObject(newObj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldObj !== newObj;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderJsonWithDiffs = (
|
||||||
|
obj: JsonValue,
|
||||||
|
diffPaths: Set<string>,
|
||||||
|
isOldVersion: boolean,
|
||||||
|
path: string = "",
|
||||||
|
indentLevel: number = 0,
|
||||||
|
keyName?: string,
|
||||||
|
isLastItem: boolean = false,
|
||||||
|
excludedFieldsHighlight: string[] = [],
|
||||||
|
oldVersionObj?: JsonValue,
|
||||||
|
newVersionObj?: JsonValue
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
const lines = JSON.stringify(json, null, 2).split("\n");
|
const indent = " ".repeat(indentLevel);
|
||||||
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"
|
let isDifferent = false;
|
||||||
const stringMatch = line.match(/: "([^"]*)"(,?)$/);
|
|
||||||
if (stringMatch) {
|
|
||||||
const beforeValue = line.substring(0, line.indexOf(': "'));
|
|
||||||
const value = `"${stringMatch[1]}"`;
|
|
||||||
const afterValue = stringMatch[2] || "";
|
|
||||||
|
|
||||||
return (
|
if (path.includes("[") && oldVersionObj && newVersionObj) {
|
||||||
<div
|
const arrayMatch = path.match(/^([^[]+)\[(\d+)\]/);
|
||||||
key={`${idx + 1}`}
|
if (arrayMatch) {
|
||||||
className={
|
const arrayPath = arrayMatch[1];
|
||||||
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
|
const itemIndex = parseInt(arrayMatch[2], 10);
|
||||||
}
|
|
||||||
>
|
|
||||||
<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 oldArray = getNestedValue(oldVersionObj, arrayPath);
|
||||||
const numberMatch = line.match(/: (-?\d+(?:\.\d+)?)(,?)$/);
|
const newArray = getNestedValue(newVersionObj, arrayPath);
|
||||||
if (numberMatch) {
|
|
||||||
const beforeValue = line.substring(0, line.indexOf(": ") + 2);
|
|
||||||
const value = numberMatch[1];
|
|
||||||
const afterValue = numberMatch[2] || "";
|
|
||||||
|
|
||||||
return (
|
if (isArray(oldArray) && isArray(newArray)) {
|
||||||
<div
|
const currentItem = isOldVersion ? oldArray[itemIndex] : newArray[itemIndex];
|
||||||
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
|
if (isOldVersion) {
|
||||||
const nullMatch = line.match(/: (null)(,?)$/);
|
isDifferent = !newArray.some((newItem: JsonValue) => deepEqual(currentItem, newItem));
|
||||||
if (nullMatch) {
|
} else {
|
||||||
const beforeValue = line.substring(0, line.indexOf(": ") + 2);
|
isDifferent = !oldArray.some((oldItem: JsonValue) => deepEqual(currentItem, oldItem));
|
||||||
const value = nullMatch[1];
|
}
|
||||||
const afterValue = nullMatch[2] || "";
|
} else {
|
||||||
|
isDifferent = isPathDifferent(path, diffPaths);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isDifferent = isPathDifferent(path, diffPaths);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isDifferent = isPathDifferent(path, diffPaths);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
const getLineClass = (different: boolean) => {
|
||||||
<div
|
if (!different) return "flex";
|
||||||
key={`${idx + 1}`}
|
return isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300";
|
||||||
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 getHighlightClass = (different: boolean) => {
|
||||||
const booleanMatch = line.match(/: (true|false)(,?)$/);
|
if (!different) return "";
|
||||||
if (booleanMatch) {
|
return isOldVersion ? "bg-red-900 rounded px-1" : "bg-green-900 rounded px-1";
|
||||||
const beforeValue = line.substring(0, line.indexOf(": ") + 2);
|
};
|
||||||
const value = booleanMatch[1];
|
|
||||||
const afterValue = booleanMatch[2] || "";
|
|
||||||
|
|
||||||
return (
|
const prefix = isDifferent ? (isOldVersion ? "-" : "+") : " ";
|
||||||
<div
|
const keyDisplay = keyName ? `"${keyName}": ` : "";
|
||||||
key={`${idx + 1}`}
|
const comma = !isLastItem ? "," : "";
|
||||||
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 reactKey = `${path || "root"}-${keyName || "value"}-${indentLevel}-${typeof obj}`;
|
||||||
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": []
|
if (
|
||||||
const emptyArrayMatch = line.match(/: \[\](,?)$/);
|
obj === null ||
|
||||||
if (emptyArrayMatch) {
|
typeof obj === "string" ||
|
||||||
const beforeValue = line.substring(0, line.indexOf(": ") + 2);
|
typeof obj === "number" ||
|
||||||
const value = "[]";
|
typeof obj === "boolean"
|
||||||
const afterValue = emptyArrayMatch[1] || "";
|
) {
|
||||||
|
let valueDisplay = "";
|
||||||
|
if (obj === null) valueDisplay = "null";
|
||||||
|
else if (typeof obj === "string") valueDisplay = `"${obj}"`;
|
||||||
|
else valueDisplay = String(obj);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={reactKey} className={getLineClass(isDifferent)}>
|
||||||
key={`${idx + 1}`}
|
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||||
className={
|
<div>
|
||||||
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
|
{indent}
|
||||||
}
|
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
|
||||||
>
|
<span className={getHighlightClass(isDifferent)}>{valueDisplay}</span>
|
||||||
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
|
{comma}
|
||||||
<div>
|
</div>
|
||||||
{beforeValue.substring(0, beforeValue.length - 2)}:{" "}
|
</div>
|
||||||
<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
|
if (isArray(obj) && obj.length === 0) {
|
||||||
// This handles array items, closing brackets, and other complex structure contents
|
return (
|
||||||
const belongsToChangedField = () => {
|
<div key={reactKey} className={getLineClass(isDifferent)}>
|
||||||
// If we're inside an array or object of a changed field, highlight it
|
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||||
let openBrackets = 0;
|
<div>
|
||||||
let openBraces = 0;
|
{indent}
|
||||||
let currentFieldName = null;
|
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
|
||||||
|
<span className={getHighlightClass(isDifferent)}>[]</span>
|
||||||
|
{comma}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = idx - 1; i >= 0; i -= 1) {
|
if (isObject(obj) && Object.keys(obj).length === 0) {
|
||||||
const prevLine = lines[i];
|
return (
|
||||||
|
<div key={reactKey} className={getLineClass(isDifferent)}>
|
||||||
|
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
|
||||||
|
<span className={getHighlightClass(isDifferent)}>{"{}"}</span>
|
||||||
|
{comma}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Count brackets and braces to track nesting
|
let isContainerAddedOrRemoved = false;
|
||||||
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
|
if (oldVersionObj && newVersionObj) {
|
||||||
const fieldNameMatch = prevLine.match(/"([^"]+)":/);
|
const oldValue = getNestedValue(oldVersionObj, path);
|
||||||
if (fieldNameMatch && (openBrackets > 0 || openBraces > 0)) {
|
const newValue = getNestedValue(newVersionObj, path);
|
||||||
[, currentFieldName] = fieldNameMatch;
|
|
||||||
return changedFields.has(currentFieldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've reached the root level, stop looking
|
if (oldValue == null || newValue == null) {
|
||||||
if (openBrackets <= 0 && openBraces <= 0) {
|
isContainerAddedOrRemoved = true;
|
||||||
return false;
|
} else if (typeof oldValue !== typeof newValue) {
|
||||||
}
|
isContainerAddedOrRemoved = true;
|
||||||
}
|
} else if (isArray(oldValue) !== isArray(newValue)) {
|
||||||
|
isContainerAddedOrRemoved = true;
|
||||||
|
} else if (isObject(oldValue) !== isObject(newValue)) {
|
||||||
|
isContainerAddedOrRemoved = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isContainerAddedOrRemoved = isContainerActuallyChanged(
|
||||||
|
path,
|
||||||
|
diffPaths,
|
||||||
|
isOldVersion ? obj : oldVersionObj || null,
|
||||||
|
isOldVersion ? newVersionObj || null : obj
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
if (isArray(obj)) {
|
||||||
};
|
return (
|
||||||
|
<div key={reactKey}>
|
||||||
|
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||||
|
<div className="w-4 flex-shrink-0">
|
||||||
|
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
{keyName && (
|
||||||
|
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>
|
||||||
|
{keyDisplay}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>[</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
// Regular line with changed field or part of a changed complex structure
|
{obj.map((item: JsonValue, index: number) => {
|
||||||
if (belongsToChangedField()) {
|
const itemPath = path ? `${path}[${index}]` : `[${index}]`;
|
||||||
return (
|
const isLast = index === obj.length - 1;
|
||||||
<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 (
|
return (
|
||||||
<div
|
<div key={`${reactKey}-item-${index + 1}`}>
|
||||||
key={`${idx + 1}`}
|
{renderJsonWithDiffs(
|
||||||
className={
|
item,
|
||||||
isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300"
|
diffPaths,
|
||||||
}
|
isOldVersion,
|
||||||
>
|
itemPath,
|
||||||
<div className="w-4 flex-shrink-0">{isOldVersion ? "-" : "+"}</div>
|
indentLevel + 1,
|
||||||
<div>{line}</div>
|
undefined,
|
||||||
|
isLast,
|
||||||
|
excludedFieldsHighlight,
|
||||||
|
oldVersionObj,
|
||||||
|
newVersionObj
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
})}
|
||||||
|
|
||||||
// Unchanged lines
|
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||||
|
<div className="w-4 flex-shrink-0">
|
||||||
|
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>]</span>
|
||||||
|
{comma}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject(obj)) {
|
||||||
|
const keys = Object.keys(obj);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={reactKey}>
|
||||||
|
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||||
|
<div className="w-4 flex-shrink-0">
|
||||||
|
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
{keyName && (
|
||||||
|
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>
|
||||||
|
{keyDisplay}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>{"{"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{keys.map((key, index) => {
|
||||||
|
const keyPath = path ? `${path}.${key}` : key;
|
||||||
|
const isLast = index === keys.length - 1;
|
||||||
|
const propKey = `${reactKey}-prop-${key}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={propKey}>
|
||||||
|
{renderJsonWithDiffs(
|
||||||
|
obj[key],
|
||||||
|
diffPaths,
|
||||||
|
isOldVersion,
|
||||||
|
keyPath,
|
||||||
|
indentLevel + 1,
|
||||||
|
key,
|
||||||
|
isLast,
|
||||||
|
excludedFieldsHighlight,
|
||||||
|
oldVersionObj,
|
||||||
|
newVersionObj
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||||
|
<div className="w-4 flex-shrink-0">
|
||||||
|
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>{"}"}</span>
|
||||||
|
{comma}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={reactKey} className={getLineClass(isDifferent)}>
|
||||||
|
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
{keyDisplay}
|
||||||
|
{String(obj)}
|
||||||
|
{comma}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatAddedJson = (json: JsonValue): JSX.Element => {
|
||||||
|
const lines = JSON.stringify(json, null, 2).split("\n");
|
||||||
|
return (
|
||||||
|
<div className="font-mono text-sm text-green-300">
|
||||||
|
{lines.map((line, lineIndex) => {
|
||||||
|
const lineKey = `added-${line.slice(0, 30)}-${lineIndex}`;
|
||||||
return (
|
return (
|
||||||
<div key={`${idx + 1}`} className="flex">
|
<div key={lineKey} className="flex">
|
||||||
<div className="w-4 flex-shrink-0" />
|
<div className="w-4 flex-shrink-0">+</div>
|
||||||
<div>{line}</div>
|
<div>{line}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -281,224 +456,121 @@ const highlightChangedFields = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper: Format added JSON with + in a separate column
|
const formatDeletedJson = (json: JsonValue): JSX.Element => {
|
||||||
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");
|
const lines = JSON.stringify(json, null, 2).split("\n");
|
||||||
return (
|
return (
|
||||||
<div className="font-mono text-sm text-red-300">
|
<div className="font-mono text-sm text-red-300">
|
||||||
{lines.map((line, idx) => (
|
{lines.map((line, lineIndex) => {
|
||||||
<div key={`${idx + 1}`} className="flex">
|
const lineKey = `deleted-${line.slice(0, 30)}-${lineIndex}`;
|
||||||
<div className="w-4 flex-shrink-0">-</div>
|
return (
|
||||||
<div>{line}</div>
|
<div key={lineKey} className="flex">
|
||||||
</div>
|
<div className="w-4 flex-shrink-0">-</div>
|
||||||
))}
|
<div>{line}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper: Get differences between secret versions
|
const cleanVersionForComparison = (version: Version): JsonValue => {
|
||||||
export const getVersionDifferences = (versions: Version[]) => {
|
const { id, version: versionNumber, ...cleanVersion } = version;
|
||||||
if (!versions || versions.length === 0) return [];
|
return cleanVersion;
|
||||||
|
|
||||||
// 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 SecretVersionDiffView = ({
|
||||||
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,
|
item,
|
||||||
isCollapsed = false,
|
isCollapsed = false,
|
||||||
onToggleCollapse,
|
onToggleCollapse,
|
||||||
showHeader = true,
|
showHeader = true,
|
||||||
customHeader
|
customHeader,
|
||||||
}) => {
|
excludedFieldsHighlight = ["metadata", "tags"]
|
||||||
|
}: SecretVersionDiffViewProps) => {
|
||||||
const oldContainerRef = useRef<HTMLDivElement>(null);
|
const oldContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const newContainerRef = useRef<HTMLDivElement>(null);
|
const newContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isScrollingSynced, setIsScrollingSynced] = useState(false);
|
|
||||||
const [internalCollapsed, setInternalCollapsed] = useState(isCollapsed);
|
const [internalCollapsed, setInternalCollapsed] = useState(isCollapsed);
|
||||||
|
|
||||||
const collapsed = onToggleCollapse ? isCollapsed : internalCollapsed;
|
const handleToggle = useCallback(() => {
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
if (onToggleCollapse && item.id) {
|
if (onToggleCollapse && item.id) {
|
||||||
onToggleCollapse(item.id);
|
onToggleCollapse(item.id);
|
||||||
} else {
|
} else {
|
||||||
setInternalCollapsed(!internalCollapsed);
|
setInternalCollapsed((prev) => !prev);
|
||||||
}
|
}
|
||||||
};
|
}, [onToggleCollapse, item.id]);
|
||||||
|
|
||||||
if (!item.versions) {
|
const collapsed = onToggleCollapse ? isCollapsed : internalCollapsed;
|
||||||
|
|
||||||
|
if (!item.versions || item.versions.length === 0) {
|
||||||
return <div className="px-6 py-3 text-gray-400">No details available</div>;
|
return <div className="px-6 py-3 text-gray-400">No details available</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const differences = getVersionDifferences(item.versions);
|
|
||||||
|
|
||||||
if (differences.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
const sortedVersions = [...item.versions].sort((a, b) => b.version - a.version);
|
||||||
|
|
||||||
let oldVersion = null;
|
let oldVersion = null;
|
||||||
let newVersion = null;
|
let newVersion = null;
|
||||||
let oldVersionContent = null;
|
let oldVersionContent = null;
|
||||||
let newVersionContent = null;
|
let newVersionContent = null;
|
||||||
|
let diffPaths = new Set<string>();
|
||||||
|
|
||||||
if (item.isUpdated) {
|
if (item.isUpdated && sortedVersions.length >= 2) {
|
||||||
if (item.isRollback) {
|
if (item.isRollback) {
|
||||||
[oldVersion, newVersion] = sortedVersions;
|
[oldVersion, newVersion] = sortedVersions;
|
||||||
} else {
|
} else {
|
||||||
[newVersion, oldVersion] = sortedVersions;
|
[newVersion, oldVersion] = sortedVersions;
|
||||||
}
|
}
|
||||||
oldVersionContent = highlightChangedFields(oldVersion, changedFields, true);
|
|
||||||
newVersionContent = highlightChangedFields(newVersion, changedFields, false);
|
const cleanOldVersion = cleanVersionForComparison(oldVersion);
|
||||||
|
const cleanNewVersion = cleanVersionForComparison(newVersion);
|
||||||
|
diffPaths = getDiffPaths(cleanOldVersion, cleanNewVersion);
|
||||||
|
|
||||||
|
if (diffPaths.size === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
oldVersionContent = (
|
||||||
|
<div className="font-mono text-sm">
|
||||||
|
{renderJsonWithDiffs(
|
||||||
|
cleanOldVersion,
|
||||||
|
diffPaths,
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
excludedFieldsHighlight,
|
||||||
|
cleanOldVersion,
|
||||||
|
cleanNewVersion
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
newVersionContent = (
|
||||||
|
<div className="font-mono text-sm">
|
||||||
|
{renderJsonWithDiffs(
|
||||||
|
cleanNewVersion,
|
||||||
|
diffPaths,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
excludedFieldsHighlight,
|
||||||
|
cleanOldVersion,
|
||||||
|
cleanNewVersion
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
} else if (item.isAdded) {
|
} else if (item.isAdded) {
|
||||||
[newVersion] = sortedVersions;
|
[newVersion] = sortedVersions;
|
||||||
oldVersionContent = <div className="italic text-gray-400" />;
|
const cleanNewVersion = cleanVersionForComparison(newVersion);
|
||||||
newVersionContent = formatAddedJson(newVersion);
|
oldVersionContent = <div className="italic text-gray-400">No previous version</div>;
|
||||||
|
newVersionContent = formatAddedJson(cleanNewVersion);
|
||||||
} else if (item.isDeleted) {
|
} else if (item.isDeleted) {
|
||||||
[oldVersion] = sortedVersions;
|
[oldVersion] = sortedVersions;
|
||||||
oldVersionContent = formatDeletedJson(oldVersion);
|
const cleanOldVersion = cleanVersionForComparison(oldVersion);
|
||||||
newVersionContent = <div className="italic text-gray-400" />;
|
oldVersionContent = formatDeletedJson(cleanOldVersion);
|
||||||
|
newVersionContent = <div className="italic text-gray-400">Version deleted</div>;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderHeader = () => {
|
const renderHeader = () => {
|
||||||
@@ -507,7 +579,7 @@ export const SecretVersionDiffView: React.FC<SecretVersionDiffViewProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isSecret = item.type === "secret";
|
const isSecret = item.type === "secret";
|
||||||
const key = isSecret ? item.secretKey || "" : item.folderName || "";
|
const key = isSecret ? item.secretKey || "Unnamed Secret" : item.folderName || "Unnamed Folder";
|
||||||
let textStyle = "text-white";
|
let textStyle = "text-white";
|
||||||
let changeBadge = null;
|
let changeBadge = null;
|
||||||
|
|
||||||
@@ -565,7 +637,6 @@ export const SecretVersionDiffView: React.FC<SecretVersionDiffViewProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={oldContainerRef}
|
ref={oldContainerRef}
|
||||||
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
|
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
onScroll={() => handleScroll("old")}
|
|
||||||
>
|
>
|
||||||
{oldVersionContent}
|
{oldVersionContent}
|
||||||
</div>
|
</div>
|
||||||
@@ -573,7 +644,6 @@ export const SecretVersionDiffView: React.FC<SecretVersionDiffViewProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={newContainerRef}
|
ref={newContainerRef}
|
||||||
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
|
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
onScroll={() => handleScroll("new")}
|
|
||||||
>
|
>
|
||||||
{newVersionContent}
|
{newVersionContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+4
-3
@@ -43,11 +43,12 @@ const CommitItem = ({
|
|||||||
<div className="border-b border-zinc-800 last:border-b-0">
|
<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="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 flex-col sm:flex-row sm:justify-between">
|
||||||
<div className="flex-1">
|
<div className="w-5/6 flex-1">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
className="text-white hover:underline"
|
className="truncate text-left text-white hover:underline"
|
||||||
|
isFullWidth
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSelectCommit(commit.id, "tab-commit-details");
|
onSelectCommit(commit.id, "tab-commit-details");
|
||||||
@@ -64,7 +65,7 @@ const CommitItem = ({
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-center sm:mt-0">
|
<div className="mt-2 flex w-1/6 items-center justify-end sm:mt-0">
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
|
|||||||
Reference in New Issue
Block a user