From 78926247091a9404e62f5a8ef2bf40f1b4f3a267 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Tue, 7 Feb 2023 16:29:15 -0800 Subject: [PATCH] Added tags to secrets in the dashboard --- .../src/controllers/v2/secretsController.ts | 15 ++ backend/src/controllers/v2/tagController.ts | 4 +- .../v1/secretSnapshotController.ts | 8 +- frontend/public/data/frequentInterfaces.ts | 10 + .../basic/dialog/AddWorkspaceDialog.tsx | 8 +- .../basic/table/EnvironmentsTable.tsx | 2 +- .../src/components/dashboard/AddTagsMenu.tsx | 61 ++++++ .../dashboard/DashboardInputField.tsx | 7 +- .../dashboard/DeleteActionButton.tsx | 2 +- .../src/components/dashboard/DropZone.tsx | 6 +- frontend/src/components/dashboard/KeyPair.tsx | 91 +++++++-- frontend/src/components/dashboard/SideBar.tsx | 2 +- .../utilities/secrets/encryptSecrets.ts | 6 +- .../utilities/secrets/getSecretsForProject.ts | 12 +- .../DeleteActionModal/DeleteActionModal.tsx | 2 +- frontend/src/components/v2/Select/Select.tsx | 2 +- .../src/ee/components/PITRecoverySidebar.tsx | 8 +- frontend/src/hooks/api/index.tsx | 1 + frontend/src/hooks/api/tags/index.tsx | 3 + frontend/src/hooks/api/tags/queries.tsx | 62 ++++++ frontend/src/hooks/api/tags/types.ts | 39 ++++ frontend/src/hooks/api/types.ts | 3 +- frontend/src/hooks/api/workspace/index.tsx | 3 +- frontend/src/hooks/api/workspace/types.ts | 1 + .../pages/api/workspace/getWorkspaceTags.ts | 22 +++ frontend/src/pages/dashboard/[id].tsx | 171 +++++++++++----- .../ProjectSettingsPage.tsx | 56 +++++- .../EnvironmentSection/EnvironmentSection.tsx | 2 +- .../SecretTagsSection/SecretTagsSection.tsx | 186 ++++++++++++++++++ .../components/SecretTagsSection/index.tsx | 1 + .../ProjectSettingsPage/components/index.tsx | 1 + 31 files changed, 695 insertions(+), 102 deletions(-) create mode 100644 frontend/src/components/dashboard/AddTagsMenu.tsx create mode 100644 frontend/src/hooks/api/tags/index.tsx create mode 100644 frontend/src/hooks/api/tags/queries.tsx create mode 100644 frontend/src/hooks/api/tags/types.ts create mode 100644 frontend/src/pages/api/workspace/getWorkspaceTags.ts create mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx create mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/index.tsx diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 5ac86e9039..ff39791ac9 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -103,6 +103,9 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueCiphertext: string; secretValueIV: string; secretValueTag: string; + secretCommentCiphertext: string; + secretCommentIV: string; + secretCommentTag: string; tags: string[] } @@ -115,6 +118,9 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, tags }: secretsToCreateType) => { return ({ @@ -129,6 +135,9 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, tags }); }) @@ -160,6 +169,9 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueIV, secretValueTag, secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, tags }) => ({ _id: new Types.ObjectId(), @@ -178,6 +190,9 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueIV, secretValueTag, secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, tags })) }); diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts index 250ee08a5b..b737eb41a7 100644 --- a/backend/src/controllers/v2/tagController.ts +++ b/backend/src/controllers/v2/tagController.ts @@ -52,9 +52,9 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => { UnauthorizedRequestError({ message: 'Failed to validate membership' }); } - await Tag.findByIdAndDelete(tagId) + const result = await Tag.findByIdAndDelete(tagId); - res.sendStatus(200) + res.json(result); } export const getWorkspaceTags = async (req: Request, res: Response) => { diff --git a/backend/src/ee/controllers/v1/secretSnapshotController.ts b/backend/src/ee/controllers/v1/secretSnapshotController.ts index 6e8605c2f9..ae3af7958d 100644 --- a/backend/src/ee/controllers/v1/secretSnapshotController.ts +++ b/backend/src/ee/controllers/v1/secretSnapshotController.ts @@ -15,7 +15,13 @@ export const getSecretSnapshot = async (req: Request, res: Response) => { secretSnapshot = await SecretSnapshot .findById(secretSnapshotId) - .populate('secretVersions'); + .populate({ + path: 'secretVersions', + populate: { + path: 'tags', + model: 'Tag', + } + }); if (!secretSnapshot) throw new Error('Failed to find secret snapshot'); diff --git a/frontend/public/data/frequentInterfaces.ts b/frontend/public/data/frequentInterfaces.ts index c2fa797fd5..9865d9909b 100644 --- a/frontend/public/data/frequentInterfaces.ts +++ b/frontend/public/data/frequentInterfaces.ts @@ -1,3 +1,12 @@ +export interface Tag { + _id: string; + name: string; + slug: string; + user: string; + workspace: string; + createdAt: string; +} + export interface SecretDataProps { pos: number; key: string; @@ -5,4 +14,5 @@ export interface SecretDataProps { valueOverride: string | undefined; id: string; comment: string; + tags: Tag[]; } \ No newline at end of file diff --git a/frontend/src/components/basic/dialog/AddWorkspaceDialog.tsx b/frontend/src/components/basic/dialog/AddWorkspaceDialog.tsx index d9cf176451..88dc7cb78b 100644 --- a/frontend/src/components/basic/dialog/AddWorkspaceDialog.tsx +++ b/frontend/src/components/basic/dialog/AddWorkspaceDialog.tsx @@ -36,7 +36,7 @@ const AddWorkspaceDialog = ({ return (
- + -
+
- +

- This project will contain your environmental variables. + This project will contain your secrets and configs.

diff --git a/frontend/src/components/basic/table/EnvironmentsTable.tsx b/frontend/src/components/basic/table/EnvironmentsTable.tsx index 37411c366e..f4e02a3bbf 100644 --- a/frontend/src/components/basic/table/EnvironmentsTable.tsx +++ b/frontend/src/components/basic/table/EnvironmentsTable.tsx @@ -83,7 +83,7 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
+ + )})} + + + + + ); +}; + +export default AddTagsMenu; diff --git a/frontend/src/components/dashboard/DashboardInputField.tsx b/frontend/src/components/dashboard/DashboardInputField.tsx index c3f7e9a65e..5b8cf9b697 100644 --- a/frontend/src/components/dashboard/DashboardInputField.tsx +++ b/frontend/src/components/dashboard/DashboardInputField.tsx @@ -3,7 +3,6 @@ import { faCircle, faExclamationCircle, faEye, faLayerGroup } from '@fortawesome import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import guidGenerator from '../utilities/randomId'; -import { Button } from '../v2'; import { HoverObject } from '../v2/HoverCard'; const REGEX = /([$]{.*?})/g; @@ -99,8 +98,8 @@ const DashboardInputField = ({ )} {!error &&
- +
}
); diff --git a/frontend/src/components/dashboard/DeleteActionButton.tsx b/frontend/src/components/dashboard/DeleteActionButton.tsx index d0708f239f..91819b43c2 100644 --- a/frontend/src/components/dashboard/DeleteActionButton.tsx +++ b/frontend/src/components/dashboard/DeleteActionButton.tsx @@ -19,7 +19,7 @@ export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
+ : 'cursor-pointer w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'}`}> {isPlain ?
null} diff --git a/frontend/src/components/dashboard/DropZone.tsx b/frontend/src/components/dashboard/DropZone.tsx index 5b1c0cd45d..cd6d8650b1 100644 --- a/frontend/src/components/dashboard/DropZone.tsx +++ b/frontend/src/components/dashboard/DropZone.tsx @@ -64,7 +64,8 @@ const DropZone = ({ key, value: keyPairs[key as keyof typeof keyPairs].value, comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'), - type: 'shared' + type: 'shared', + tags: [] })); break; } @@ -86,7 +87,8 @@ const DropZone = ({ key, value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '', comment, - type: 'shared' + type: 'shared', + tags: [] }; }); break; diff --git a/frontend/src/components/dashboard/KeyPair.tsx b/frontend/src/components/dashboard/KeyPair.tsx index c87dd7f96d..27409a57a6 100644 --- a/frontend/src/components/dashboard/KeyPair.tsx +++ b/frontend/src/components/dashboard/KeyPair.tsx @@ -1,7 +1,8 @@ -import { faEllipsis } from '@fortawesome/free-solid-svg-icons'; +import { faEllipsis, faXmark } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { SecretDataProps } from 'public/data/frequentInterfaces'; +import { SecretDataProps, Tag } from 'public/data/frequentInterfaces'; +import AddTagsMenu from './AddTagsMenu'; import DashboardInputField from './DashboardInputField'; import { DeleteActionButton } from './DeleteActionButton'; @@ -11,12 +12,15 @@ interface KeyPairProps { modifyValue: (value: string, position: number) => void; modifyValueOverride: (value: string | undefined, position: number) => void; modifyComment: (value: string, position: number) => void; + modifyTags: (value: Tag[], position: number) => void; isBlurred: boolean; isDuplicate: boolean; toggleSidebar: (id: string) => void; sidebarSecretId: string; isSnapshot: boolean; deleteRow?: (props: DeleteRowFunctionProps) => void; + tags: Tag[]; + togglePITSidebar?: (value: boolean) => void; } export interface DeleteRowFunctionProps { @@ -24,6 +28,23 @@ export interface DeleteRowFunctionProps { secretName: string; } +const colors = [ + 'bg-[#f1c40f]/40', + 'bg-[#cb1c8d]/40', + 'bg-[#badc58]/40', + 'bg-[#ff5400]/40', + 'bg-[#00bbf9]/40' +] + + +const colorsText = [ + 'text-[#fcf0c3]/70', + 'text-[#f2c6e3]/70', + 'text-[#eef6d5]/70', + 'text-[#ffddcc]/70', + 'text-[#f0fffd]/70' +] + /** * This component represent a single row for an environemnt variable on the dashboard * @param {object} obj @@ -32,12 +53,15 @@ export interface DeleteRowFunctionProps { * @param {function} obj.modifyValue - modify the value of a certain environment variable * @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden * @param {function} obj.modifyComment - modify the comment of a certain environment variable + * @param {function} obj.modifyTags - modify the tags of a certain environment variable * @param {boolean} obj.isBlurred - if the blurring setting is turned on * @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard * @param {function} obj.toggleSidebar - open/close/switch sidebar * @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed * @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar * @param {function} obj.deleteRow - a function to delete a certain keyPair + * @param {function} obj.togglePITSidebar - open or close the Point-in-time recovery sidebar + * @param {Tag[]} obj.tags - tags for a certain secret * @returns */ const KeyPair = ({ @@ -46,21 +70,31 @@ const KeyPair = ({ modifyValue, modifyValueOverride, modifyComment, + modifyTags, isBlurred, isDuplicate, toggleSidebar, sidebarSecretId, isSnapshot, - deleteRow -}: KeyPairProps) => ( + deleteRow, + togglePITSidebar, + tags +}: KeyPairProps) => { + const tagData = (tags.map((tag, index) => {return { + ...tag, + color: colors[index%colors.length], + colorText: colorsText[index%colorsText.length] + }})); + + return (
-
{keyPair.pos + 1}
-
+
+
{keyPair.pos + 1}
-
+
@@ -89,7 +123,7 @@ const KeyPair = ({ />
-
+
- {!isSnapshot && ( -
null} - role="button" - tabIndex={0} - onClick={() => toggleSidebar(keyPair.id)} - className="cursor-pointer w-[2.35rem] h-[2.35rem] px-6 rounded-md invisible group-hover:visible flex flex-row justify-center items-center" - > - +
+
+ {keyPair.tags.map((tag, index) => ( + index < 2 &&
tagDp._id === tag._id)[0]?.color} rounded-sm text-sm ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.colorText} flex items-center`}> + {tag.name} + modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.pos)}/> +
+ ))} + +
- )} - {!isSnapshot && ( +
+
null} + role="button" + tabIndex={0} + onClick={() => { + if (togglePITSidebar) { + togglePITSidebar(false); + } + toggleSidebar(keyPair.id) + }} + className={`cursor-pointer w-[1.5rem] h-[2.35rem] ml-auto group-hover:bg-mineshaft-700 z-50 rounded-md invisible group-hover:visible flex flex-row justify-center items-center ${isSnapshot ?? 'invisible'}`} + > + +
+
{ if (deleteRow) { deleteRow({ ids: [keyPair.id], secretName: keyPair?.key }) }}} isPlain /> - )} +
-); +)}; export default KeyPair; diff --git a/frontend/src/components/dashboard/SideBar.tsx b/frontend/src/components/dashboard/SideBar.tsx index a40dc16344..1d0996376f 100644 --- a/frontend/src/components/dashboard/SideBar.tsx +++ b/frontend/src/components/dashboard/SideBar.tsx @@ -80,7 +80,7 @@ const SideBar = ({ const { t } = useTranslation(); return ( -
+
{isLoading ? (
secret.key === key && secret.type === 'shared' - )[0]?.comment + )[0]?.comment, + tags: tempDecryptedSecrets.filter( + (secret) => secret.key === key && secret.type === 'shared' + )[0]?.tags })); if (typeof setData === 'function') { diff --git a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx index 7678af3cfc..1d072cc89c 100644 --- a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx +++ b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx @@ -52,7 +52,7 @@ export const DeleteActionModal = ({ >
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx new file mode 100644 index 0000000000..6aa4d26be8 --- /dev/null +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx @@ -0,0 +1,186 @@ +import { Controller, useForm } from 'react-hook-form'; +import { faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; + +import { + Button, + DeleteActionModal, + FormControl, + IconButton, + Input, + Modal, + ModalClose, + ModalContent, + ModalTrigger, + Table, + TableContainer, + TBody, + Td, + Th, + THead, + Tr, +} from '@app/components/v2'; +import { usePopUp } from '@app/hooks'; +import { WorkspaceTag } from '@app/hooks/api/types'; + +const createTagSchema = yup.object({ + name: yup.string().required().label('Tag Name'), +}); + +export type CreateWsTag = yup.InferType; + +type Props = { + tags: WorkspaceTag[]; + workspaceName: string; + onDeleteTag: (tagID: string) => Promise; + onCreateTag: (data: CreateWsTag) => Promise; +}; + +type DeleteModalData = { name: string; id: string }; + +export const SecretTagsSection = ({ + tags = [], + onDeleteTag, + workspaceName, + onCreateTag +}: Props): JSX.Element => { + const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([ + 'CreateSecretTag', + 'deleteTagConfirmation' + ] as const); + + const { + control, + reset, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(createTagSchema) + }); + + const onFormSubmit = async (data: CreateWsTag) => { + console.log(19191, data); + await onCreateTag(data); + handlePopUpClose('CreateSecretTag'); + }; + + const onDeleteApproved = async () => { + await onDeleteTag((popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id); + handlePopUpClose('deleteTagConfirmation'); + }; + + return ( +
+
+
+

Secret Tags

+

Every secret can be assigned to one or more tags. Here you can add and remove tags for the current project.

+
+
+ { + handlePopUpToggle('CreateSecretTag', open); + reset(); + }} + > + + + + +
+ ( + + + + )} + /> +
+ + + + +
+ +
+
+
+
+ + + + + + + + + + {tags?.length > 0 ? ( + tags.map(({ _id, name, slug }) => ( + + + + + + )) + ) : ( + + + + )} + +
TagSlug +
{name}{slug} + + handlePopUpOpen('deleteTagConfirmation', { + name, + id: _id + }) + } + colorSchema="danger" + ariaLabel="update" + > + + +
+ No tags found for this project +
+
+ handlePopUpToggle('deleteTagConfirmation', isOpen)} + deleteKey={(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name} + onClose={() => handlePopUpClose('deleteTagConfirmation')} + onDeleteApproved={onDeleteApproved} + /> +
+ ); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/index.tsx new file mode 100644 index 0000000000..0d06152931 --- /dev/null +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/index.tsx @@ -0,0 +1 @@ +export {SecretTagsSection} from './SecretTagsSection' \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx index 1634832780..98160b5b7c 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx @@ -2,5 +2,6 @@ export { CopyProjectIDSection } from './CopyProjectIDSection'; export { EnvironmentSection } from './EnvironmentSection'; export type { CreateUpdateEnvFormData } from './EnvironmentSection/EnvironmentSection'; export { ProjectNameChangeSection } from './ProjectNameChangeSection'; +export type { CreateWsTag } from './SecretTagsSection/SecretTagsSection'; export { ServiceTokenSection } from './ServiceTokenSection'; export type { CreateServiceToken } from './ServiceTokenSection/ServiceTokenSection';