mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 23:26:09 +00:00
Fix merge conflicts
This commit is contained in:
Generated
+109
@@ -5,6 +5,9 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/modifiers": "^6.0.1",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
@@ -2468,6 +2471,68 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
|
||||
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
|
||||
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/modifiers": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
|
||||
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.0.6",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
|
||||
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.0.7",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
|
||||
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin": {
|
||||
"version": "11.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
|
||||
@@ -24491,6 +24556,50 @@
|
||||
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
|
||||
"dev": true
|
||||
},
|
||||
"@dnd-kit/accessibility": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
|
||||
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@dnd-kit/core": {
|
||||
"version": "6.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
|
||||
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
|
||||
"requires": {
|
||||
"@dnd-kit/accessibility": "^3.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@dnd-kit/modifiers": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
|
||||
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
|
||||
"requires": {
|
||||
"@dnd-kit/utilities": "^3.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@dnd-kit/sortable": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
|
||||
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
|
||||
"requires": {
|
||||
"@dnd-kit/utilities": "^3.2.0",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@dnd-kit/utilities": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
|
||||
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@emotion/babel-plugin": {
|
||||
"version": "11.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/modifiers": "^6.0.1",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
|
||||
@@ -20,7 +20,8 @@ const integrationSlugNameMapping: Mapping = {
|
||||
'supabase': 'Supabase',
|
||||
'checkly': 'Checkly',
|
||||
'hashicorp-vault': 'Vault',
|
||||
'cloudflare-pages': 'Cloudflare Pages'
|
||||
'cloudflare-pages': 'Cloudflare Pages',
|
||||
'codefresh': 'Codefresh'
|
||||
}
|
||||
|
||||
const envMapping: Mapping = {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@ export const EmptyState = ({
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} size={iconSize} className="mr-4" />
|
||||
<FontAwesomeIcon icon={icon} size={iconSize} />
|
||||
<div className="flex flex-row items-center py-4">
|
||||
<div className="text-sm text-bunker-300">{title}</div>
|
||||
<div>{children}</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from "./keys";
|
||||
export * from "./organization";
|
||||
export * from "./ssoConfig";
|
||||
export * from "./secretFolders";
|
||||
export * from "./secretImports";
|
||||
export * from "./secrets";
|
||||
export * from "./secretSnapshots";
|
||||
export * from "./serviceAccounts";
|
||||
|
||||
@@ -5,5 +5,5 @@ export {
|
||||
useGetIntegrationAuthRailwayEnvironments,
|
||||
useGetIntegrationAuthRailwayServices,
|
||||
useGetIntegrationAuthTeams,
|
||||
useGetIntegrationAuthVercelBranches
|
||||
useGetIntegrationAuthVercelBranches,
|
||||
} from "./queries";
|
||||
|
||||
@@ -19,6 +19,7 @@ const integrationAuthKeys = {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
|
||||
|
||||
getIntegrationAuthRailwayEnvironments: ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
@@ -64,6 +65,7 @@ const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
|
||||
return data.teams;
|
||||
};
|
||||
|
||||
|
||||
const fetchIntegrationAuthVercelBranches = async ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
@@ -224,6 +226,7 @@ export const useGetIntegrationAuthRailwayServices = ({
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useDeleteIntegrationAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -235,3 +238,4 @@ export const useDeleteIntegrationAuth = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useCreateSecretImport, useDeleteSecretImport, useUpdateSecretImport } from "./mutation";
|
||||
export { useGetImportedSecrets, useGetSecretImports } from "./queries";
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { secretImportKeys } from "./queries";
|
||||
import { TCreateSecretImportDTO, TDeleteSecretImportDTO, TUpdateSecretImportDTO } from "./types";
|
||||
|
||||
export const useCreateSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TCreateSecretImportDTO>({
|
||||
mutationFn: async ({ secretImport, environment, workspaceId, folderId }) => {
|
||||
const { data } = await apiRequest.post("/api/v1/secret-imports", {
|
||||
secretImport,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TUpdateSecretImportDTO>({
|
||||
mutationFn: async ({ environment, workspaceId, folderId, secretImports, id }) => {
|
||||
const { data } = await apiRequest.put(`/api/v1/secret-imports/${id}`, {
|
||||
secretImports,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TDeleteSecretImportDTO>({
|
||||
mutationFn: async ({ id, secretImportEnv, secretImportPath }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/secret-imports/${id}`, {
|
||||
data: {
|
||||
secretImportPath,
|
||||
secretImportEnv
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useCallback } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TGetImportedSecrets, TImportedSecrets, TSecretImports } from "./types";
|
||||
|
||||
export const secretImportKeys = {
|
||||
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets-imports"
|
||||
],
|
||||
getSecretImportSecrets: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets-import-sec"
|
||||
]
|
||||
};
|
||||
|
||||
const fetchSecretImport = async (workspaceId: string, environment: string, folderId?: string) => {
|
||||
const { data } = await apiRequest.get<{ secretImport: TSecretImports }>(
|
||||
"/api/v1/secret-imports",
|
||||
{
|
||||
params: {
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.secretImport;
|
||||
};
|
||||
|
||||
export const useGetSecretImports = (workspaceId: string, env: string, folderId?: string) =>
|
||||
useQuery({
|
||||
enabled: Boolean(workspaceId) && Boolean(env),
|
||||
queryKey: secretImportKeys.getProjectSecretImports(workspaceId, env, folderId),
|
||||
queryFn: () => fetchSecretImport(workspaceId, env, folderId)
|
||||
});
|
||||
|
||||
const fetchImportedSecrets = async (
|
||||
workspaceId: string,
|
||||
environment: string,
|
||||
folderId?: string
|
||||
) => {
|
||||
const { data } = await apiRequest.get<{ secrets: TImportedSecrets }>(
|
||||
"/api/v1/secret-imports/secrets",
|
||||
{
|
||||
params: {
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.secrets;
|
||||
};
|
||||
|
||||
export const useGetImportedSecrets = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId,
|
||||
decryptFileKey
|
||||
}: TGetImportedSecrets) =>
|
||||
useQuery({
|
||||
enabled: Boolean(workspaceId) && Boolean(environment) && Boolean(decryptFileKey),
|
||||
queryKey: secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId),
|
||||
queryFn: () => fetchImportedSecrets(workspaceId, environment, folderId),
|
||||
select: useCallback(
|
||||
(data: TImportedSecrets) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
return data.map((el) => ({
|
||||
environment: el.environment,
|
||||
secretPath: el.secretPath,
|
||||
folderId: el.folderId,
|
||||
secrets: el.secrets.map((encSecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretComment = decryptSymmetric({
|
||||
ciphertext: encSecret.secretCommentCiphertext,
|
||||
iv: encSecret.secretCommentIV,
|
||||
tag: encSecret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
|
||||
return {
|
||||
_id: encSecret._id,
|
||||
env: encSecret.environment,
|
||||
key: secretKey,
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt
|
||||
};
|
||||
})
|
||||
}));
|
||||
},
|
||||
[decryptFileKey]
|
||||
)
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { EncryptedSecret } from "../secrets/types";
|
||||
import { UserWsKeyPair } from "../types";
|
||||
|
||||
export type TSecretImports = {
|
||||
_id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
imports: Array<{ environment: string; secretPath: string }>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TImportedSecrets = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
folderId: string;
|
||||
secrets: EncryptedSecret[];
|
||||
}[];
|
||||
|
||||
export type TGetImportedSecrets = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
};
|
||||
|
||||
export type TCreateSecretImportDTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretImport: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TUpdateSecretImportDTO = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretImports: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type TDeleteSecretImportDTO = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretImportPath: string;
|
||||
secretImportEnv: string;
|
||||
};
|
||||
@@ -24,6 +24,10 @@ export const secretKeys = {
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets"
|
||||
],
|
||||
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets-imports"
|
||||
],
|
||||
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"]
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export type DecryptedSecret = {
|
||||
valueOverride?: string;
|
||||
idOverride?: string;
|
||||
overrideAction?: string;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
export type EncryptedSecretVersion = {
|
||||
@@ -98,6 +99,7 @@ export type GetProjectSecretsDTO = {
|
||||
folderId?: string;
|
||||
secretPath?: string;
|
||||
isPaused?: boolean;
|
||||
include_imports?: boolean;
|
||||
onSuccess?: (data: DecryptedSecret[]) => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
CreateAPIKeyRes,
|
||||
DeletOrgMembershipDTO,
|
||||
OrgUser,
|
||||
RenameUserDTO,
|
||||
TokenVersion,
|
||||
UpdateOrgUserRoleDTO,
|
||||
User} from "./types";
|
||||
@@ -45,6 +46,18 @@ const fetchUserAction = async (action: string) => {
|
||||
return data.userAction;
|
||||
};
|
||||
|
||||
export const useRenameUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, RenameUserDTO>({
|
||||
mutationFn: ({ newName }) =>
|
||||
apiRequest.patch("/api/v2/users/me/name", { firstName: newName?.split(" ")[0], lastName: newName?.split(" ").slice(1).join(" ") }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(userKeys.getUser);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUserAction = (action: string) =>
|
||||
useQuery({
|
||||
queryKey: userKeys.userAction,
|
||||
|
||||
@@ -67,6 +67,10 @@ export type CreateAPIKeyRes = {
|
||||
apiKeyData: APIKeyData;
|
||||
}
|
||||
|
||||
export type RenameUserDTO = {
|
||||
newName: string;
|
||||
};
|
||||
|
||||
export type APIKeyData = {
|
||||
_id: string;
|
||||
name: string;
|
||||
|
||||
@@ -408,7 +408,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`}
|
||||
// icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
|
||||
icon="system-outline-168-view-headline"
|
||||
>
|
||||
Audit Logs
|
||||
@@ -473,6 +472,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?._id}/secret-scanning`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/org/${currentOrg?._id}/secret-scanning`}
|
||||
icon="system-outline-69-document-scan"
|
||||
>
|
||||
Secret Scanning
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?._id}/billing`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
export type GitRisks = {
|
||||
id: string;
|
||||
_id: string;
|
||||
description: string;
|
||||
startLine: string;
|
||||
endLine: string;
|
||||
@@ -20,6 +20,7 @@ export type GitRisks = {
|
||||
tags: string[];
|
||||
ruleID: string;
|
||||
fingerprint: string;
|
||||
status: string;
|
||||
|
||||
isFalsePositive: boolean; // New field for marking risks as false positives
|
||||
isResolved: boolean; // New field for marking risks as resolved
|
||||
|
||||
@@ -16,7 +16,7 @@ const linkGitAppInstallationWithOrganization = (installationId: string, sessionI
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
return true
|
||||
}
|
||||
console.log("Failed to link installation to organization");
|
||||
return undefined;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
|
||||
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
|
||||
|
||||
export default function CodefreshCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
setApiKeyErrorText("");
|
||||
if (apiKey.length === 0) {
|
||||
setApiKeyErrorText("API Key cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const integrationAuth = await saveIntegrationAccessToken({
|
||||
workspaceId: localStorage.getItem("projectData.id"),
|
||||
integration: "codefresh",
|
||||
accessId: null,
|
||||
accessToken: apiKey,
|
||||
url: null,
|
||||
namespace: null
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
router.push(`/integrations/codefresh/create?integrationAuthId=${integrationAuth._id}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Codefresh Integration</CardTitle>
|
||||
<FormControl
|
||||
label="Codefresh API Key"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== "" ?? false}
|
||||
>
|
||||
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to Codefresh
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CodefreshCreateIntegrationPage.requireAuth = true;
|
||||
@@ -0,0 +1,155 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import queryString from "query-string";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "../../../components/v2";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
} from "../../../hooks/api/integrationAuth";
|
||||
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
||||
import createIntegration from "../../api/integrations/createIntegration";
|
||||
|
||||
export default function CodefreshCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
||||
|
||||
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||
const [targetApp, setTargetApp] = useState("");
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
if (integrationAuthApps.length > 0) {
|
||||
setTargetApp(integrationAuthApps[0].name);
|
||||
} else {
|
||||
setTargetApp("none");
|
||||
}
|
||||
}
|
||||
}, [integrationAuthApps]);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
if (!integrationAuth?._id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
await createIntegration({
|
||||
integrationAuthId: integrationAuth?._id,
|
||||
isActive: true,
|
||||
app: targetApp,
|
||||
appId:
|
||||
integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
|
||||
?.appId ?? null,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
targetEnvironment: null,
|
||||
targetEnvironmentId: null,
|
||||
targetService: null,
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return integrationAuth &&
|
||||
workspace &&
|
||||
selectedSourceEnvironment &&
|
||||
integrationAuthApps &&
|
||||
targetApp ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Codefresh Integration</CardTitle>
|
||||
<FormControl label="Project Environment" className="mt-4">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Codefresh Service" className="mt-4">
|
||||
<Select
|
||||
value={targetApp}
|
||||
onValueChange={(val) => setTargetApp(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
{integrationAuthApps.length > 0 ? (
|
||||
integrationAuthApps.map((integrationAuthApp) => (
|
||||
<SelectItem
|
||||
value={integrationAuthApp.name}
|
||||
key={`target-app-${integrationAuthApp.name}`}
|
||||
>
|
||||
{integrationAuthApp.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" key="target-app-none">
|
||||
No services found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
}
|
||||
|
||||
CodefreshCreateIntegrationPage.requireAuth = true;
|
||||
@@ -11,6 +11,7 @@ export default function SettingsBilling() {
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("billing.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<BillingSettingsPage />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
|
||||
|
||||
import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession";
|
||||
import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus";
|
||||
import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization";
|
||||
|
||||
export default function SecretScanning() {
|
||||
const router = useRouter()
|
||||
const queryParams = router.query
|
||||
const [integrationEnabled, setIntegrationStatus] = useState(false)
|
||||
|
||||
useEffect(()=>{
|
||||
const linkInstallation = async () => {
|
||||
if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){
|
||||
try {
|
||||
const isLinked = await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string)
|
||||
if (isLinked){
|
||||
router.reload()
|
||||
}
|
||||
|
||||
console.log("installation verification complete")
|
||||
}catch (e){
|
||||
console.log("app installation is stale, start new session", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fetchInstallationStatus = async () => {
|
||||
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")))
|
||||
setIntegrationStatus(status)
|
||||
}
|
||||
|
||||
fetchInstallationStatus()
|
||||
linkInstallation()
|
||||
},[queryParams.state, queryParams.installation_id])
|
||||
|
||||
const generateNewIntegrationSession = async () => {
|
||||
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")))
|
||||
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Secret scanning</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
|
||||
<div className="max-w-7xl px-6 w-full">
|
||||
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div>
|
||||
<div className="mb-6 text-lg text-mineshaft-300">Automatically monitor your GitHub activity and prevent secret leaks</div>
|
||||
<div className="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6">
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="flex flex-row mb-1">Secret Scanning Status: {integrationEnabled ? <p className="text-green ml-1.5 font-semibold">Enabled</p> : <p className="text-red ml-1.5 font-semibold">Not enabled</p>}</div>
|
||||
<div>{integrationEnabled ? <p className="text-mineshaft-300">Your GitHub organization is connected to Infisical, and is being continuously monitored for secret leaks.</p> : <p className="text-mineshaft-300">Connect your GitHub organization to Infisical.</p>}</div>
|
||||
</div>
|
||||
{integrationEnabled ? (
|
||||
<div>
|
||||
<div className="absolute right-[2.5rem] top-[2.5rem] animate-ping rounded-full h-6 w-6 bg-green flex items-center justify-center"/>
|
||||
<div className="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center"/>
|
||||
<div className="absolute right-[2.82rem] top-[2.82rem] animate-ping rounded-full h-3.5 w-3.5 bg-green flex items-center justify-center"/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center h-[3.25rem]">
|
||||
<Button
|
||||
variant="solid"
|
||||
colorSchema="primary"
|
||||
onClick={generateNewIntegrationSession}
|
||||
className="py-2 h-min"
|
||||
>
|
||||
Integrate with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SecretScanningLogsTable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SecretScanning.requireAuth = true;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
export default function SecretScanning() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(()=>{
|
||||
router.push(`${router.asPath.split("secret-scanning")[0]}/org/${localStorage.getItem("orgData.id")}/secret-scanning${router.asPath.split("secret-scanning")[1]}`)
|
||||
}, [])
|
||||
|
||||
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
SecretScanning.requireAuth = true;
|
||||
@@ -1,156 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import createNewIntegrationSession from "../api/secret-scanning/createSecretScanningSession";
|
||||
import getInstallationStatus from "../api/secret-scanning/getInstallationStatus";
|
||||
import getRisksByOrganization, { GitRisks } from "../api/secret-scanning/getRisksByOrganization";
|
||||
import linkGitAppInstallationWithOrganization from "../api/secret-scanning/linkGitAppInstallationWithOrganization";
|
||||
import { RiskStatus } from "../api/secret-scanning/updateRiskStatus";
|
||||
|
||||
export default function SecretScanning() {
|
||||
const router = useRouter()
|
||||
const { state, installationId} = router.query
|
||||
const [integrationEnabled, setIntegrationStatus] = useState(false)
|
||||
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
|
||||
const [selectedRiskStatus, setSelectedRiskStatus] = useState("");
|
||||
|
||||
const handleSelectRiskStatusUpdate = (event: any) => {
|
||||
setSelectedRiskStatus(event.target.value);
|
||||
};
|
||||
|
||||
console.log("selectedRiskStatus===>", selectedRiskStatus)
|
||||
|
||||
useEffect(()=>{
|
||||
const fetchRisks = async () =>{
|
||||
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")))
|
||||
setGitRisks(risks)
|
||||
}
|
||||
|
||||
const linkInstallation = async () => {
|
||||
if (typeof state === "string" && typeof installationId === "string"){
|
||||
try {
|
||||
await linkGitAppInstallationWithOrganization(installationId as string, state as string)
|
||||
console.log("installation verification complete")
|
||||
}catch (e){
|
||||
console.log("app installation is stale, start new session", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fetchInstallationStatus = async () => {
|
||||
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")))
|
||||
setIntegrationStatus(status)
|
||||
}
|
||||
|
||||
fetchInstallationStatus()
|
||||
linkInstallation()
|
||||
fetchRisks()
|
||||
},[state, installationId])
|
||||
|
||||
const generateNewIntegrationSession = async () => {
|
||||
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")))
|
||||
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Secret scanning</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
{/* <NavHeader pageName={"Secret scanning"} isProjectRelated={false} /> */}
|
||||
|
||||
<div className="text-left">
|
||||
{integrationEnabled ? (
|
||||
<b className="text-green-500">Git app is linked to this organization</b>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
onClick={generateNewIntegrationSession}
|
||||
>
|
||||
Integrate with GitHub
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Date
|
||||
</th>
|
||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Secret Type
|
||||
</th>
|
||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
View Risk
|
||||
</th>
|
||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Info
|
||||
</th>
|
||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{gitRisks.map((risk) => {
|
||||
return (
|
||||
<tr key={risk.ruleID}>
|
||||
<td className="py-4 px-6 whitespace-nowrap">{risk.createdAt}</td>
|
||||
<td className="py-4 px-6 whitespace-nowrap">{risk.ruleID}</td>
|
||||
<td className="py-4 px-6 whitespace-nowrap">
|
||||
<a
|
||||
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
|
||||
target="_blank"
|
||||
className="text-red-500" rel="noreferrer"
|
||||
>
|
||||
View Exposed Secret
|
||||
</a>
|
||||
</td>
|
||||
<td className="py-4 px-6 whitespace-nowrap">
|
||||
<div className="font-bold">
|
||||
<a href={`https://github.com/${risk.repositoryFullName}`}>
|
||||
{risk.repositoryFullName}
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span>{risk.file}</span><br/>
|
||||
<br/>
|
||||
<span className="font-bold">{risk.author}</span><br/>
|
||||
<span>{risk.email}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4 px-6 whitespace-nowrap">
|
||||
{risk.isResolved ? "Resolved" : "Needs Attention"}
|
||||
</td>
|
||||
<td className="py-4 px-6 whitespace-nowrap">
|
||||
{risk.isResolved ? "Resolved" : "Needs Attention"}
|
||||
</td>
|
||||
<td className="py-4 px-6 whitespace-nowrap">
|
||||
<select
|
||||
value={selectedRiskStatus}
|
||||
onChange={handleSelectRiskStatusUpdate}
|
||||
className="block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option>Unresolved</option>
|
||||
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>This is a false positive</option>
|
||||
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolve risk</option>
|
||||
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolve</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,19 @@ import { FormProvider, useFieldArray, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors
|
||||
} from "@dnd-kit/core";
|
||||
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
||||
import { arrayMove } from "@dnd-kit/sortable";
|
||||
import {
|
||||
faAngleDown,
|
||||
faArrowLeft,
|
||||
faCheck,
|
||||
faClockRotateLeft,
|
||||
@@ -10,12 +23,14 @@ import {
|
||||
faDownload,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileImport,
|
||||
faFolderPlus,
|
||||
faMagnifyingGlass,
|
||||
faPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
@@ -41,10 +56,14 @@ import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks";
|
||||
import {
|
||||
useBatchSecretsOp,
|
||||
useCreateFolder,
|
||||
useCreateSecretImport,
|
||||
useCreateWsTag,
|
||||
useDeleteFolder,
|
||||
useDeleteSecretImport,
|
||||
useGetImportedSecrets,
|
||||
useGetProjectFolders,
|
||||
useGetProjectSecrets,
|
||||
useGetSecretImports,
|
||||
useGetSecretVersion,
|
||||
useGetSnapshotSecrets,
|
||||
useGetUserAction,
|
||||
@@ -55,7 +74,8 @@ import {
|
||||
useGetWsTags,
|
||||
usePerformSecretRollback,
|
||||
useRegisterUserAction,
|
||||
useUpdateFolder
|
||||
useUpdateFolder,
|
||||
useUpdateSecretImport
|
||||
} from "@app/hooks/api";
|
||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
@@ -71,6 +91,8 @@ import {
|
||||
import { PitDrawer } from "./components/PitDrawer";
|
||||
import { SecretDetailDrawer } from "./components/SecretDetailDrawer";
|
||||
import { SecretDropzone } from "./components/SecretDropzone";
|
||||
import { SecretImportForm } from "./components/SecretImportForm";
|
||||
import { SecretImportSection } from "./components/SecretImportSection";
|
||||
import { SecretInputRow } from "./components/SecretInputRow";
|
||||
import { SecretTableHeader } from "./components/SecretTableHeader";
|
||||
import {
|
||||
@@ -84,7 +106,7 @@ import {
|
||||
} from "./DashboardPage.utils";
|
||||
|
||||
const USER_ACTION_PUSH = "first_time_secrets_pushed";
|
||||
|
||||
type TDeleteSecretImport = { environment: string; secretPath: string };
|
||||
/*
|
||||
* Some imp aspects to consider. Here there are multiple stats changing
|
||||
* Thus ideally we need to use a context. But instead we rely on react hook form
|
||||
@@ -113,7 +135,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
"compareSecrets",
|
||||
"folderForm",
|
||||
"deleteFolder",
|
||||
"upgradePlan"
|
||||
"upgradePlan",
|
||||
"addSecretImport",
|
||||
"deleteSecretImport"
|
||||
] as const);
|
||||
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
@@ -129,6 +153,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const { currentWorkspace, isLoading } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const workspaceId = currentWorkspace?._id as string;
|
||||
const selectedEnvSlug = selectedEnv?.slug || "";
|
||||
|
||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -161,7 +187,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
workspaceId,
|
||||
env: selectedEnv?.slug || "",
|
||||
env: selectedEnvSlug,
|
||||
decryptFileKey: latestFileKey!,
|
||||
isPaused: Boolean(snapshotId),
|
||||
folderId
|
||||
@@ -169,7 +195,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
|
||||
const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({
|
||||
workspaceId: workspaceId || "",
|
||||
environment: selectedEnv?.slug || "",
|
||||
environment: selectedEnvSlug,
|
||||
parentFolderId: folderId,
|
||||
isPaused: isRollbackMode,
|
||||
sortDir
|
||||
@@ -182,7 +208,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
isFetchingNextPage
|
||||
} = useGetWorkspaceSecretSnapshots({
|
||||
workspaceId,
|
||||
environment: selectedEnv?.slug || "",
|
||||
environment: selectedEnvSlug,
|
||||
folder: folderId,
|
||||
limit: 10
|
||||
});
|
||||
@@ -193,17 +219,18 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
isFetching: isSnapshotChanging
|
||||
} = useGetSnapshotSecrets({
|
||||
snapshotId: snapshotId || "",
|
||||
env: selectedEnv?.slug || "",
|
||||
env: selectedEnvSlug,
|
||||
decryptFileKey: latestFileKey!
|
||||
});
|
||||
|
||||
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount(
|
||||
workspaceId,
|
||||
selectedEnv?.slug || "",
|
||||
selectedEnvSlug,
|
||||
folderId
|
||||
);
|
||||
|
||||
const { data: wsTags } = useGetWsTags(workspaceId);
|
||||
|
||||
// mutation calls
|
||||
const { mutateAsync: batchSecretOp } = useBatchSecretsOp();
|
||||
const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
|
||||
@@ -213,6 +240,50 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const { mutateAsync: updateFolder } = useUpdateFolder(folderId);
|
||||
const { mutateAsync: deleteFolder } = useDeleteFolder(folderId);
|
||||
|
||||
const { data: secretImportCfg, isFetching: isSecretImportCfgFetching } = useGetSecretImports(
|
||||
workspaceId,
|
||||
selectedEnvSlug,
|
||||
folderId
|
||||
);
|
||||
|
||||
const { data: importedSecrets } = useGetImportedSecrets({
|
||||
workspaceId,
|
||||
decryptFileKey: latestFileKey!,
|
||||
environment: selectedEnvSlug,
|
||||
folderId
|
||||
});
|
||||
|
||||
// This is for dnd-kit. As react-query state mutation async
|
||||
// This will act as a placeholder to avoid a glitching animation on dropping items
|
||||
const [items, setItems] = useState<
|
||||
Array<{ environment: string; secretPath: string; id: string }>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isSecretImportCfgFetching ||
|
||||
// case in which u go to a folder and come back to fill in with cache data
|
||||
(items.length === 0 && secretImportCfg?.imports?.length !== 0 && isSecretImportCfgFetching)
|
||||
) {
|
||||
setItems(
|
||||
secretImportCfg?.imports?.map((el) => ({
|
||||
...el,
|
||||
id: `${el.environment}-${el.secretPath}`
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
}, [isSecretImportCfgFetching]);
|
||||
|
||||
const { mutateAsync: createSecretImport } = useCreateSecretImport();
|
||||
const { mutate: updateSecretImportSync } = useUpdateSecretImport();
|
||||
const { mutateAsync: deleteSecretImport } = useDeleteSecretImport();
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {}),
|
||||
useSensor(TouchSensor, {}),
|
||||
useSensor(KeyboardSensor, {})
|
||||
);
|
||||
|
||||
const method = useForm<FormData>({
|
||||
// why any: well yup inferred ts expects other keys to defined as undefined
|
||||
defaultValues: secrets as any,
|
||||
@@ -319,14 +390,12 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
await performSecretRollback({
|
||||
workspaceId,
|
||||
version: snapshotSecret.version,
|
||||
environment: selectedEnv?.slug || "",
|
||||
environment: selectedEnvSlug,
|
||||
folderId
|
||||
});
|
||||
setValue("isSnapshotMode", false);
|
||||
setSnaphotId(null);
|
||||
queryClient.invalidateQueries(
|
||||
secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || "")
|
||||
);
|
||||
queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, selectedEnvSlug));
|
||||
createNotification({
|
||||
text: "Successfully rollback secrets",
|
||||
type: "success"
|
||||
@@ -522,6 +591,79 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecretImportCreate = async (env: string, secretPath: string) => {
|
||||
try {
|
||||
await createSecretImport({
|
||||
workspaceId,
|
||||
environment: selectedEnv?.slug || "",
|
||||
folderId,
|
||||
secretImport: {
|
||||
environment: env,
|
||||
secretPath
|
||||
}
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully create secret link"
|
||||
});
|
||||
handlePopUpClose("addSecretImport");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create secret link",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecretImportDelete = async () => {
|
||||
const { environment: importEnv, secretPath: impSecPath } = popUp.deleteSecretImport
|
||||
?.data as TDeleteSecretImport;
|
||||
try {
|
||||
if (secretImportCfg?._id) {
|
||||
await deleteSecretImport({
|
||||
workspaceId,
|
||||
environment: selectedEnvSlug,
|
||||
folderId,
|
||||
id: secretImportCfg?._id,
|
||||
secretImportEnv: importEnv,
|
||||
secretImportPath: impSecPath
|
||||
});
|
||||
handlePopUpClose("deleteSecretImport");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully removed secret link"
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to remove secret link",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (evt: DragEndEvent) => {
|
||||
const { active, over } = evt;
|
||||
if (over?.id && active.id !== over.id) {
|
||||
const oldIndex = items.findIndex(({ id }) => id === active.id);
|
||||
const newIndex = items.findIndex(({ id }) => id === over.id);
|
||||
const newImportOrder = arrayMove(items, oldIndex, newIndex);
|
||||
setItems(newImportOrder);
|
||||
updateSecretImportSync({
|
||||
workspaceId,
|
||||
environment: selectedEnvSlug,
|
||||
folderId,
|
||||
id: secretImportCfg?._id || "",
|
||||
secretImports: newImportOrder.map((el) => ({
|
||||
environment: el.environment,
|
||||
secretPath: el.secretPath
|
||||
}))
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// when secrets is not loading and secrets list is empty
|
||||
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
|
||||
|
||||
@@ -534,7 +676,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const isSnapshotSecretEmtpy =
|
||||
isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
|
||||
const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy;
|
||||
const isEmptyPage = isFoldersEmpty && isSecretEmpty;
|
||||
const isSecretImportEmpty = !secretImportCfg?.imports?.length;
|
||||
const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty;
|
||||
|
||||
if (isSecretsLoading || isEnvListLoading) {
|
||||
return (
|
||||
@@ -652,54 +795,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
</Button>
|
||||
</div>
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<>
|
||||
<div className="block lg:hidden">
|
||||
<Tooltip content="Add Folder">
|
||||
<IconButton
|
||||
ariaLabel="recovery"
|
||||
variant="outline_bg"
|
||||
onClick={() => handlePopUpOpen("folderForm")}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFolderPlus} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="block lg:hidden">
|
||||
<Tooltip content="Point-in-time Recovery">
|
||||
<IconButton
|
||||
ariaLabel="recovery"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (secretContainer.current) {
|
||||
secretContainer.current.scroll({
|
||||
top: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||
onClick={() => handlePopUpOpen("folderForm")}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
>
|
||||
Add Folder
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!(isReadOnly || isRollbackMode)) {
|
||||
if (secretContainer.current) {
|
||||
secretContainer.current.scroll({
|
||||
top: 0,
|
||||
@@ -708,15 +808,48 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
}
|
||||
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
|
||||
setSearchFilter("");
|
||||
}}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
>
|
||||
Add Secret
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}}
|
||||
className="font-semibold bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-l-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 pr-4 duration-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="px-2"/>Add Secret
|
||||
</button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
||||
<div className="bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-r-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 duration-200">
|
||||
<FontAwesomeIcon icon={faAngleDown} className="pr-2 pl-1.5"/>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="mt-1 z-[60] left-20 w-[10.8rem]">
|
||||
<div className="bg-mineshaft-800 p-1 border border-mineshaft-600 rounded-md">
|
||||
<div className="w-full pb-1">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||
onClick={() => handlePopUpOpen("folderForm")}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
isFullWidth
|
||||
>
|
||||
Add Folder
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
|
||||
onClick={() => handlePopUpOpen("addSecretImport")}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
isFullWidth
|
||||
>
|
||||
Add Import
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
{isRollbackMode && (
|
||||
<Button
|
||||
@@ -751,49 +884,67 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
ref={secretContainer}
|
||||
>
|
||||
{!isEmptyPage && (
|
||||
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
||||
<table className="secret-table relative">
|
||||
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
||||
<tbody className="max-h-96 overflow-y-auto">
|
||||
<FolderSection
|
||||
onFolderOpen={handleFolderOpen}
|
||||
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
|
||||
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
|
||||
folders={folderList}
|
||||
search={searchFilter}
|
||||
/>
|
||||
{fields.map(({ id, _id }, index) => (
|
||||
<SecretInputRow
|
||||
key={id}
|
||||
isReadOnly={isReadOnly}
|
||||
isRollbackMode={isRollbackMode}
|
||||
isAddOnly={isAddOnly}
|
||||
index={index}
|
||||
searchTerm={searchFilter}
|
||||
onSecretDelete={onSecretDelete}
|
||||
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
|
||||
isSecretValueHidden={isSecretValueHidden}
|
||||
wsTags={wsTags}
|
||||
onCreateTagOpen={() => handlePopUpOpen("addTag")}
|
||||
<DndContext
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
>
|
||||
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
||||
<table className="secret-table relative">
|
||||
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
||||
<tbody className="max-h-96 overflow-y-auto">
|
||||
<SecretImportSection
|
||||
onSecretImportDelete={(impSecEnv, impSecPath) =>
|
||||
handlePopUpOpen("deleteSecretImport", {
|
||||
environment: impSecEnv,
|
||||
secretPath: impSecPath
|
||||
})
|
||||
}
|
||||
secrets={secrets?.secrets}
|
||||
importedSecrets={importedSecrets}
|
||||
items={items}
|
||||
/>
|
||||
))}
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<tr>
|
||||
<td colSpan={3} className="hover:bg-mineshaft-700">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
|
||||
onClick={onAppendSecret}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
<span className="ml-2 w-20">Add Secret</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
<FolderSection
|
||||
onFolderOpen={handleFolderOpen}
|
||||
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
|
||||
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
|
||||
folders={folderList}
|
||||
search={searchFilter}
|
||||
/>
|
||||
{fields.map(({ id, _id }, index) => (
|
||||
<SecretInputRow
|
||||
key={id}
|
||||
isReadOnly={isReadOnly}
|
||||
isRollbackMode={isRollbackMode}
|
||||
isAddOnly={isAddOnly}
|
||||
index={index}
|
||||
searchTerm={searchFilter}
|
||||
onSecretDelete={onSecretDelete}
|
||||
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
|
||||
isSecretValueHidden={isSecretValueHidden}
|
||||
wsTags={wsTags}
|
||||
onCreateTagOpen={() => handlePopUpOpen("addTag")}
|
||||
/>
|
||||
))}
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<tr>
|
||||
<td colSpan={3} className="hover:bg-mineshaft-700">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
|
||||
onClick={onAppendSecret}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
<span className="ml-2 w-20">Add Secret</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
</DndContext>
|
||||
)}
|
||||
<PitDrawer
|
||||
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
|
||||
@@ -881,6 +1032,20 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Modal
|
||||
isOpen={popUp?.addSecretImport?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addSecretImport", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Add Secret Link"
|
||||
subTitle="To inherit secrets from another environment or folder"
|
||||
>
|
||||
<SecretImportForm
|
||||
environments={currentWorkspace?.environments}
|
||||
onCreate={handleSecretImportCreate}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteFolder.isOpen}
|
||||
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
|
||||
@@ -888,6 +1053,16 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
||||
onDeleteApproved={handleFolderDelete}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSecretImport.isOpen}
|
||||
deleteKey="unlink"
|
||||
title="Do you want to remove this secret import?"
|
||||
subTitle={`This will unlink secrets from environment ${
|
||||
(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.environment
|
||||
} of path ${(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.secretPath}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
||||
onDeleteApproved={handleSecretImportDelete}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={popUp?.compareSecrets?.isOpen}
|
||||
onOpenChange={(open) => handlePopUpToggle("compareSecrets", open)}
|
||||
|
||||
@@ -67,7 +67,7 @@ export const FolderSection = ({
|
||||
ariaLabel="delete"
|
||||
onClick={() => handleFolderDelete(id, name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
onCreate: (environment: string, secretPath: string) => Promise<void>;
|
||||
environments?: Array<{ slug: string; name: string }>;
|
||||
};
|
||||
|
||||
const formSchema = yup.object({
|
||||
environment: yup.string().required().label("Environment").trim(),
|
||||
secretPath: yup
|
||||
.string()
|
||||
.required()
|
||||
.label("Secret Path")
|
||||
.trim()
|
||||
.transform((val) =>
|
||||
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
|
||||
)
|
||||
});
|
||||
|
||||
type TFormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
export const SecretImportForm = ({ onCreate, environments = [] }: Props): JSX.Element => {
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TFormData>({
|
||||
resolver: yupResolver(formSchema)
|
||||
});
|
||||
|
||||
const onSubmit = async ({ environment, secretPath }: TFormData) => {
|
||||
await onCreate(environment, secretPath);
|
||||
reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Environment" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{environments.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
defaultValue="/"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||
Create
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SecretImportForm } from "./SecretImportForm";
|
||||
@@ -0,0 +1,157 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import {
|
||||
faFileImport,
|
||||
faFolder,
|
||||
faKey,
|
||||
faUpDown,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { EmptyState, IconButton, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks/useToggle";
|
||||
|
||||
type Props = {
|
||||
onDelete: (environment: string, secretPath: string) => void;
|
||||
importedEnv: string;
|
||||
importedSecPath: string;
|
||||
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
|
||||
};
|
||||
|
||||
// to show the environment and folder icon
|
||||
export const EnvFolderIcon = ({ env, secretPath }: { env: string; secretPath: string }) => (
|
||||
<div className="inline-flex items-center space-x-2">
|
||||
<div style={{ minWidth: "96px" }}>{env || "-"}</div>
|
||||
{secretPath && (
|
||||
<div className="inline-flex items-center space-x-2 border-l border-mineshaft-600 pl-2">
|
||||
<FontAwesomeIcon icon={faFolder} size="lg" className="text-primary-700" />
|
||||
<span>{secretPath}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SecretImportItem = ({
|
||||
importedEnv,
|
||||
importedSecPath,
|
||||
onDelete,
|
||||
importedSecrets = []
|
||||
}: Props) => {
|
||||
const [isExpanded, setIsExpanded] = useToggle();
|
||||
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
|
||||
id: `${importedEnv}-${importedSecPath}`
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
setIsExpanded.off();
|
||||
}
|
||||
}, [isDragging]);
|
||||
|
||||
const style = {
|
||||
transform: transform ? `translateY(${transform.y ? Math.round(transform.y) : 0}px)` : "",
|
||||
transition
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
|
||||
onClick={() => setIsExpanded.toggle()}
|
||||
>
|
||||
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<FontAwesomeIcon icon={faFileImport} className="text-primary-700" />
|
||||
</td>
|
||||
<td
|
||||
colSpan={2}
|
||||
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
|
||||
style={{ paddingTop: "0", paddingBottom: "0" }}
|
||||
>
|
||||
<div className="flex-grow p-2">
|
||||
<EnvFolderIcon env={rowEnv?.name || ""} secretPath={importedSecPath} />
|
||||
</div>
|
||||
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Change Order" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="expand"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUpDown} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
onDelete(importedEnv, importedSecPath);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{isExpanded && !isDragging && (
|
||||
<td colSpan={3}>
|
||||
<div className="rounded-md bg-bunker-700 p-4 pb-6">
|
||||
<div className="mb-2 text-lg font-medium">Secrets Imported</div>
|
||||
<TableContainer>
|
||||
<table className="secret-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Key</td>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Value</td>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Override</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{importedSecrets?.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<EmptyState title="No secrets found" icon={faKey} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{importedSecrets.map(({ key, value, overriden }, index) => (
|
||||
<tr key={`${importedEnv}-${importedSecPath}-${key}-${index + 1}`}>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{value}
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
|
||||
|
||||
import { SecretImportItem } from "./SecretImportItem";
|
||||
|
||||
type TImportedSecrets = Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
folderId: string;
|
||||
secrets: DecryptedSecret[];
|
||||
}>;
|
||||
|
||||
const SECRET_IN_DASHBOARD = "Present In Dashboard";
|
||||
|
||||
export const computeImportedSecretRows = (
|
||||
importedSecEnv: string,
|
||||
importedSecPath: string,
|
||||
importSecrets: TImportedSecrets = [],
|
||||
secrets: DecryptedSecret[] = [],
|
||||
environments: { name: string; slug: string }[] = []
|
||||
) => {
|
||||
const importedSecIndex = importSecrets.findIndex(
|
||||
({ secretPath, environment }) =>
|
||||
secretPath === importedSecPath && importedSecEnv === environment
|
||||
);
|
||||
if (importedSecIndex === -1) return [];
|
||||
|
||||
const importedSec = importSecrets[importedSecIndex];
|
||||
|
||||
const overridenSec: Record<string, { env: string; secretPath: string }> = {};
|
||||
const envSlug2Name: Record<string, string> = {};
|
||||
environments.forEach((el) => {
|
||||
envSlug2Name[el.slug] = el.name;
|
||||
});
|
||||
|
||||
for (let i = importedSecIndex + 1; i < importSecrets.length; i += 1) {
|
||||
importSecrets[i].secrets.forEach((el) => {
|
||||
overridenSec[el.key] = {
|
||||
env: envSlug2Name?.[importSecrets[i].environment] || "unknown",
|
||||
secretPath: importSecrets[i].secretPath
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
secrets.forEach((el) => {
|
||||
overridenSec[el.key] = { env: SECRET_IN_DASHBOARD, secretPath: "" };
|
||||
});
|
||||
|
||||
return importedSec.secrets.map(({ key, value }) => ({
|
||||
key,
|
||||
value,
|
||||
overriden: overridenSec?.[key]
|
||||
}));
|
||||
};
|
||||
|
||||
type Props = {
|
||||
secrets?: DecryptedSecret[];
|
||||
importedSecrets?: TImportedSecrets;
|
||||
onSecretImportDelete: (env: string, secPath: string) => void;
|
||||
items: { id: string; environment: string; secretPath: string }[];
|
||||
};
|
||||
|
||||
export const SecretImportSection = ({
|
||||
secrets = [],
|
||||
importedSecrets = [],
|
||||
onSecretImportDelete,
|
||||
items = []
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
|
||||
return (
|
||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||
{items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
|
||||
<SecretImportItem
|
||||
key={id}
|
||||
importedEnv={importSecEnv}
|
||||
importedSecrets={computeImportedSecretRows(
|
||||
importSecEnv,
|
||||
impSecPath,
|
||||
importedSecrets,
|
||||
secrets,
|
||||
environments
|
||||
)}
|
||||
onDelete={onSecretImportDelete}
|
||||
importedSecPath={impSecPath}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SecretImportSection } from "./SecretImportSection";
|
||||
@@ -407,7 +407,7 @@ export const SecretInputRow = memo(
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete">
|
||||
<IconButton
|
||||
size="md"
|
||||
size="lg"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
|
||||
@@ -92,6 +92,10 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
|
||||
case "cloudflare-pages":
|
||||
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
|
||||
break;
|
||||
case "codefresh":
|
||||
link = `${window.location.origin}/integrations/codefresh/authorize`;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export const IntegrationsPage = ({ frameworkIntegrations }: Props) => {
|
||||
|
||||
const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
|
||||
useGetCloudIntegrations();
|
||||
|
||||
const { data: integrationAuths, isLoading: isIntegrationAuthLoading } =
|
||||
useGetWorkspaceAuthorizations(
|
||||
workspaceId,
|
||||
|
||||
+3
-1
@@ -31,6 +31,8 @@ export const CloudIntegrationSection = ({
|
||||
|
||||
const isEmpty = !isLoading && !cloudIntegrations?.length;
|
||||
|
||||
const sortedCloudIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||
@@ -43,7 +45,7 @@ export const CloudIntegrationSection = ({
|
||||
<Skeleton className="h-32" key={`cloud-integration-skeleton-${index + 1}`} />
|
||||
))}
|
||||
{!isLoading &&
|
||||
cloudIntegrations?.map((cloudIntegration) => (
|
||||
sortedCloudIntegrations?.map((cloudIntegration) => (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
|
||||
+3
-1
@@ -12,6 +12,8 @@ type Props = {
|
||||
export const FrameworkIntegrationSection = ({ frameworks }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sortedFrameworks = frameworks.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-4 mt-12 mb-4 flex flex-col items-start justify-between px-2 text-xl">
|
||||
@@ -22,7 +24,7 @@ export const FrameworkIntegrationSection = ({ frameworks }: Props) => {
|
||||
className="mx-6 mt-4 grid grid-flow-dense gap-3"
|
||||
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))" }}
|
||||
>
|
||||
{frameworks.map((framework) => (
|
||||
{sortedFrameworks.map((framework) => (
|
||||
<a
|
||||
key={`framework-integration-${framework.slug}`}
|
||||
href={framework.docsLink}
|
||||
|
||||
-1
@@ -32,7 +32,6 @@ export const IntegrationsSection = ({
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"deleteConfirmation"
|
||||
] as const);
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="mx-4 mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import updateRiskStatus, { RiskStatus } from "@app/pages/api/secret-scanning/updateRiskStatus";
|
||||
|
||||
export const RiskStatusSelection = ({riskId, currentSelection}: {riskId: any, currentSelection: any }) => {
|
||||
const [selectedRiskStatus, setSelectedRiskStatus] = useState(currentSelection);
|
||||
useEffect(()=>{
|
||||
if (currentSelection !== selectedRiskStatus){
|
||||
const updateSelection = async () =>{
|
||||
await updateRiskStatus(String(localStorage.getItem("orgData.id")), riskId, selectedRiskStatus)
|
||||
}
|
||||
updateSelection()
|
||||
}
|
||||
},[selectedRiskStatus])
|
||||
|
||||
return (
|
||||
<select
|
||||
value={selectedRiskStatus}
|
||||
onChange={(e) => setSelectedRiskStatus(e.target.value)}
|
||||
className="block w-full py-2 px-3 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option>Unresolved</option>
|
||||
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>This is a false positive, resolved</option>
|
||||
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolved</option>
|
||||
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolved</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr} from "@app/components/v2";
|
||||
import timeSince from "@app/ee/utilities/timeSince";
|
||||
import getRisksByOrganization, { GitRisks } from "@app/pages/api/secret-scanning/getRisksByOrganization";
|
||||
|
||||
import { RiskStatusSelection } from "./RiskStatusSelection";
|
||||
|
||||
export const SecretScanningLogsTable = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRisks = async () => {
|
||||
setIsLoading(true);
|
||||
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")))
|
||||
setGitRisks(risks);
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
fetchRisks();
|
||||
},[])
|
||||
|
||||
|
||||
return (
|
||||
<TableContainer className="mt-8">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">Date</Th>
|
||||
<Th className="flex-1">Secret Type</Th>
|
||||
<Th className="flex-1">View Risk</Th>
|
||||
<Th className="flex-1">Info</Th>
|
||||
<Th className="flex-1">Status</Th>
|
||||
<Th className="flex-1">Action</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isLoading && gitRisks && gitRisks?.map((risk) => {
|
||||
return (
|
||||
<Tr key={risk.ruleID} className="h-10">
|
||||
<Td>{timeSince(new Date(risk.createdAt))}</Td>
|
||||
<Td>{risk.ruleID}</Td>
|
||||
<Td>
|
||||
<a
|
||||
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
|
||||
target="_blank"
|
||||
className="text-red-500" rel="noreferrer"
|
||||
>
|
||||
View Exposed Secret
|
||||
</a>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="font-bold">
|
||||
<a href={`https://github.com/${risk.repositoryFullName}`}>
|
||||
{risk.repositoryFullName}
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span>{risk.file}</span><br/>
|
||||
<br/>
|
||||
<span className="font-bold">{risk.author}</span><br/>
|
||||
<span>{risk.email}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{risk.isResolved ? "Resolved" : "Needs Attention"}</Td>
|
||||
<Td>
|
||||
<RiskStatusSelection riskId={risk._id} currentSelection={risk.status}/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isLoading && <TableSkeleton columns={7} key="gitRisks" />}
|
||||
{!isLoading && gitRisks && gitRisks?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={7}>
|
||||
<EmptyState
|
||||
title="No risks detected."
|
||||
icon={faCheck}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { SecretScanningLogsTable } from "./SecretScanningLogsTable";
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
export const BillingSettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full px-6">
|
||||
<div className="max-w-7xl w-full">
|
||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
|
||||
<div className="max-w-7xl px-6 w-full">
|
||||
<div className="my-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">{t("billing.title")}</p>
|
||||
<div />
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export const ChangeLanguageSection = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<p className="text-xl font-semibold text-mineshaft-100 mb-8">
|
||||
{t("settings.personal.change-language")}
|
||||
</p>
|
||||
|
||||
+2
@@ -3,10 +3,12 @@ import { ChangePasswordSection } from "../ChangePasswordSection";
|
||||
import { EmergencyKitSection } from "../EmergencyKitSection";
|
||||
import { SecuritySection } from "../SecuritySection";
|
||||
import { SessionsSection } from "../SessionsSection";
|
||||
import { UserNameSection } from "../UserNameSection";
|
||||
|
||||
export const PersonalSecurityTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<UserNameSection />
|
||||
<ChangeLanguageSection />
|
||||
<SecuritySection />
|
||||
<SessionsSection />
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ const tabs = [
|
||||
export const PersonalTabGroup = () => {
|
||||
return (
|
||||
<Tab.Group>
|
||||
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
|
||||
<Tab.List className="mb-4 border-b-2 border-mineshaft-800 w-full">
|
||||
{tabs.map((tab) => (
|
||||
<Tab as={Fragment} key={tab.key}>
|
||||
{({ selected }) => (
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
import { useRenameUser } from "@app/hooks/api/users/queries";
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup.string().required().label("User Name"),
|
||||
});
|
||||
|
||||
type FormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
export const UserNameSection = (): JSX.Element => {
|
||||
const { user } = useUser();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset
|
||||
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
const { mutateAsync, isLoading } = useRenameUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
reset({ name: `${user?.firstName}${user?.lastName && " "}${user?.lastName}` });
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
try {
|
||||
if (!user?._id) return;
|
||||
if (name === "") return;
|
||||
|
||||
await mutateAsync({ newName: name});
|
||||
createNotification({
|
||||
text: "Successfully renamed user",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to rename user",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
|
||||
User name
|
||||
</p>
|
||||
<div className="mb-2 max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder={`${user?.firstName} ${user?.lastName}`} {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { UserNameSection } from "./UserNameSection";
|
||||
@@ -26,15 +26,13 @@ export const ProjectSettingsPage = () => {
|
||||
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
|
||||
</div>
|
||||
<Tab.Group>
|
||||
<Tab.List className="mb-6 w-full border-b-2 border-mineshaft-800">
|
||||
<Tab.List className="mb-4 w-full border-b-2 border-mineshaft-800">
|
||||
{tabs.map((tab) => (
|
||||
<Tab as={Fragment} key={tab.key}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`w-30 p-4 font-semibold outline-none ${
|
||||
selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"
|
||||
}`}
|
||||
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
|
||||
+1
-3
@@ -1,6 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
@@ -27,7 +26,6 @@ export const ProjectNameChangeSection = () => {
|
||||
control,
|
||||
reset
|
||||
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
@@ -67,7 +65,7 @@ export const ProjectNameChangeSection = () => {
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
|
||||
{t("common.display-name")}
|
||||
Project Name
|
||||
</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
|
||||
Reference in New Issue
Block a user