From 66ea3ba17267d38b788e267c4dbe394d4e214145 Mon Sep 17 00:00:00 2001 From: Ebezer Igbinoba Date: Sun, 20 Aug 2023 10:02:40 +0100 Subject: [PATCH 01/80] feat: added custom design for tags --- frontend/src/components/v2/Tag/Tag.tsx | 6 +- frontend/src/hooks/api/tags/types.ts | 15 +- .../DesignTagModal/DesignTagModal.tsx | 138 ++++++++++++++++++ .../components/DesignTagModal/index.tsx | 1 + .../SecretInputRow/SecretInputRow.tsx | 78 ++++++++-- 5 files changed, 225 insertions(+), 13 deletions(-) create mode 100644 frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx create mode 100644 frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx diff --git a/frontend/src/components/v2/Tag/Tag.tsx b/frontend/src/components/v2/Tag/Tag.tsx index 13672e7e9..cbb833b30 100644 --- a/frontend/src/components/v2/Tag/Tag.tsx +++ b/frontend/src/components/v2/Tag/Tag.tsx @@ -9,6 +9,7 @@ type Props = { className?: string; onClose?: () => void; color?: string; + styles?: Record isDisabled?: boolean; } & VariantProps; @@ -35,11 +36,12 @@ export const Tag = ({ color, isDisabled, size = "sm", - onClose + onClose, + styles = {} }: Props) => (
{children} {onClose && ( diff --git a/frontend/src/hooks/api/tags/types.ts b/frontend/src/hooks/api/tags/types.ts index 56b7171bf..87486ee03 100644 --- a/frontend/src/hooks/api/tags/types.ts +++ b/frontend/src/hooks/api/tags/types.ts @@ -36,4 +36,17 @@ export type DeleteWsTagRes = { createdAt: string; user: string; _id: string; -}; \ No newline at end of file +}; + +export type TagDesign = { + tagBackground: string; + tagLabel: string +} + +export type SecretTags = { + id: string; + _id: string; + slug: string; + tagBackground: string; + tagLabel: string +} \ No newline at end of file diff --git a/frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx b/frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx new file mode 100644 index 000000000..6993471e9 --- /dev/null +++ b/frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx @@ -0,0 +1,138 @@ +import { Controller, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { Button, FormControl, Input, ModalClose, Tooltip, IconButton, Tag } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { TagDesign } from "~/hooks/api/tags/types"; + +import { + faEye, + faEyeSlash +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useState } from 'react'; +import { WsTag } from '../../../../hooks/api/tags/types'; + +type TagData = { + tagBackground: string; + tagLabel: string +} + +type Props = { + onDesignTag: (tagData: TagData) => void; + selectedTag: WsTag +}; + +const designTagSchema = yup.object({ + tagBackground: yup.string().required().trim().label("Tag Background"), + tagLabel: yup.string().required().trim().label("Tag Label"), +}); +type FormData = yup.InferType; + + +export const DesignTagModal = ({ onDesignTag, selectedTag }: Props): JSX.Element => { + const [tagDesignObj, setTagDesignObj] = useState({ + tagColor: { + bg: "", + text: "" + } + }) + + const { + control, + reset, + formState, + handleSubmit, + setValue + } = useForm({ + resolver: yupResolver(designTagSchema) + }); + + const onFormSubmit = ({ tagBackground, tagLabel }: FormData) => { + onDesignTag({ tagBackground, tagLabel }); + reset(); + }; + + const [previewTag, setPreviewTag] = useToggle(false); + + const handleInputChange = (e: React.ChangeEvent, type: string) => { + setTagDesignObj((prev: { tagColor: { bg: string, text: string }; }) => ({ + tagColor: { + ...prev.tagColor, + [type]: e.target.value + } + })) + if (type === 'bg') { + setValue('tagBackground', e.target.value) + } else { + setValue('tagLabel', e.target.value) + } + } + + return ( +
+
+ { + return ( + <> + + ) => handleInputChange(e, 'bg')} /> + + + ) + }} + /> + {/* */} +
+ setPreviewTag.toggle()} className="absolute top-[2px] left-[127px] cursor-pointer" /> + {previewTag && ( + void (0)} + key={selectedTag._id} + className="absolute top-[-5px] right-[-5px] cursor-pointer" + > + {selectedTag.slug} + + )} + +
+ {/*
*/} +
+ + { + return ( + <> + + ) => handleInputChange(e, 'text')} value={tagDesignObj.tagColor.text} /> + + + ) + }} + /> +
+ + + + +
+ + ); +}; diff --git a/frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx b/frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx new file mode 100644 index 000000000..165e8e3a3 --- /dev/null +++ b/frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx @@ -0,0 +1 @@ +export {DesignTagModal} from "./DesignTagModal" \ No newline at end of file diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index 65e5436b7..f58360dcb 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -38,12 +38,17 @@ import { SecretInput, Tag, TextArea, - Tooltip + Tooltip, + Modal, + ModalContent, } from "@app/components/v2"; -import { useToggle } from "@app/hooks"; + import { WsTag } from "@app/hooks/api/types"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; +import { SecretTags, TagDesign } from "~/hooks/api/tags/types"; +import { DesignTagModal } from "../../components/DesignTagModal"; +import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks"; const tagColors = [ { bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" }, @@ -73,6 +78,7 @@ type Props = { // tag props wsTags?: WsTag[]; onCreateTagOpen: () => void; + onDesignTagOpen: (selectedTag: WsTag, selectedFieldIndex: number) => void; // rhf specific functions, dont put this using useFormContext. This is passed as props to avoid re-rendering control: Control; register: UseFormRegister; @@ -80,6 +86,9 @@ type Props = { isKeyError?: boolean; keyError?: string; autoCapitalization?: boolean; + designObj: TagDesign & WsTag; + updateDesign: boolean; + selectedFieldIndex: number }; export const SecretInputRow = memo( @@ -92,6 +101,8 @@ export const SecretInputRow = memo( isAddOnly, wsTags, onCreateTagOpen, + onDesignTagOpen, + designObj, onSecretDelete, searchTerm, control, @@ -100,7 +111,9 @@ export const SecretInputRow = memo( isKeyError, keyError, secUniqId, - autoCapitalization + autoCapitalization, + updateDesign, + selectedFieldIndex }: Props): JSX.Element => { const isKeySubDisabled = useRef(false); // comment management in a row @@ -113,7 +126,7 @@ export const SecretInputRow = memo( const tagColorByTagId = new Map((wsTags || []).map((wsTag, i) => [wsTag._id, tagColors[i % tagColors.length]])) // display the tags in alphabetical order - secretTags.sort((a, b) => a.name.localeCompare(b.name)) + secretTags.sort((a, b) => a?.name?.localeCompare(b?.name)) // to get details on a secret const overrideAction = useWatch({ @@ -145,7 +158,10 @@ export const SecretInputRow = memo( // when secret is override by personal values const isOverridden = overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; + const [editorRef, setEditorRef] = useState(isOverridden ? secValueOverride : secValue); + const [tagDesignObj, setTagDesignObj] = useState({}) + const [selectedTag, setSelectedTag] = useState({}) const secId = useWatch({ control, name: `secrets.${index}._id`, exact: true }); const tags = @@ -174,6 +190,20 @@ export const SecretInputRow = memo( setInviteLinkCopied.on(); }; + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "secretDetails", + "addTag", + "secretSnapshots", + "uploadedSecOpts", + "compareSecrets", + "folderForm", + "deleteFolder", + "upgradePlan", + "addSecretImport", + "deleteSecretImport", + "designTag" + ] as const); + const onSecretOverride = () => { if (isOverridden) { // when user created a new override but then removes @@ -193,14 +223,22 @@ export const SecretInputRow = memo( }; const onSelectTag = (selectedTag: WsTag) => { + const checkBoxSelected = !selectedTagIds[selectedTag.slug] + checkBoxSelected && handlePopUpOpen('designTag') + setSelectedTag(selectedTag) + }; + + const onDesignWsTag = (_tagDesignObj: TagDesign) => { + setTagDesignObj(() => (_tagDesignObj)) + handlePopUpClose("designTag"); const shouldAppend = !selectedTagIds[selectedTag.slug]; if (shouldAppend) { - append(selectedTag); + append({...selectedTag, ..._tagDesignObj}); } else { - const pos = tags.findIndex(({ slug }) => selectedTag.slug === slug); + const pos = tags.findIndex(({ slug }: {slug: string}) => selectedTag.slug === slug); remove(pos); } - }; + } const isCreatedSecret = !secId; const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly; @@ -223,11 +261,28 @@ export const SecretInputRow = memo( return <>; } + + return (
{index + 1}
+ {/* Add a custom design to new tag to make visible */} + { + handlePopUpToggle("designTag", open); + }} + > + + + + +
- {secretTags.map(({ id, _id, slug }, i) => { + {secretTags.map(({ id, _id, slug, tagBackground, tagLabel }: SecretTags, i: number) => { // This map lookup shouldn't ever fail, but if it does we default to the first color const tagColor = tagColorByTagId.get(_id) || tagColors[0] return ( @@ -335,6 +390,10 @@ export const SecretInputRow = memo( tagColor.bg, tagColor.text )} + styles={{ + backgroundColor: tagBackground, + color: tagLabel + }} isDisabled={isReadOnly || isAddOnly || isRollbackMode} onClose={() => remove(i)} key={id} @@ -395,9 +454,8 @@ export const SecretInputRow = memo( className="mr-0 data-[state=checked]:bg-primary" id="autoCapitalization" isChecked={selectedTagIds?.[wsTag.slug]} - onCheckedChange={() => {}} > - {} + { } } key={wsTag._id} From 534d96ffb67c090b79934ac59b3820a7049fae73 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 14:05:00 +1000 Subject: [PATCH 02/80] Set max password length (100 chars) to help prevent DDOS attack --- frontend/public/locales/en/translations.json | 9 ++--- frontend/public/locales/fr/translations.json | 9 ++--- .../src/components/signup/UserInfoStep.tsx | 3 +- .../utilities/checks/PasswordCheck.ts | 17 ++++++--- .../utilities/checks/checkPassword.ts | 30 +++++++++------- frontend/src/pages/password-reset.tsx | 35 +++++++++++++------ .../ChangePasswordSection.tsx | 3 +- 7 files changed, 69 insertions(+), 37 deletions(-) diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index a70a7dd8b..506495b3f 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -231,10 +231,11 @@ "current": "Current password", "current-wrong": "The current password may be wrong", "new": "New password", - "validate-base": "Password should contain at least:", - "validate-length": "14 characters", - "validate-case": "1 lowercase character", - "validate-number": "1 number" + "validate-base": "Password should contain:", + "validate-too-short": "at least 14 characters", + "validate-too-long": "at most 100 characters", + "validate-case": "at least 1 lowercase character", + "validate-number": "at least 1 number" }, "token": { "service-tokens": "Service Tokens", diff --git a/frontend/public/locales/fr/translations.json b/frontend/public/locales/fr/translations.json index 6914e7ea1..49edd33fa 100644 --- a/frontend/public/locales/fr/translations.json +++ b/frontend/public/locales/fr/translations.json @@ -215,10 +215,11 @@ "current": "Mot de passe actuel", "current-wrong": "Le mot de passe actuel peut être érroné", "new": "Nouveau mot de passe", - "validate-base": "Le mot de passe doit contenir au moins:", - "validate-length": "14 caractères", - "validate-case": "1 caractère miniscule", - "validate-number": "1 chiffre" + "validate-base": "Le mot de passe doit contenir:", + "validate-too-short": "au moins 14 caractères", + "validate-too-long": "au maximum 100 caractères", + "validate-case": "au moins 1 caractère miniscule", + "validate-number": "au moins 1 chiffre" }, "token": { "service-tokens": "Jetons de service", diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index db4d040ad..3123950c3 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -39,7 +39,8 @@ interface UserInfoStepProps { } type Errors = { - length?: string, + tooShort?: string, + tooLong?: string, upperCase?: string, lowerCase?: string, number?: string, diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts index 5fb9dfe2c..cd75b53f9 100644 --- a/frontend/src/components/utilities/checks/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/PasswordCheck.ts @@ -2,7 +2,8 @@ interface PasswordCheckProps { password: string; errorCheck: boolean; - setPasswordErrorLength: (value: boolean) => void; + setPasswordErrorTooShort: (value: boolean) => void; + setPasswordErrorTooLong: (value: boolean) => void; setPasswordErrorNumber: (value: boolean) => void; setPasswordErrorLowerCase: (value: boolean) => void; } @@ -12,17 +13,25 @@ interface PasswordCheckProps { */ const passwordCheck = ({ password, - setPasswordErrorLength, + setPasswordErrorTooShort, setPasswordErrorNumber, setPasswordErrorLowerCase, + setPasswordErrorTooLong, errorCheck }: PasswordCheckProps) => { if (!password || password.length < 14) { - setPasswordErrorLength(true); + setPasswordErrorTooShort(true); errorCheck = true; } else { - setPasswordErrorLength(false); + setPasswordErrorTooShort(false); + } + + if (password.length > 100) { + setPasswordErrorTooLong(true); + errorCheck = true; + } else { + setPasswordErrorTooLong(false); } if (!/\d/.test(password)) { diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 69dba2397..7f8b5d45f 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -1,5 +1,6 @@ type Errors = { - length?: string, + tooShort?: string, + tooLong?: string, upperCase?: string, lowerCase?: string, number?: string, @@ -15,11 +16,12 @@ interface CheckPasswordParams { } /** - * Validate that the password [password] is at least: - * - 8 characters long - * - Contains 1 uppercase character (A-Z) - * - Contains 1 lowercase character (a-z) - * - Contains 1 number (0-9) + * Validate that the password [password]: + * - Contains at least 14 characters long + * - Contains at most 100 characters long + * - Contains at least 1 uppercase character (A-Z) + * - Contains at least 1 lowercase character (a-z) + * - Contains at least 1 number (0-9) * - Does not contain 3 repeat, consecutive characters * * The function returns whether or not the password [password] @@ -37,24 +39,28 @@ const checkPassword = ({ }: CheckPasswordParams): boolean => { const errors: Errors = {}; - if (password.length < 8) { - errors.length = "8 characters"; + if (password.length < 14) { + errors.tooShort = "at least 14 characters"; + } + + if (password.length > 100) { + errors.tooLong = "at most 100 characters"; } if (!/[A-Z]/.test(password)) { - errors.upperCase = "1 uppercase character (A-Z)"; + errors.upperCase = "at least 1 uppercase character (A-Z)"; } if (!/[a-z]/.test(password)) { - errors.lowerCase = "1 lowercase character (a-z)"; + errors.lowerCase = "at least 1 lowercase character (a-z)"; } if (!/[0-9]/.test(password)) { - errors.number = "1 number (0-9)"; + errors.number = "at least 1 number (0-9)"; } if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { - errors.specialChar = "1 special character (!@#$%^&*(),.?)"; + errors.specialChar = "at least 1 special character (!@#$%^&*(),.?)"; } if (/([A-Za-z0-9])\1\1\1/.test(password)) { diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx index b38f67c01..90fd6be51 100644 --- a/frontend/src/pages/password-reset.tsx +++ b/frontend/src/pages/password-reset.tsx @@ -28,7 +28,8 @@ export default function PasswordReset() { const [privateKey, setPrivateKey] = useState(""); const [newPassword, setNewPassword] = useState(""); const [backupKeyError, setBackupKeyError] = useState(false); - const [passwordErrorLength, setPasswordErrorLength] = useState(false); + const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(false); + const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false); const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); @@ -67,7 +68,8 @@ export default function PasswordReset() { e.preventDefault(); const errorCheck = passwordCheck({ password: newPassword, - setPasswordErrorLength, + setPasswordErrorTooShort, + setPasswordErrorTooLong, setPasswordErrorNumber, setPasswordErrorLowerCase, errorCheck: false @@ -221,7 +223,8 @@ export default function PasswordReset() { setNewPassword(password); passwordCheck({ password, - setPasswordErrorLength, + setPasswordErrorTooShort, + setPasswordErrorTooLong, setPasswordErrorNumber, setPasswordErrorLowerCase, errorCheck: false @@ -230,22 +233,32 @@ export default function PasswordReset() { type="password" value={newPassword} isRequired - error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber} + error={passwordErrorTooShort && passwordErrorTooLong && passwordErrorLowerCase && passwordErrorNumber} autoComplete="new-password" id="new-password" />
- {passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( + {passwordErrorTooShort || passwordErrorTooLong || passwordErrorLowerCase || passwordErrorNumber ? (
-
Password should contain at least:
+
Password should contain:
- {passwordErrorLength ? ( + {passwordErrorTooShort ? ( ) : ( )} -
- 14 characters +
+ at least 14 characters +
+
+
+ {passwordErrorTooLong ? ( + + ) : ( + + )} +
+ at most 100 characters
@@ -257,7 +270,7 @@ export default function PasswordReset() {
- 1 lowercase character + at least 1 lowercase character
@@ -267,7 +280,7 @@ export default function PasswordReset() { )}
- 1 number + at least 1 number
diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index fefa7516e..c873395c4 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -18,7 +18,8 @@ import { useUser } from "@app/context"; import { useGetCommonPasswords } from "@app/hooks/api"; type Errors = { - length?: string, + tooShort?: string, + tooLong?: string, upperCase?: string, lowerCase?: string, number?: string, From 9a1b453c863959facd7f31652b71c72c381fbb81 Mon Sep 17 00:00:00 2001 From: Ebezer Igbinoba Date: Tue, 22 Aug 2023 05:12:23 +0100 Subject: [PATCH 03/80] Feat: added tag color widgt and changed tag popover design --- backend/spec.json | 3 + backend/src/controllers/v2/tagController.ts | 3 +- backend/src/models/tag.ts | 6 + backend/src/routes/v2/tags.ts | 1 + docs/spec.yaml | 2 + .../components/utilities/isValidHexColor.ts | 5 + frontend/src/components/v2/Tag/Tag.tsx | 14 +- frontend/src/const.ts | 66 +++++++ frontend/src/hooks/api/tags/queries.tsx | 13 +- frontend/src/hooks/api/tags/types.ts | 23 ++- frontend/src/styles/globals.css | 25 +++ .../src/views/DashboardPage/DashboardPage.tsx | 3 +- .../CreateTagModal/CreateTagModal.tsx | 176 +++++++++++++++--- .../SecretInputRow/SecretInputRow.tsx | 163 ++++++++-------- 14 files changed, 368 insertions(+), 135 deletions(-) create mode 100644 frontend/src/components/utilities/isValidHexColor.ts diff --git a/backend/spec.json b/backend/spec.json index 013b5fd4d..1afbc8dce 100644 --- a/backend/spec.json +++ b/backend/spec.json @@ -3203,6 +3203,9 @@ "name": { "example": "any" }, + "tagColor": { + "example": "any" + }, "slug": { "example": "any" } diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts index 0d945c3e5..fa21fc971 100644 --- a/backend/src/controllers/v2/tagController.ts +++ b/backend/src/controllers/v2/tagController.ts @@ -6,10 +6,11 @@ import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; export const createWorkspaceTag = async (req: Request, res: Response) => { const { workspaceId } = req.params; - const { name, slug } = req.body; + const { name, slug, tagColor } = req.body; const tagToCreate = { name, + tagColor, workspace: new Types.ObjectId(workspaceId), slug, user: new Types.ObjectId(req.user._id), diff --git a/backend/src/models/tag.ts b/backend/src/models/tag.ts index 53bf085d3..ec649e254 100644 --- a/backend/src/models/tag.ts +++ b/backend/src/models/tag.ts @@ -3,6 +3,7 @@ import { Schema, Types, model } from "mongoose"; export interface ITag { _id: Types.ObjectId; name: string; + tagColor: string; slug: string; user: Types.ObjectId; workspace: Types.ObjectId; @@ -15,6 +16,11 @@ const tagSchema = new Schema( required: true, trim: true, }, + tagColor: { + type: String, + required: false, + trim: true, + }, slug: { type: String, required: true, diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts index 7ccfd17cd..aca1b6d8c 100644 --- a/backend/src/routes/v2/tags.ts +++ b/backend/src/routes/v2/tags.ts @@ -48,6 +48,7 @@ router.post( }), param("workspaceId").exists().trim(), body("name").exists().trim(), + body("tagColor").exists().trim(), body("slug").exists().trim(), validateRequest, tagController.createWorkspaceTag diff --git a/docs/spec.yaml b/docs/spec.yaml index 799c7f6f4..5cf129be3 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -1949,6 +1949,8 @@ paths: properties: name: example: any + tagColor: + example: any slug: example: any /api/v2/workspace/tags/{tagId}: diff --git a/frontend/src/components/utilities/isValidHexColor.ts b/frontend/src/components/utilities/isValidHexColor.ts new file mode 100644 index 000000000..86c14b142 --- /dev/null +++ b/frontend/src/components/utilities/isValidHexColor.ts @@ -0,0 +1,5 @@ +export const isValidHexColor = (hexColor: string) => { + const hexColorPattern = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/; + + return hexColorPattern.test(hexColor); +} \ No newline at end of file diff --git a/frontend/src/components/v2/Tag/Tag.tsx b/frontend/src/components/v2/Tag/Tag.tsx index cbb833b30..a2de200ef 100644 --- a/frontend/src/components/v2/Tag/Tag.tsx +++ b/frontend/src/components/v2/Tag/Tag.tsx @@ -11,10 +11,11 @@ type Props = { color?: string; styles?: Record isDisabled?: boolean; + tagColor: string; } & VariantProps; const tagVariants = cva( - "inline-flex items-center whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200", + "inline-flex items-center whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200 rounded-[30px] text-gray-400 ", { variants: { colorSchema: { @@ -41,18 +42,7 @@ export const Tag = ({ }: Props) => (
{children} - {onClose && ( - - )}
); diff --git a/frontend/src/const.ts b/frontend/src/const.ts index f2309df31..7f24f8fc5 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -51,3 +51,69 @@ const plansProd: Mapping = { export const plans = plansProd || plansDev; export const leaveConfirmDefaultMessage = "Your changes will be lost if you leave the page. Are you sure you want to continue?"; + +export const secretTagsColors = [ + { + id: 1, + hex: '#bec2c8', + rgba: "rgb(128,128,128, 0.8)", + name: 'Grey', + selected: true + }, + { + id: 2, + hex: '#95a2b3', + rgba: "rgb(0,0,255, 0.8)", + name: 'blue', + selected: false + }, + { + id: 3, + hex: '#5e6ad2', + rgba: "rgb(128,0,128, 0.8)", + name: 'Purple', + selected: false + }, + { + id: 4, + hex: '#26b5ce', + rgba: "rgb(0,128,128, 0.8)", + name: 'Teal', + selected: false + }, + { + id: 5, + hex: '#4cb782', + rgba: "rgb(0,128,0, 0.8)", + name: 'Green', + selected: false + }, + { + id: 6, + hex: '#f2c94c', + rgba: "rgb(255,255,0, 0.8)", + name: 'Yellow', + selected: false + }, + { + id: 7, + hex: '#f2994a', + rgba: "rgb(128,128,0, 0.8)", + name: 'Orange', + selected: false + }, + { + id: 8, + hex: '#f7c8c1', + rgba: "rgb(128,0,0, 0.8)", + name: 'Pink', + selected: false + }, + { + id: 9, + hex: '#eb5757', + rgba: "rgb(255,0,0, 0.8)", + name: 'Red', + selected: false + }, +] \ No newline at end of file diff --git a/frontend/src/hooks/api/tags/queries.tsx b/frontend/src/hooks/api/tags/queries.tsx index 74900da0e..b0216828c 100644 --- a/frontend/src/hooks/api/tags/queries.tsx +++ b/frontend/src/hooks/api/tags/queries.tsx @@ -7,7 +7,9 @@ import { CreateTagRes, DeleteTagDTO, DeleteWsTagRes, - UserWsTags + QueryTag, + UserWsTags, + WsTag } from "./types"; const workspaceTags = { @@ -34,14 +36,15 @@ export const useCreateWsTag = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ workspaceID, tagName, tagSlug }) => { + mutationFn: async ({ workspaceID, tagName, tagColor, tagSlug }: QueryTag) => { const { data } = await apiRequest.post(`/api/v2/workspace/${workspaceID}/tags`, { name: tagName, + tagColor: tagColor, slug: tagSlug }) return data; }, - onSuccess: (tagData) => { + onSuccess: (tagData: WsTag) => { queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace)); } }); @@ -51,11 +54,11 @@ export const useDeleteWsTag = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ tagID }) => { + mutationFn: async ({ tagID }: {tagID: string}) => { const { data } = await apiRequest.delete(`/api/v2/workspace/tags/${tagID}`); return data }, - onSuccess: (tagData) => { + onSuccess: (tagData: WsTag) => { queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace)); } }); diff --git a/frontend/src/hooks/api/tags/types.ts b/frontend/src/hooks/api/tags/types.ts index 87486ee03..db162415e 100644 --- a/frontend/src/hooks/api/tags/types.ts +++ b/frontend/src/hooks/api/tags/types.ts @@ -38,15 +38,24 @@ export type DeleteWsTagRes = { _id: string; }; -export type TagDesign = { - tagBackground: string; - tagLabel: string -} - export type SecretTags = { id: string; _id: string; slug: string; - tagBackground: string; - tagLabel: string + tagColor: string; +} + +export type TagColor = { + id: number; + hex: string + rgba: string + name: string + selected: boolean +} + +export type QueryTag = { + workspaceID: string; + tagName: string; + tagColor: string; + tagSlug: string } \ No newline at end of file diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index ae8d17427..6d5070fe0 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -107,6 +107,31 @@ @apply bg-primary-400; } } +.tags-conic-bg { + background: conic-gradient(rgb(235, 87, 87), rgb(242, 201, 76), rgb(76, 183, 130), rgb(78, 167, 252), rgb(250, 96, 122)); +} + +.show-tags { + transform: translateY(10px); + transition: all 0.2s; + opacity: 1; +} +.hide-tags { + transform: translateY(-20px); + transition: all 0.2s; + opacity: 0; +} + +.show-hex-input { + transform: translateY(-33px); + transition: all 0.2s; + opacity: 1; +} +.hide-hex-input { + transform: translateY(20px); + transition: all 0.2s; + opacity: 0; +} @import "@fontsource/inter/400.css"; @import "@fontsource/inter/500.css"; diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index b83cb839d..4c84aab84 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -513,11 +513,12 @@ export const DashboardPage = () => { }, []); const onCreateWsTag = useCallback( - async (tagName: string) => { + async (tagName: string, tagColor: string) => { try { await createWsTag({ workspaceID: workspaceId, tagName, + tagColor, tagSlug: tagName.replace(" ", "_") }); handlePopUpClose("addTag"); diff --git a/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx index 22973c1a6..c378bf54c 100644 --- a/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx +++ b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx @@ -1,11 +1,21 @@ +import { useState, useEffect } from 'react'; import { Controller, useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; +import { secretTagsColors } from "~/const" +import { + faCheck +} from "@fortawesome/free-solid-svg-icons"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Button, FormControl, Input, ModalClose, Tooltip } from "@app/components/v2"; +import { isValidHexColor } from "~/components/utilities/isValidHexColor"; +import { TagColor } from '~/hooks/api/tags/types'; -import { Button, FormControl, Input, ModalClose } from "@app/components/v2"; type Props = { - onCreateTag: (tagName: string) => Promise; + onCreateTag: (tagName: string, tagColor: string) => Promise; }; const createTagSchema = yup.object({ @@ -23,33 +33,151 @@ export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => { resolver: yupResolver(createTagSchema) }); + const [tagsColors, setTagsColors] = useState(secretTagsColors) + const [selectedTagColor, setSelectedTagColor] = useState({}) + const [showHexInput, setShowHexInput] = useState(false) + const [tagColor, setTagColor] = useState("") + + const onFormSubmit = async ({ name }: FormData) => { - await onCreateTag(name); + await onCreateTag(name, tagColor); reset(); }; + useEffect(() => { + const clonedTagColors = [...tagsColors] + for (const tagColor of clonedTagColors) { + if (tagColor.selected) { + setSelectedTagColor(tagColor) + setTagColor(tagColor.hex) + break + } + } + }, []) + + useEffect(() => { + const tagsList = document.querySelector(".secret-tags-wrapper") + const tagsHexWrapper = document.querySelector(".tags-hex-wrapper") + + if (showHexInput) { + tagsList?.classList.add('hide-tags') + tagsList?.classList.remove('show-tags') + tagsHexWrapper?.classList.add('show-hex-input') + tagsHexWrapper?.classList.remove('hide-hex-input') + } else { + tagsList?.classList.remove('hide-tags') + tagsList?.classList.add('show-tags') + tagsHexWrapper?.classList.remove('show-hex-input') + tagsHexWrapper?.classList.add('hide-hex-input') + } + }, [showHexInput]) + + const handleColorChange = (tagColor: TagColor) => { + const clonedTagColors = [...tagsColors] + const tagColorIndex = clonedTagColors.findIndex(_tagColor => _tagColor.id === tagColor.id) + const _selectedTagColor = clonedTagColors[tagColorIndex] + clonedTagColors.forEach(tagColor => { + tagColor.selected = false + }) + if (selectedTagColor.id !== tagColor.id) { + _selectedTagColor.selected = !_selectedTagColor.selected + setSelectedTagColor(_selectedTagColor) + setTagColor(_selectedTagColor.hex) + } + } + return ( -
- ( - - - - )} - /> -
- - - - -
- + + + +
+ + ); }; diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index f58360dcb..9b70b936e 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -46,9 +46,8 @@ import { import { WsTag } from "@app/hooks/api/types"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; -import { SecretTags, TagDesign } from "~/hooks/api/tags/types"; -import { DesignTagModal } from "../../components/DesignTagModal"; -import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks"; +import { SecretTags } from "~/hooks/api/tags/types"; +import { useToggle } from "@app/hooks"; const tagColors = [ { bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" }, @@ -86,9 +85,6 @@ type Props = { isKeyError?: boolean; keyError?: string; autoCapitalization?: boolean; - designObj: TagDesign & WsTag; - updateDesign: boolean; - selectedFieldIndex: number }; export const SecretInputRow = memo( @@ -102,7 +98,6 @@ export const SecretInputRow = memo( wsTags, onCreateTagOpen, onDesignTagOpen, - designObj, onSecretDelete, searchTerm, control, @@ -112,8 +107,6 @@ export const SecretInputRow = memo( keyError, secUniqId, autoCapitalization, - updateDesign, - selectedFieldIndex }: Props): JSX.Element => { const isKeySubDisabled = useRef(false); // comment management in a row @@ -160,12 +153,10 @@ export const SecretInputRow = memo( overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; const [editorRef, setEditorRef] = useState(isOverridden ? secValueOverride : secValue); - const [tagDesignObj, setTagDesignObj] = useState({}) - const [selectedTag, setSelectedTag] = useState({}) const secId = useWatch({ control, name: `secrets.${index}._id`, exact: true }); - const tags = - useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || []; + const tags = useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || []; + const selectedTagIds = tags.reduce>( (prev, curr) => ({ ...prev, [curr.slug]: true }), {} @@ -190,20 +181,6 @@ export const SecretInputRow = memo( setInviteLinkCopied.on(); }; - const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ - "secretDetails", - "addTag", - "secretSnapshots", - "uploadedSecOpts", - "compareSecrets", - "folderForm", - "deleteFolder", - "upgradePlan", - "addSecretImport", - "deleteSecretImport", - "designTag" - ] as const); - const onSecretOverride = () => { if (isOverridden) { // when user created a new override but then removes @@ -223,23 +200,14 @@ export const SecretInputRow = memo( }; const onSelectTag = (selectedTag: WsTag) => { - const checkBoxSelected = !selectedTagIds[selectedTag.slug] - checkBoxSelected && handlePopUpOpen('designTag') - setSelectedTag(selectedTag) - }; - - const onDesignWsTag = (_tagDesignObj: TagDesign) => { - setTagDesignObj(() => (_tagDesignObj)) - handlePopUpClose("designTag"); const shouldAppend = !selectedTagIds[selectedTag.slug]; if (shouldAppend) { - append({...selectedTag, ..._tagDesignObj}); + append(selectedTag); } else { - const pos = tags.findIndex(({ slug }: {slug: string}) => selectedTag.slug === slug); + const pos = tags.findIndex(({ slug }: { slug: string }) => selectedTag.slug === slug); remove(pos); } - } - + }; const isCreatedSecret = !secId; const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly; @@ -261,27 +229,13 @@ export const SecretInputRow = memo( return <>; } - + return (
{index + 1}
- {/* Add a custom design to new tag to make visible */} - { - handlePopUpToggle("designTag", open); - }} - > - - - -
- {secretTags.map(({ id, _id, slug, tagBackground, tagLabel }: SecretTags, i: number) => { - // This map lookup shouldn't ever fail, but if it does we default to the first color - const tagColor = tagColorByTagId.get(_id) || tagColors[0] + {secretTags.map(({ id, _id, slug, tagColor }: SecretTags, i: number) => { return ( - remove(i)} - key={id} - > - {slug} - ) + + +
+ remove(i)} + key={id} + className="cursor-pointer" + > +
+
+ {slug} +
+ +
+
+
+ +
+ Add tags to {secKey || "this secret"} +
+
+ {wsTags?.map((wsTag) => ( + + ))} + +
+
+
+ + ) })}
@@ -433,25 +430,22 @@ export const SecretInputRow = memo( -
+
Add tags to {secKey || "this secret"}
-
+
{wsTags?.map((wsTag) => (
@@ -207,7 +208,10 @@ export default function PasswordReset() { // Enter new password const stepEnterNewPassword = ( -
+

Enter new password

@@ -227,18 +231,29 @@ export default function PasswordReset() { setPasswordErrorTooLong, setPasswordErrorNumber, setPasswordErrorLowerCase, + setPasswordErrorIsBreached, errorCheck: false }); }} type="password" value={newPassword} isRequired - error={passwordErrorTooShort && passwordErrorTooLong && passwordErrorLowerCase && passwordErrorNumber} + error={ + passwordErrorTooShort && + passwordErrorTooLong && + passwordErrorNumber && + passwordErrorLowerCase && + passwordErrorIsBreached + } autoComplete="new-password" id="new-password" />
- {passwordErrorTooShort || passwordErrorTooLong || passwordErrorLowerCase || passwordErrorNumber ? ( + {passwordErrorTooShort || + passwordErrorTooLong || + passwordErrorNumber || + passwordErrorLowerCase || + passwordErrorIsBreached ? (
Password should contain:
@@ -261,6 +276,16 @@ export default function PasswordReset() { at most 100 characters
+
+ {passwordErrorNumber ? ( + + ) : ( + + )} +
+ at least 1 number +
+
{passwordErrorLowerCase ? ( @@ -272,15 +297,18 @@ export default function PasswordReset() { > at least 1 lowercase character
-
-
- {passwordErrorNumber ? ( - - ) : ( - - )} -
- at least 1 number +
+ {passwordErrorIsBreached ? ( + + ) : ( + + )} +
+ The password you provided is in a list of passwords commonly used on other websites. + Please try again with a stronger password. +
From 1242d88acb8438f5a5f50aaf92025d42edf05cfe Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 20:20:54 +1000 Subject: [PATCH 15/80] Fixed breached pwd error messages --- frontend/public/locales/en/translations.json | 3 +- frontend/public/locales/es/translations.json | 4 +- frontend/public/locales/fr/translations.json | 6 +- frontend/public/locales/ko/translations.json | 6 +- .../public/locales/pt-BR/translations.json | 6 +- frontend/public/locales/tr/translations.json | 4 +- .../utilities/checks/PasswordCheck.ts | 13 ++- .../utilities/checks/checkPassword.ts | 109 +++++++++--------- 8 files changed, 81 insertions(+), 70 deletions(-) diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index 506495b3f..84554d8fc 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -234,8 +234,9 @@ "validate-base": "Password should contain:", "validate-too-short": "at least 14 characters", "validate-too-long": "at most 100 characters", + "validate-number": "at least 1 number", "validate-case": "at least 1 lowercase character", - "validate-number": "at least 1 number" + "validate-breached": "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password." }, "token": { "service-tokens": "Service Tokens", diff --git a/frontend/public/locales/es/translations.json b/frontend/public/locales/es/translations.json index 28e2793cb..b9fe5f335 100644 --- a/frontend/public/locales/es/translations.json +++ b/frontend/public/locales/es/translations.json @@ -231,8 +231,8 @@ "validate-base": "La contraseña debe contener:", "validate-too-short": "como mínimo 14 caracteres", "validate-too-long": "como máximo 100 caracteres", - "validate-case": "como mínimo 1 letra en minúsculas", - "validate-number": "como mínimo 1 número" + "validate-number": "como mínimo 1 número", + "validate-case": "como mínimo 1 letra en minúsculas" }, "token": { "service-tokens": "Tokens de servicio", diff --git a/frontend/public/locales/fr/translations.json b/frontend/public/locales/fr/translations.json index 49edd33fa..00556b1d8 100644 --- a/frontend/public/locales/fr/translations.json +++ b/frontend/public/locales/fr/translations.json @@ -218,8 +218,8 @@ "validate-base": "Le mot de passe doit contenir:", "validate-too-short": "au moins 14 caractères", "validate-too-long": "au maximum 100 caractères", - "validate-case": "au moins 1 caractère miniscule", - "validate-number": "au moins 1 chiffre" + "validate-number": "au moins 1 chiffre", + "validate-case": "au moins 1 caractère miniscule" }, "token": { "service-tokens": "Jetons de service", @@ -297,4 +297,4 @@ "step5-subtitle": "Infisical a pour but d'être utilisé avec vos coéquipiers. Invitez-les à le tester.", "step5-skip": "Passer" } -} \ No newline at end of file +} diff --git a/frontend/public/locales/ko/translations.json b/frontend/public/locales/ko/translations.json index d29a5846c..b40109400 100644 --- a/frontend/public/locales/ko/translations.json +++ b/frontend/public/locales/ko/translations.json @@ -185,8 +185,8 @@ "validate-base": "비밀번호는 다음 조건을 만족해야 합니다:", "validate-too-short": "14 글자 이상", "validate-too-long": "100 자 이하", - "validate-case": "1개 이상의 소문자", - "validate-number": "1개 이상의 숫자" + "validate-number": "1개 이상의 숫자", + "validate-case": "1개 이상의 소문자" }, "token": { "add-dialog": { @@ -257,4 +257,4 @@ "step4-description3": "분실시 접근하거나 복구할 수 없는 시크릿 키가 포함되어 있어요.", "step4-download": "PDF 다운로드" } -} \ No newline at end of file +} diff --git a/frontend/public/locales/pt-BR/translations.json b/frontend/public/locales/pt-BR/translations.json index e04de50da..805abd126 100644 --- a/frontend/public/locales/pt-BR/translations.json +++ b/frontend/public/locales/pt-BR/translations.json @@ -213,8 +213,8 @@ "validate-base": "A senha deve conter:", "validate-too-short": "pelo menos 14 caracteres", "validate-too-long": "no máximo 100 caracteres", - "validate-case": "pelo menos 1 caractere minúsculo", - "validate-number": "pelo menos 1 número" + "validate-number": "pelo menos 1 número", + "validate-case": "pelo menos 1 caractere minúsculo" }, "token": { "service-tokens": "Tokens de Serviço", @@ -291,4 +291,4 @@ "step5-subtitle": "Infisical foi feito para ser usado com seus colegas. Convide-os para testar também.", "step5-skip": "Pular" } -} \ No newline at end of file +} diff --git a/frontend/public/locales/tr/translations.json b/frontend/public/locales/tr/translations.json index c98399f7f..a03b9eb2c 100644 --- a/frontend/public/locales/tr/translations.json +++ b/frontend/public/locales/tr/translations.json @@ -231,8 +231,8 @@ "validate-base": "Şifre kısıtlamaları:", "validate-too-short": "en az 14 karakter", "validate-too-long": "en fazla 100 karakter", - "validate-case": "en az 1 küçük harf", - "validate-number": "en az 1 rakam" + "validate-number": "en az 1 rakam", + "validate-case": "en az 1 küçük harf" }, "token": { "service-tokens": "Servis Belirteçleri", diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts index 92d891d97..133eb5a35 100644 --- a/frontend/src/components/utilities/checks/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/PasswordCheck.ts @@ -1,3 +1,5 @@ +import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; + /* eslint-disable no-param-reassign */ interface PasswordCheckProps { password: string; @@ -6,17 +8,19 @@ interface PasswordCheckProps { setPasswordErrorTooLong: (value: boolean) => void; setPasswordErrorNumber: (value: boolean) => void; setPasswordErrorLowerCase: (value: boolean) => void; + setPasswordErrorIsBreached: (value: boolean) => void; } /** * This function checks a user password with respect to some criteria. */ -const passwordCheck = ({ +const passwordCheck = async ({ password, setPasswordErrorTooShort, setPasswordErrorTooLong, setPasswordErrorNumber, setPasswordErrorLowerCase, + setPasswordErrorIsBreached, errorCheck }: PasswordCheckProps) => { if (!password || password.length < 14) { @@ -57,6 +61,13 @@ const passwordCheck = ({ setPasswordErrorLowerCase(false); } + if (await checkIsPasswordBreached(password)) { + setPasswordErrorIsBreached(true); + errorCheck = true; + } else { + setPasswordErrorIsBreached(false); + } + // if (!/[A-Z]/.test(password)) { // setPasswordErrorUpperCase(true); // errorCheck = true; diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 0ef1e949b..4f0d0a523 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -1,21 +1,21 @@ import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; type Errors = { - tooShort?: string, - tooLong?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, - commonPassword?: string, - breachedPassword?: string - }; + tooShort?: string; + tooLong?: string; + upperCase?: string; + lowerCase?: string; + number?: string; + specialChar?: string; + repeatedChar?: string; + commonPassword?: string; + breachedPassword?: string; +}; interface CheckPasswordParams { - password: string; - commonPasswords: string[]; - setErrors: (value: Errors) => void; + password: string; + commonPasswords: string[]; + setErrors: (value: Errors) => void; } /** @@ -26,62 +26,61 @@ interface CheckPasswordParams { * - Contains at least 1 lowercase character (a-z) * - Contains at least 1 number (0-9) * - Does not contain 3 repeat, consecutive characters - * + * * The function returns whether or not the password [password] - * passes the minimum requirements above. It sets errors on + * passes the minimum requirements above. It sets errors on * an erorr object via [setErrors]. - * + * * @param {Object} obj * @param {String} obj.password - the password to check * @param {Function} obj.setErrors - set state function to set error object */ const checkPassword = async ({ - password, - commonPasswords, - setErrors + password, + commonPasswords, + setErrors }: CheckPasswordParams): Promise => { - const errors: Errors = {}; + const errors: Errors = {}; - const isBreachedPassword = await checkIsPasswordBreached(password) - - if (password.length < 14) { - errors.tooShort = "at least 14 characters"; - } + if (password.length < 14) { + errors.tooShort = "at least 14 characters"; + } - if (password.length > 100) { - errors.tooLong = "at most 100 characters"; - } + if (password.length > 100) { + errors.tooLong = "at most 100 characters"; + } - if (!/[A-Z]/.test(password)) { - errors.upperCase = "at least 1 uppercase character (A-Z)"; - } + if (!/[A-Z]/.test(password)) { + errors.upperCase = "at least 1 uppercase character (A-Z)"; + } - if (!/[a-z]/.test(password)) { - errors.lowerCase = "at least 1 lowercase character (a-z)"; - } + if (!/[a-z]/.test(password)) { + errors.lowerCase = "at least 1 lowercase character (a-z)"; + } - if (!/[0-9]/.test(password)) { - errors.number = "at least 1 number (0-9)"; - } + if (!/[0-9]/.test(password)) { + errors.number = "at least 1 number (0-9)"; + } - if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { - errors.specialChar = "at least 1 special character (!@#$%^&*(),.?)"; - } + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + errors.specialChar = "at least 1 special character (!@#$%^&*(),.?)"; + } - if (/([A-Za-z0-9])\1\1\1/.test(password)) { - errors.repeatedChar = "No 3 repeat, consecutive characters"; - } - - if (commonPasswords.includes(password)) { - errors.commonPassword = "No common passwords"; - } + if (/([A-Za-z0-9])\1\1\1/.test(password)) { + errors.repeatedChar = "No 3 repeat, consecutive characters"; + } - if (isBreachedPassword) { - errors.breachedPassword = "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password."; - } - - setErrors(errors); - return Object.keys(errors).length > 0; -} + if (commonPasswords.includes(password)) { + errors.commonPassword = "No common passwords"; + } -export default checkPassword; \ No newline at end of file + if (await checkIsPasswordBreached(password)) { + errors.breachedPassword = + "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password."; + } + + setErrors(errors); + return Object.keys(errors).length > 0; +}; + +export default checkPassword; From 026ea29847ea4e154fd17c40a931f1dc1c7efd39 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 20:42:07 +1000 Subject: [PATCH 16/80] further fixes to password check logic --- .../checks/checkIsPasswordBreached.ts | 13 +- .../utilities/checks/checkPassword.ts | 8 +- .../ChangePasswordSection.tsx | 264 +++++++++--------- 3 files changed, 138 insertions(+), 147 deletions(-) diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts index 40a9330db..fba537977 100644 --- a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts @@ -15,21 +15,26 @@ export const checkIsPasswordBreached = async (password: string) => { const textEncoder = new TextEncoder(); const encodedPwd = textEncoder.encode(password); const hash = crypto.createHash("sha1").update(encodedPwd).digest(); - let hashedPwd = Array.from(new Uint8Array(hash)) + + const hashedPwd = Array.from(new Uint8Array(hash)) .map((byte) => byte.toString(16).padStart(2, "0")) .join("") .toUpperCase(); - hashedPwd = hashedPwd.slice(0, 5); // ONLY the first five SHA-1 hash chars are sent over HTTPS (the whole string can be sent but that's not very secure due to SHA-1 flaws) - const response = await axios.get(`${dataBreachCheckAPIBaseURL}${hashedPwd}`); + const hashedPwdToSend = hashedPwd.slice(0, 5); // ONLY the first five SHA-1 hash chars are sent over HTTPS (the whole string can be sent but that's not very secure due to SHA-1 flaws) + + const response = await axios.get(`${dataBreachCheckAPIBaseURL}${hashedPwdToSend}`); const responseData = response.data.toUpperCase(); + const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40)); // compare against the API's ranged db's hash table + console.log("isBreachedPassword:", isBreachedPassword); // remove log later // Clear the hashed password from memory + const zeroBuffer = new Uint8Array(encodedPwd.length); encodedPwd.set(zeroBuffer); - return isBreachedPassword; // boolean: true === "password has been involved in a data breach" + return isBreachedPassword; // boolean: true indicates the password has been involved in a data breach } catch (err: any) { if (axios.isAxiosError(err) && err.response && err.response.status === 429) { console.error("Received a 429 response from the Pwnd Passwords API"); diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 4f0d0a523..210d3f040 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -70,15 +70,15 @@ const checkPassword = async ({ errors.repeatedChar = "No 3 repeat, consecutive characters"; } - if (commonPasswords.includes(password)) { - errors.commonPassword = "No common passwords"; - } - if (await checkIsPasswordBreached(password)) { errors.breachedPassword = "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password."; } + if (commonPasswords.includes(password)) { + errors.commonPassword = "No common passwords"; + } + setErrors(errors); return Object.keys(errors).length > 0; }; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index e17864485..86cb58a01 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Controller, useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -9,160 +9,146 @@ import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import attemptChangePassword from "@app/components/utilities/attemptChangePassword"; import checkPassword from "@app/components/utilities/checks/checkPassword"; -import { - Button, - FormControl, - Input -} from "@app/components/v2"; +import { Button, FormControl, Input } from "@app/components/v2"; import { useUser } from "@app/context"; import { useGetCommonPasswords } from "@app/hooks/api"; type Errors = { - tooShort?: string, - tooLong?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, - breachedPassword?: string + tooShort?: string; + tooLong?: string; + upperCase?: string; + lowerCase?: string; + number?: string; + specialChar?: string; + repeatedChar?: string; + commonPassword?: string; + breachedPassword?: string; }; -const schema = yup.object({ +const schema = yup + .object({ oldPassword: yup.string().required("Old password is required"), newPassword: yup.string().required("New password is required") -}).required(); + }) + .required(); export type FormData = yup.InferType; export const ChangePasswordSection = () => { - const { t } = useTranslation(); - const { createNotification } = useNotificationContext(); - const { user } = useUser(); - const { data: commonPasswords } = useGetCommonPasswords(); - const { reset, control, handleSubmit } = useForm({ - defaultValues: { - oldPassword: "", - newPassword: "" - }, - resolver: yupResolver(schema) - }); - const [errors, setErrors] = useState({}); - const [isLoading, setIsLoading] = useState(false); + const { t } = useTranslation(); + const { createNotification } = useNotificationContext(); + const { user } = useUser(); + const { data: commonPasswords } = useGetCommonPasswords(); + const { reset, control, handleSubmit } = useForm({ + defaultValues: { + oldPassword: "", + newPassword: "" + }, + resolver: yupResolver(schema) + }); + const [errors, setErrors] = useState({}); + const [isLoading, setIsLoading] = useState(false); - const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => { - try { - if (!user?.email) return; - if (!commonPasswords) return; + const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => { + try { + if (!user?.email) return; + if (!commonPasswords) return; - const errorCheck = await checkPassword({ - password: newPassword, - commonPasswords, - setErrors - }); + const errorCheck = await checkPassword({ + password: newPassword, + commonPasswords, + setErrors + }); - if (errorCheck) return; - - setIsLoading(true); - await attemptChangePassword({ - email: user.email, - currentPassword: oldPassword, - newPassword - }); - - setIsLoading(false); - createNotification({ - text: "Successfully changed password", - type: "success" - }); + if (errorCheck) return; - reset(); - window.location.href = "/login"; - - } catch (err) { - console.error(err); - setIsLoading(false); - createNotification({ - text: "Failed to change password", - type: "error" - }); - } + setIsLoading(true); + await attemptChangePassword({ + email: user.email, + currentPassword: oldPassword, + newPassword + }); + + setIsLoading(false); + createNotification({ + text: "Successfully changed password", + type: "success" + }); + + reset(); + window.location.href = "/login"; + } catch (err) { + console.error(err); + setIsLoading(false); + createNotification({ + text: "Failed to change password", + type: "error" + }); } - - return ( - -

- Change password -

-
- ( - - - - )} - control={control} - name="oldPassword" - /> -
-
- ( - - - - )} - control={control} - name="newPassword" - /> -
- {Object.keys(errors).length > 0 && ( -
-
{t("section.password.validate-base")}
- {Object.keys(errors).map((key) => { - if (errors[key as keyof Errors]) { - return ( -
-
- -
-

- {errors[key as keyof Errors]} -

-
- ); - } + }; - return null; - })} + return ( + +

Change password

+
+ ( + + + + )} + control={control} + name="oldPassword" + /> +
+
+ ( + + + + )} + control={control} + name="newPassword" + /> +
+ {Object.keys(errors).length > 0 && ( +
+
{t("section.password.validate-base")}
+ {Object.keys(errors).map((key) => { + if (errors[key as keyof Errors]) { + return ( +
+
+ +
+

{errors[key as keyof Errors]}

- )} - - - ); -} \ No newline at end of file + ); + } + + return null; + })} +
+ )} + + + ); +}; From 00089a6bba65b21f8d8ad17eb764f97a6c4870b3 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 20:57:12 +1000 Subject: [PATCH 17/80] Added breached pwd error translations --- frontend/public/locales/es/translations.json | 3 ++- frontend/public/locales/fr/translations.json | 3 ++- frontend/public/locales/ko/translations.json | 3 ++- frontend/public/locales/pt-BR/translations.json | 3 ++- frontend/public/locales/tr/translations.json | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/public/locales/es/translations.json b/frontend/public/locales/es/translations.json index b9fe5f335..ffb0a5dd4 100644 --- a/frontend/public/locales/es/translations.json +++ b/frontend/public/locales/es/translations.json @@ -232,7 +232,8 @@ "validate-too-short": "como mínimo 14 caracteres", "validate-too-long": "como máximo 100 caracteres", "validate-number": "como mínimo 1 número", - "validate-case": "como mínimo 1 letra en minúsculas" + "validate-case": "como mínimo 1 letra en minúsculas", + "validate-breached": "La contraseña que proporcionó se encuentra en una lista de contraseñas comúnmente utilizadas en otros sitios web. Inténtelo de nuevo con una contraseña más segura." }, "token": { "service-tokens": "Tokens de servicio", diff --git a/frontend/public/locales/fr/translations.json b/frontend/public/locales/fr/translations.json index 00556b1d8..7ce136191 100644 --- a/frontend/public/locales/fr/translations.json +++ b/frontend/public/locales/fr/translations.json @@ -219,7 +219,8 @@ "validate-too-short": "au moins 14 caractères", "validate-too-long": "au maximum 100 caractères", "validate-number": "au moins 1 chiffre", - "validate-case": "au moins 1 caractère miniscule" + "validate-case": "au moins 1 caractère miniscule", + "validate-breached": "Le mot de passe que vous avez fourni figure dans une liste de mots de passe couramment utilisés sur d'autres sites Web. Veuillez réessayer avec un mot de passe plus fort." }, "token": { "service-tokens": "Jetons de service", diff --git a/frontend/public/locales/ko/translations.json b/frontend/public/locales/ko/translations.json index b40109400..bd86c9fdc 100644 --- a/frontend/public/locales/ko/translations.json +++ b/frontend/public/locales/ko/translations.json @@ -186,7 +186,8 @@ "validate-too-short": "14 글자 이상", "validate-too-long": "100 자 이하", "validate-number": "1개 이상의 숫자", - "validate-case": "1개 이상의 소문자" + "validate-case": "1개 이상의 소문자", + "validate-breached": "귀하가 제공한 비밀번호는 다른 웹사이트에서 일반적으로 사용되는 비밀번호 목록에 포함되어 있습니다. 더 강력한 비밀번호로 다시 시도해 주세요." }, "token": { "add-dialog": { diff --git a/frontend/public/locales/pt-BR/translations.json b/frontend/public/locales/pt-BR/translations.json index 805abd126..9412facdb 100644 --- a/frontend/public/locales/pt-BR/translations.json +++ b/frontend/public/locales/pt-BR/translations.json @@ -214,7 +214,8 @@ "validate-too-short": "pelo menos 14 caracteres", "validate-too-long": "no máximo 100 caracteres", "validate-number": "pelo menos 1 número", - "validate-case": "pelo menos 1 caractere minúsculo" + "validate-case": "pelo menos 1 caractere minúsculo", + "validate-breached": "A senha que você forneceu está em uma lista de senhas comumente usadas em outros sites. Tente novamente com uma senha mais forte." }, "token": { "service-tokens": "Tokens de Serviço", diff --git a/frontend/public/locales/tr/translations.json b/frontend/public/locales/tr/translations.json index a03b9eb2c..53d1bfddc 100644 --- a/frontend/public/locales/tr/translations.json +++ b/frontend/public/locales/tr/translations.json @@ -232,7 +232,8 @@ "validate-too-short": "en az 14 karakter", "validate-too-long": "en fazla 100 karakter", "validate-number": "en az 1 rakam", - "validate-case": "en az 1 küçük harf" + "validate-case": "en az 1 küçük harf", + "validate-breached": "Sağladığınız şifre, diğer web sitelerinde yaygın olarak kullanılan şifreler listesinde yer almaktadır. Lütfen daha güçlü bir şifre ile tekrar deneyiniz." }, "token": { "service-tokens": "Servis Belirteçleri", From 1f60a3d73e799309d5579251b09884d7fb19050c Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 22:42:02 +1000 Subject: [PATCH 18/80] fixed more error handling for password checks & translations --- frontend/public/locales/en/translations.json | 7 +- frontend/public/locales/es/translations.json | 11 +- frontend/public/locales/fr/translations.json | 7 +- frontend/public/locales/ko/translations.json | 11 +- .../public/locales/pt-BR/translations.json | 5 +- frontend/public/locales/tr/translations.json | 7 +- .../src/components/signup/UserInfoStep.tsx | 93 ++++++++------ .../utilities/checks/PasswordCheck.ts | 79 +++++++----- .../utilities/checks/checkPassword.ts | 24 +++- frontend/src/pages/password-reset.tsx | 89 ++++++++++--- .../ChangePasswordSection.tsx | 4 +- .../UserInfoSSOStep/UserInfoSSOStep.tsx | 120 ++++++++++-------- 12 files changed, 294 insertions(+), 163 deletions(-) diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index 84554d8fc..8bac30094 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -234,9 +234,12 @@ "validate-base": "Password should contain:", "validate-too-short": "at least 14 characters", "validate-too-long": "at most 100 characters", + "validate-uppercase": "at least 1 uppercase character", + "validate-lowercase": "at least 1 lowercase character", "validate-number": "at least 1 number", - "validate-case": "at least 1 lowercase character", - "validate-breached": "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password." + "validate-special-char": "at least 1 special character", + "validate-repeated-char": "at most 2 repeated consecutive characters", + "validate-is-breached": "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password." }, "token": { "service-tokens": "Service Tokens", diff --git a/frontend/public/locales/es/translations.json b/frontend/public/locales/es/translations.json index ffb0a5dd4..7f3bbf84f 100644 --- a/frontend/public/locales/es/translations.json +++ b/frontend/public/locales/es/translations.json @@ -229,11 +229,14 @@ "current-wrong": "La contraseña actual puede puede que sea incorrecta", "new": "Nueva contraseña", "validate-base": "La contraseña debe contener:", - "validate-too-short": "como mínimo 14 caracteres", + "validate-too-short": "al menos 14 caracteres", "validate-too-long": "como máximo 100 caracteres", - "validate-number": "como mínimo 1 número", - "validate-case": "como mínimo 1 letra en minúsculas", - "validate-breached": "La contraseña que proporcionó se encuentra en una lista de contraseñas comúnmente utilizadas en otros sitios web. Inténtelo de nuevo con una contraseña más segura." + "validate-uppercase": "al menos 1 carácter en mayúscula", + "validate-lowercase": "al menos 1 carácter en minúsculas", + "validate-number": "al menos 1 número", + "validate-special-char": "al menos 1 carácter especial", + "validate-repeated-char": "au plus 2 caracteres consecutivos repetidos", + "validate-breached": "La contraseña que proporcionó se encuentra en una lista de contraseñas comúnmente utilizadas en otros sitios web. Vuelva a intentarlo con una contraseña más segura." }, "token": { "service-tokens": "Tokens de servicio", diff --git a/frontend/public/locales/fr/translations.json b/frontend/public/locales/fr/translations.json index 7ce136191..25e2de3c0 100644 --- a/frontend/public/locales/fr/translations.json +++ b/frontend/public/locales/fr/translations.json @@ -218,9 +218,12 @@ "validate-base": "Le mot de passe doit contenir:", "validate-too-short": "au moins 14 caractères", "validate-too-long": "au maximum 100 caractères", + "validate-uppercase": "au moins 1 caractère miniscule", + "validate-lowercase": "au moins 1 caractère majuscule", "validate-number": "au moins 1 chiffre", - "validate-case": "au moins 1 caractère miniscule", - "validate-breached": "Le mot de passe que vous avez fourni figure dans une liste de mots de passe couramment utilisés sur d'autres sites Web. Veuillez réessayer avec un mot de passe plus fort." + "validate-special-char": "au moins 1 caractère spécial", + "validate-repeated-char": "au plus 2 caractères consécutifs répétés", + "validate-is-breached": "Le mot de passe que vous avez fourni figure dans une liste de mots de passe couramment utilisés sur d'autres sites Web. Veuillez réessayer avec un mot de passe plus fort." }, "token": { "service-tokens": "Jetons de service", diff --git a/frontend/public/locales/ko/translations.json b/frontend/public/locales/ko/translations.json index bd86c9fdc..c6bec94f4 100644 --- a/frontend/public/locales/ko/translations.json +++ b/frontend/public/locales/ko/translations.json @@ -183,10 +183,13 @@ "new": "새 비밀번호", "current-wrong": "현재 비밀번호가 잘못되었어요", "validate-base": "비밀번호는 다음 조건을 만족해야 합니다:", - "validate-too-short": "14 글자 이상", - "validate-too-long": "100 자 이하", - "validate-number": "1개 이상의 숫자", - "validate-case": "1개 이상의 소문자", + "validate-too-short": "최소 14자", + "validate-too-long": "최대 100자", + "validate-uppercase": "최소 1개의 대문자", + "validate-lowercase": "최소 1개의 소문자", + "validate-number": "숫자 1개 이상", + "validate-special-char": "특수 문자 1개 이상", + "validate-repeated-char": "최대 2개의 반복되는 연속 문자", "validate-breached": "귀하가 제공한 비밀번호는 다른 웹사이트에서 일반적으로 사용되는 비밀번호 목록에 포함되어 있습니다. 더 강력한 비밀번호로 다시 시도해 주세요." }, "token": { diff --git a/frontend/public/locales/pt-BR/translations.json b/frontend/public/locales/pt-BR/translations.json index 9412facdb..d6958f5a8 100644 --- a/frontend/public/locales/pt-BR/translations.json +++ b/frontend/public/locales/pt-BR/translations.json @@ -213,8 +213,11 @@ "validate-base": "A senha deve conter:", "validate-too-short": "pelo menos 14 caracteres", "validate-too-long": "no máximo 100 caracteres", + "validate-uppercase": "pelo menos 1 caractere maiúsculo", + "validate-lowercase": "pelo menos 1 caractere minúsculo", "validate-number": "pelo menos 1 número", - "validate-case": "pelo menos 1 caractere minúsculo", + "validate-special-char": "pelo menos 1 caractere especial", + "validate-repeated-char": "au plus 2 caractères consécutifs répétés", "validate-breached": "A senha que você forneceu está em uma lista de senhas comumente usadas em outros sites. Tente novamente com uma senha mais forte." }, "token": { diff --git a/frontend/public/locales/tr/translations.json b/frontend/public/locales/tr/translations.json index 53d1bfddc..cf441d0ae 100644 --- a/frontend/public/locales/tr/translations.json +++ b/frontend/public/locales/tr/translations.json @@ -231,8 +231,11 @@ "validate-base": "Şifre kısıtlamaları:", "validate-too-short": "en az 14 karakter", "validate-too-long": "en fazla 100 karakter", - "validate-number": "en az 1 rakam", - "validate-case": "en az 1 küçük harf", + "validate-uppercase": "en az 1 büyük harf karakter", + "validate-lowercase": "en az 1 küçük harf karakter", + "validate-number": "en az 1 sayı", + "validate-special-char": "en az 1 özel karakter", + "validate-repeated-char": "veya artı 2 karakter ardışık tekrar", "validate-breached": "Sağladığınız şifre, diğer web sitelerinde yaygın olarak kullanılan şifreler listesinde yer almaktadır. Lütfen daha güçlü bir şifre ile tekrar deneyiniz." }, "token": { diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 581298c42..45ae09d0d 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -39,14 +39,15 @@ interface UserInfoStepProps { } type Errors = { - tooShort?: string, - tooLong?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, - breachedPassword?: string + tooShort?: string; + tooLong?: string; + upperCase?: string; + lowerCase?: string; + number?: string; + specialChar?: string; + repeatedChar?: string; + isBeachedPassword?: string; + isCommonPassword?: string; }; /** @@ -73,7 +74,7 @@ export default function UserInfoStep({ setOrganizationName, attributionSource, setAttributionSource, - providerAuthToken, + providerAuthToken }: UserInfoStepProps): JSX.Element { const { data: commonPasswords } = useGetCommonPasswords(); const [nameError, setNameError] = useState(false); @@ -101,7 +102,7 @@ export default function UserInfoStep({ } else { setOrganizationNameError(false); } - + errorCheck = await checkPassword({ password, commonPasswords, @@ -176,7 +177,7 @@ export default function UserInfoStep({ salt: result.salt, verifier: result.verifier, organizationName, - attributionSource, + attributionSource }); // unset signup JWT token and set JWT token @@ -193,7 +194,7 @@ export default function UserInfoStep({ }); const userOrgs = await fetchOrganizations(); - + const orgId = userOrgs[0]?._id; const project = await ProjectService.initProject({ organizationId: orgId, @@ -217,13 +218,15 @@ export default function UserInfoStep({ }; return ( -
-

+

+

{t("signup.step3-message")}

-
-
-

Your Name

+
+
+

+ Your Name +

setName(e.target.value)} @@ -232,10 +235,16 @@ export default function UserInfoStep({ autoComplete="given-name" className="h-12" /> - {nameError &&

Please, specify your name

} + {nameError && ( +

+ Please, specify your name +

+ )}
-
-

Organization Name

+
+

+ Organization Name +

setOrganizationName(e.target.value)} @@ -243,10 +252,16 @@ export default function UserInfoStep({ isRequired className="h-12" /> - {organizationNameError &&

Please, specify your organization name

} + {organizationNameError && ( +

+ Please, specify your organization name +

+ )}
-
-

Where did you hear about us? (optional)

+
+

+ Where did you hear about us? (optional) +

setAttributionSource(e.target.value)} @@ -254,7 +269,7 @@ export default function UserInfoStep({ className="h-12" />
-
+
{ @@ -274,23 +289,20 @@ export default function UserInfoStep({ /> {Object.keys(errors).length > 0 && (
-
{t("section.password.validate-base")}
+
+ {t("section.password.validate-base")} +
{Object.keys(errors).map((key) => { if (errors[key as keyof Errors]) { return ( -
+
-
-

- {errors[key as keyof Errors]} -

+

{errors[key as keyof Errors]}

); } @@ -300,18 +312,21 @@ export default function UserInfoStep({
)}
-
-
+
+
+ > + {" "} + {String(t("signup.signup"))}{" "} +
diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts index 133eb5a35..be0a275f4 100644 --- a/frontend/src/components/utilities/checks/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/PasswordCheck.ts @@ -6,9 +6,12 @@ interface PasswordCheckProps { errorCheck: boolean; setPasswordErrorTooShort: (value: boolean) => void; setPasswordErrorTooLong: (value: boolean) => void; - setPasswordErrorNumber: (value: boolean) => void; + setPasswordErrorUpperCase: (value: boolean) => void; setPasswordErrorLowerCase: (value: boolean) => void; - setPasswordErrorIsBreached: (value: boolean) => void; + setPasswordErrorNumber: (value: boolean) => void; + setPasswordErrorSpecialChar: (value: boolean) => void; + setPasswordErrorRepeatedChar: (value: boolean) => void; + setPasswordErrorIsBreachedPassword: (value: boolean) => void; } /** @@ -18,11 +21,15 @@ const passwordCheck = async ({ password, setPasswordErrorTooShort, setPasswordErrorTooLong, - setPasswordErrorNumber, + setPasswordErrorUpperCase, setPasswordErrorLowerCase, - setPasswordErrorIsBreached, + setPasswordErrorNumber, + setPasswordErrorSpecialChar, + setPasswordErrorRepeatedChar, + setPasswordErrorIsBreachedPassword, errorCheck }: PasswordCheckProps) => { + // tooShort if (!password || password.length < 14) { setPasswordErrorTooShort(true); errorCheck = true; @@ -30,6 +37,7 @@ const passwordCheck = async ({ setPasswordErrorTooShort(false); } + // tooLong if (password.length > 100) { setPasswordErrorTooLong(true); errorCheck = true; @@ -37,51 +45,54 @@ const passwordCheck = async ({ setPasswordErrorTooLong(false); } - if (!/\d/.test(password)) { + // upperCase + if (!/[A-Z]/.test(password)) { + setPasswordErrorUpperCase(true); + errorCheck = true; + } else { + setPasswordErrorUpperCase(false); + } + + // lowerCase + if (!/[a-z]/.test(password)) { + setPasswordErrorLowerCase(true); + errorCheck = true; + } else { + setPasswordErrorLowerCase(false); + } + + // number + if (!/[0-9]/.test(password)) { setPasswordErrorNumber(true); errorCheck = true; } else { setPasswordErrorNumber(false); } - if (!/[a-z]/.test(password)) { - setPasswordErrorLowerCase(true); + // specialChar + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + setPasswordErrorSpecialChar(true); errorCheck = true; - // } else if (/(.)(?:(?!\1).){1,2}/.test(password)) { - // console.log(111) - // setPasswordError(true); - // setPasswordErrorMessage("Password should not contain repeating characters."); - // errorCheck = true; - // } else if (RegExp(`[${email}]`).test(password)) { - // console.log(222) - // setPasswordError(true); - // setPasswordErrorMessage("Password should not contain your email."); - // errorCheck = true; } else { - setPasswordErrorLowerCase(false); + setPasswordErrorSpecialChar(false); } + // repeatedChar + if (/([A-Za-z0-9])\1\1\1/.test(password)) { + setPasswordErrorRepeatedChar(true); + errorCheck = true; + } else { + setPasswordErrorRepeatedChar(false); + } + + // breachedPassword if (await checkIsPasswordBreached(password)) { - setPasswordErrorIsBreached(true); + setPasswordErrorIsBreachedPassword(true); errorCheck = true; } else { - setPasswordErrorIsBreached(false); + setPasswordErrorIsBreachedPassword(false); } - // if (!/[A-Z]/.test(password)) { - // setPasswordErrorUpperCase(true); - // errorCheck = true; - // } else { - // setPasswordErrorUpperCase(false); - // } - - // if (!/(?=.*[!@#$%^&*])/.test(password)) { - // setPasswordErrorSpecialChar(true); - // // "Please add at least 1 special character (*, !, #, %)." - // errorCheck = true; - // } else { - // setPasswordErrorSpecialChar(false); - // } return errorCheck; }; diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 210d3f040..8ad4c6032 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -8,8 +8,8 @@ type Errors = { number?: string; specialChar?: string; repeatedChar?: string; - commonPassword?: string; - breachedPassword?: string; + isBreachedPassword?: string; + isCommonPassword?: string; }; interface CheckPasswordParams { @@ -20,12 +20,15 @@ interface CheckPasswordParams { /** * Validate that the password [password]: - * - Contains at least 14 characters long - * - Contains at most 100 characters long + * - Contains at least 14 characters + * - Contains at most 100 characters * - Contains at least 1 uppercase character (A-Z) * - Contains at least 1 lowercase character (a-z) * - Contains at least 1 number (0-9) + * - Contains at least 1 special character * - Does not contain 3 repeat, consecutive characters + * - Is not in a database of breached passwords + * - Is not in a list of common passwords * * The function returns whether or not the password [password] * passes the minimum requirements above. It sets errors on @@ -42,41 +45,50 @@ const checkPassword = async ({ }: CheckPasswordParams): Promise => { const errors: Errors = {}; + // tooShort if (password.length < 14) { errors.tooShort = "at least 14 characters"; } + // toolong if (password.length > 100) { errors.tooLong = "at most 100 characters"; } + // upperCase if (!/[A-Z]/.test(password)) { errors.upperCase = "at least 1 uppercase character (A-Z)"; } + // lowerCase if (!/[a-z]/.test(password)) { errors.lowerCase = "at least 1 lowercase character (a-z)"; } + // number if (!/[0-9]/.test(password)) { errors.number = "at least 1 number (0-9)"; } + // specialChar if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { errors.specialChar = "at least 1 special character (!@#$%^&*(),.?)"; } + // repeatedChar if (/([A-Za-z0-9])\1\1\1/.test(password)) { errors.repeatedChar = "No 3 repeat, consecutive characters"; } + // breachedPassword if (await checkIsPasswordBreached(password)) { - errors.breachedPassword = + errors.isBreachedPassword = "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password."; } + // commonPassword if (commonPasswords.includes(password)) { - errors.commonPassword = "No common passwords"; + errors.isCommonPassword = "No common passwords"; } setErrors(errors); diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx index 6aa3ea275..4a58f09fb 100644 --- a/frontend/src/pages/password-reset.tsx +++ b/frontend/src/pages/password-reset.tsx @@ -30,9 +30,12 @@ export default function PasswordReset() { const [backupKeyError, setBackupKeyError] = useState(false); const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(false); const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false); - const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); + const [passwordErrorUpperCase, setPasswordErrorUpperCase] = useState(false); const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); - const [passwordErrorIsBreached, setPasswordErrorIsBreached] = useState(false); + const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); + const [passwordErrorSpecialChar, setPasswordErrorSpecialChar] = useState(false); + const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false); + const [passwordErrorIsBreachedPassword, setPasswordErrorIsBreachedPassword] = useState(false); const router = useRouter(); @@ -43,7 +46,7 @@ export default function PasswordReset() { const token = parsedUrl.token as string; const email = (parsedUrl.to as string)?.replace(" ", "+").trim(); - // Unencrypt the private key with a backup key + // Decrypt the private key with a backup key const getEncryptedKeyHandler = async (e: FormEvent) => { e.preventDefault(); try { @@ -71,9 +74,12 @@ export default function PasswordReset() { password: newPassword, setPasswordErrorTooShort, setPasswordErrorTooLong, - setPasswordErrorNumber, + setPasswordErrorUpperCase, setPasswordErrorLowerCase, - setPasswordErrorIsBreached, + setPasswordErrorNumber, + setPasswordErrorSpecialChar, + setPasswordErrorRepeatedChar, + setPasswordErrorIsBreachedPassword, errorCheck: false }); @@ -229,9 +235,12 @@ export default function PasswordReset() { password, setPasswordErrorTooShort, setPasswordErrorTooLong, - setPasswordErrorNumber, + setPasswordErrorUpperCase, setPasswordErrorLowerCase, - setPasswordErrorIsBreached, + setPasswordErrorNumber, + setPasswordErrorSpecialChar, + setPasswordErrorRepeatedChar, + setPasswordErrorIsBreachedPassword, errorCheck: false }); }} @@ -241,9 +250,12 @@ export default function PasswordReset() { error={ passwordErrorTooShort && passwordErrorTooLong && - passwordErrorNumber && + passwordErrorUpperCase && passwordErrorLowerCase && - passwordErrorIsBreached + passwordErrorNumber && + passwordErrorSpecialChar && + passwordErrorRepeatedChar && + passwordErrorIsBreachedPassword } autoComplete="new-password" id="new-password" @@ -251,9 +263,12 @@ export default function PasswordReset() {
{passwordErrorTooShort || passwordErrorTooLong || - passwordErrorNumber || + passwordErrorUpperCase || passwordErrorLowerCase || - passwordErrorIsBreached ? ( + passwordErrorNumber || + passwordErrorSpecialChar || + passwordErrorRepeatedChar || + passwordErrorIsBreachedPassword ? (
Password should contain:
@@ -277,13 +292,15 @@ export default function PasswordReset() {
- {passwordErrorNumber ? ( + {passwordErrorUpperCase ? ( ) : ( )} -
- at least 1 number +
+ at least 1 uppercase character
@@ -297,14 +314,54 @@ export default function PasswordReset() { > at least 1 lowercase character
+
+
+ {passwordErrorNumber ? ( + + ) : ( + + )} +
+ at least 1 number +
- {passwordErrorIsBreached ? ( + {passwordErrorSpecialChar ? ( ) : ( )}
+ at least 1 special character +
+
+
+ {passwordErrorRepeatedChar ? ( + + ) : ( + + )} +
+ at most 2 repeated characters +
+
+
+ {passwordErrorIsBreachedPassword ? ( + + ) : ( + + )} +
The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password. diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index 86cb58a01..725fb0e47 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -21,8 +21,8 @@ type Errors = { number?: string; specialChar?: string; repeatedChar?: string; - commonPassword?: string; - breachedPassword?: string; + isBreachedPassword?: string; + isCommonPassword?: string; }; const schema = yup diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index c44fab790..f142a2bef 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -1,4 +1,3 @@ - import crypto from "crypto"; import React, { useEffect, useState } from "react"; @@ -25,24 +24,25 @@ import ProjectService from "@app/services/ProjectService"; const client = new jsrp.client(); type Props = { - setStep: (step: number) => void; - email: string; - password: string; - setPassword: (value: string) => void; - name: string; - providerOrganizationName: string; - providerAuthToken?: string; -} + setStep: (step: number) => void; + email: string; + password: string; + setPassword: (value: string) => void; + name: string; + providerOrganizationName: string; + providerAuthToken?: string; +}; type Errors = { - tooShort?: string, - tooLong?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, - breachedPassword?: string + tooShort?: string; + tooLong?: string; + upperCase?: string; + lowerCase?: string; + number?: string; + specialChar?: string; + repeatedChar?: string; + isBeachedPassword?: string; + isCommonPassword?: string; }; /** @@ -65,7 +65,7 @@ export const UserInfoSSOStep = ({ password, setPassword, setStep, - providerAuthToken, + providerAuthToken }: Props) => { const { data: commonPasswords } = useGetCommonPasswords(); const [nameError, setNameError] = useState(false); @@ -99,7 +99,7 @@ export const UserInfoSSOStep = ({ } else { setOrganizationNameError(false); } - + errorCheck = await checkPassword({ password, commonPasswords, @@ -212,15 +212,17 @@ export const UserInfoSSOStep = ({ setIsLoading(false); } }; - + return ( -
-

+

+

{t("signup.step3-message")}

-
-
-

Your Name

+
+
+

+ Your Name +

- {nameError &&

Please, specify your name

} + {nameError && ( +

+ Please, specify your name +

+ )}
{providerOrganizationName === undefined && ( -
-

Organization Name

+
+

+ Organization Name +

- {organizationNameError &&

Please, specify your organization name

} + {organizationNameError && ( +

+ Please, specify your organization name +

+ )}
)} {providerOrganizationName === undefined && ( -
-

Where did you hear about us? (optional)

+
+

+ Where did you hear about us? (optional) +

setAttributionSource(e.target.value)} @@ -256,12 +270,12 @@ export const UserInfoSSOStep = ({ />
)} -
+
{ + onChangeHandler={async (pass: string) => { setPassword(pass); - checkPassword({ + await checkPassword({ password: pass, commonPasswords, setErrors @@ -274,26 +288,27 @@ export const UserInfoSSOStep = ({ autoComplete="new-password" id="new-password" /> -
Infisical Password is used as part of the encryption mechanism so that even the authentication provider is not able to access your secrets.
+
+ + Infisical Password is used as part of the encryption mechanism so that even the + authentication provider is not able to access your secrets. +
{Object.keys(errors).length > 0 && (
-
{t("section.password.validate-base")}
+
+ {t("section.password.validate-base")} +
{Object.keys(errors).map((key) => { if (errors[key as keyof Errors]) { return ( -
+
-
-

- {errors[key as keyof Errors]} -

+

{errors[key as keyof Errors]}

); } @@ -303,21 +318,24 @@ export const UserInfoSSOStep = ({
)}
-
-
+
+
+ > + {" "} + {String(t("signup.signup"))}{" "} +
); -} +}; From 3e36adcf5c47290fe7d6574d9a1dd53549f93651 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 23:30:24 +1000 Subject: [PATCH 19/80] Removed all references to commonPasswords & the data file. This api route can be deprecated in favor of the client-side secure call to the haveIBeenPwnd password API. Further the datafile contains no passwords that meet the minimum password criteria. --- backend/src/controllers/v1/authController.ts | 181 +- backend/src/data/common_passwords.txt | 1497 ----------------- backend/src/routes/v1/auth.ts | 25 +- .../src/components/signup/UserInfoStep.tsx | 9 +- .../utilities/checks/checkPassword.ts | 14 +- frontend/src/hooks/api/auth/index.tsx | 6 +- frontend/src/hooks/api/auth/queries.tsx | 140 +- frontend/src/pages/signupinvite.tsx | 119 +- .../ChangePasswordSection.tsx | 5 - .../UserInfoSSOStep/UserInfoSSOStep.tsx | 5 - 10 files changed, 208 insertions(+), 1793 deletions(-) delete mode 100644 backend/src/data/common_passwords.txt diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 03a9a7717..14c717e38 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -1,32 +1,20 @@ import { Request, Response } from "express"; -import fs from "fs"; -import path from "path"; import jwt from "jsonwebtoken"; import * as bigintConversion from "bigint-conversion"; // eslint-disable-next-line @typescript-eslint/no-var-requires const jsrp = require("jsrp"); -import { - LoginSRPDetail, - TokenVersion, - User, -} from "../../models"; +import { LoginSRPDetail, TokenVersion, User } from "../../models"; import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth"; import { checkUserDevice } from "../../helpers/user"; -import { - ACTION_LOGIN, - ACTION_LOGOUT, -} from "../../variables"; -import { - BadRequestError, - UnauthorizedRequestError, -} from "../../utils/errors"; +import { ACTION_LOGIN, ACTION_LOGOUT } from "../../variables"; +import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { EELogService } from "../../ee/services"; import { getUserAgentType } from "../../utils/posthog"; import { getHttpsEnabled, getJwtAuthLifetime, getJwtAuthSecret, - getJwtRefreshSecret, + getJwtRefreshSecret } from "../../config"; import { ActorType } from "../../ee/models"; @@ -44,13 +32,10 @@ declare module "jsonwebtoken" { * @returns */ export const login1 = async (req: Request, res: Response) => { - const { - email, - clientPublicKey, - }: { email: string; clientPublicKey: string } = req.body; + const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body; const user = await User.findOne({ - email, + email }).select("+salt +verifier"); if (!user) throw new Error("Failed to find user"); @@ -59,21 +44,25 @@ export const login1 = async (req: Request, res: Response) => { server.init( { salt: user.salt, - verifier: user.verifier, + verifier: user.verifier }, async () => { // generate server-side public key const serverPublicKey = server.getPublicKey(); - await LoginSRPDetail.findOneAndReplace({ email: email }, { - email: email, - clientPublicKey: clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt), - }, { upsert: true, returnNewDocument: false }) + await LoginSRPDetail.findOneAndReplace( + { email: email }, + { + email: email, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt) + }, + { upsert: true, returnNewDocument: false } + ); return res.status(200).send({ serverPublicKey, - salt: user.salt, + salt: user.salt }); } ); @@ -89,15 +78,19 @@ export const login1 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => { const { email, clientProof } = req.body; const user = await User.findOne({ - email, + email }).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag"); if (!user) throw new Error("Failed to find user"); - const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email }) + const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email }); if (!loginSRPDetailFromDB) { - return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) + return BadRequestError( + Error( + "It looks like some details from the first login are not found. Please try login one again" + ) + ); } const server = new jsrp.server(); @@ -105,7 +98,7 @@ export const login2 = async (req: Request, res: Response) => { { salt: user.salt, verifier: user.verifier, - b: loginSRPDetailFromDB.serverBInt, + b: loginSRPDetailFromDB.serverBInt }, async () => { server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); @@ -117,13 +110,13 @@ export const login2 = async (req: Request, res: Response) => { await checkUserDevice({ user, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); - const tokens = await issueAuthTokens({ + const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); // store (refresh) token in httpOnly cookie @@ -131,20 +124,21 @@ export const login2 = async (req: Request, res: Response) => { httpOnly: true, path: "/", sameSite: "strict", - secure: await getHttpsEnabled(), + secure: await getHttpsEnabled() }); const loginAction = await EELogService.createAction({ name: ACTION_LOGIN, - userId: user._id, + userId: user._id }); - loginAction && await EELogService.createLog({ - userId: user._id, - actions: [loginAction], - channel: getUserAgentType(req.headers["user-agent"]), - ipAddress: req.realIP, - }); + loginAction && + (await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getUserAgentType(req.headers["user-agent"]), + ipAddress: req.realIP + })); // return (access) token in response return res.status(200).send({ @@ -152,12 +146,12 @@ export const login2 = async (req: Request, res: Response) => { publicKey: user.publicKey, encryptedPrivateKey: user.encryptedPrivateKey, iv: user.iv, - tag: user.tag, + tag: user.tag }); } return res.status(400).send({ - message: "Failed to authenticate. Try again?", + message: "Failed to authenticate. Try again?" }); } ); @@ -171,7 +165,7 @@ export const login2 = async (req: Request, res: Response) => { */ export const logout = async (req: Request, res: Response) => { if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) { - await clearTokens(req.authData.tokenVersionId) + await clearTokens(req.authData.tokenVersionId); } // clear httpOnly cookie @@ -179,49 +173,44 @@ export const logout = async (req: Request, res: Response) => { httpOnly: true, path: "/", sameSite: "strict", - secure: (await getHttpsEnabled()) as boolean, + secure: (await getHttpsEnabled()) as boolean }); const logoutAction = await EELogService.createAction({ name: ACTION_LOGOUT, - userId: req.user._id, + userId: req.user._id }); - logoutAction && await EELogService.createLog({ - userId: req.user._id, - actions: [logoutAction], - channel: getUserAgentType(req.headers["user-agent"]), - ipAddress: req.realIP, - }); + logoutAction && + (await EELogService.createLog({ + userId: req.user._id, + actions: [logoutAction], + channel: getUserAgentType(req.headers["user-agent"]), + ipAddress: req.realIP + })); return res.status(200).send({ - message: "Successfully logged out.", + message: "Successfully logged out." }); }; -export const getCommonPasswords = async (req: Request, res: Response) => { - const commonPasswords = fs.readFileSync( - path.resolve(__dirname, "../../data/" + "common_passwords.txt"), - "utf8" - ).split("\n"); - - return res.status(200).send(commonPasswords); -} - export const revokeAllSessions = async (req: Request, res: Response) => { - await TokenVersion.updateMany({ - user: req.user._id, - }, { - $inc: { - refreshVersion: 1, - accessVersion: 1, + await TokenVersion.updateMany( + { + user: req.user._id }, - }); + { + $inc: { + refreshVersion: 1, + accessVersion: 1 + } + } + ); return res.status(200).send({ - message: "Successfully revoked all sessions.", - }); -} + message: "Successfully revoked all sessions." + }); +}; /** * Return user is authenticated @@ -231,9 +220,9 @@ export const revokeAllSessions = async (req: Request, res: Response) => { */ export const checkAuth = async (req: Request, res: Response) => { return res.status(200).send({ - message: "Authenticated", + message: "Authenticated" }); -} +}; /** * Return new JWT access token by first validating the refresh token @@ -244,47 +233,47 @@ export const checkAuth = async (req: Request, res: Response) => { export const getNewToken = async (req: Request, res: Response) => { const refreshToken = req.cookies.jid; - if (!refreshToken) throw BadRequestError({ - message: "Failed to find refresh token in request cookies" - }); + if (!refreshToken) + throw BadRequestError({ + message: "Failed to find refresh token in request cookies" + }); - const decodedToken = ( - jwt.verify(refreshToken, await getJwtRefreshSecret()) - ); + const decodedToken = jwt.verify(refreshToken, await getJwtRefreshSecret()); const user = await User.findOne({ - _id: decodedToken.userId, + _id: decodedToken.userId }).select("+publicKey +refreshVersion +accessVersion"); if (!user) throw new Error("Failed to authenticate unfound user"); - if (!user?.publicKey) - throw new Error("Failed to authenticate not fully set up account"); - + if (!user?.publicKey) throw new Error("Failed to authenticate not fully set up account"); + const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId); - if (!tokenVersion) throw UnauthorizedRequestError({ - message: "Failed to validate refresh token", - }); + if (!tokenVersion) + throw UnauthorizedRequestError({ + message: "Failed to validate refresh token" + }); - if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({ - message: "Failed to validate refresh token", - }); + if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) + throw BadRequestError({ + message: "Failed to validate refresh token" + }); const token = createToken({ payload: { userId: decodedToken.userId, tokenVersionId: tokenVersion._id.toString(), - accessVersion: tokenVersion.refreshVersion, + accessVersion: tokenVersion.refreshVersion }, expiresIn: await getJwtAuthLifetime(), - secret: await getJwtAuthSecret(), + secret: await getJwtAuthSecret() }); return res.status(200).send({ - token, + token }); }; export const handleAuthProviderCallback = (req: Request, res: Response) => { res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`); -} +}; diff --git a/backend/src/data/common_passwords.txt b/backend/src/data/common_passwords.txt deleted file mode 100644 index 01a442b17..000000000 --- a/backend/src/data/common_passwords.txt +++ /dev/null @@ -1,1497 +0,0 @@ -123456 -123456789 -111111 -password -qwerty -abc123 -12345678 -password1 -1234567 -123123 -1234567890 -000000 -12345 -iloveyou -1q2w3e4r5t -1234 -123456a -qwertyuiop -monkey -123321 -dragon -654321 -666666 -123 -myspace1 -a123456 -121212 -1qaz2wsx -123qwe -123abc -tinkle -target123 -gwerty -1g2w3e4r -gwerty123 -zag12wsx -7777777 -qwerty1 -1q2w3e4r -987654321 -222222 -qwe123 -qwerty123 -zxcvbnm -555555 -112233 -fuckyou -asdfghjkl -12345a -123123123 -1q2w3e -qazwsx -computer -aaaaaa -159753 -iloveyou1 -fuckyou1 -princess -789456123 -11111111 -123654 -princess1 -888888 -linkedin -michael -sunshine -football -11111 -777777 -1234qwer -999999 -j38ifUbn -monkey1 -football1 -daniel -azerty -a12345 -123456789a -789456 -asdfgh -love123 -abcd1234 -jordan23 -88888888 -5201314 -12qwaszx -FQRG7CS493 -ashley -asdf -asd123 -superman -jessica -love -samsung -shadow -blink182 -333333 -michael1 -babygirl1 -jesus1 -qwert -k.: -baseball -charlie -0 -hello1 -soccer -killer -131313 -master -1111111 -gfhjkm -0123456789 -987654 -iloveyou2 -angel1 -jordan -147258369 -bitch1 -michelle -q1w2e3r4 -jessica1 -qwer1234 -159357 -soccer1 -liverpool -101010 -zxcvbn -thomas -asdasd -fuckyou2 -justin -nicole -1111111111 -1 -1111 -qazwsxedc -baseball1 -andrew -hello -apple -0987654321 -anthony1 -102030 -money1 -parola -abc -147258 -anthony -111222 -jennifer -number1 -naruto -123456q -696969 -00000000 -joshua -golfer -29rsavoy -myspace -andrea -basketball -qwerty12 -charlie1 -passw0rd -asshole1 -hunter -marina -welcome -010203 -superman1 -password12 -xbox360 -sunshine1 -ashley1 -lovely -babygirl -! -trustno1 -666 -asdf1234 -chocolate -buster -summer -tigger -purple -freedom -loveme -matthew -50cent -password2 -maggie -george -chelsea -12341234 -amanda -hannah -q1w2e3 -friends -shadow1 -william -abcdefg -samantha -12344321 -nicole1 -q1w2e3r4t5y6 -robert -mother -jordan1 -secret -letmein -qweasdzxc -212121 -pokemon -$HEX -internet -batman -love12 -a123456789 -VQsaBLPzLa -qweqwe -hello123 -232323 -butterfly -martin -flower -forever -mustang -1qazxsw2 -iloveu -cjmasterinf -orange -harley -user -brandon1 -london -1234567891 -pepper -chris1 -lol123 -abcdef -whatever -1342 -alexander -loveyou -290966 -wall.e -junior -12413 -qweasd -PE#5GZ29PTZMSE -tudelft -dpbk1234 -DIOSESFIEL -U38fa39 -147852 -cookie -family -jasmine -dragon1 -12345q -nikita -pakistan -123654789 -123789 -amanda1 -joseph -happy1 -ginger -: -matthew1 -snoopy -justin1 -lastfm -3rJs1la7qE -пїЅпїЅпїЅпїЅпїЅпїЅ -antonio -barcelona -matrix -computer1 -hottie1 -sophie -sandra -michelle1 -12345678910 -qqqqqq -arsenal -444444 -brandon -daniel1 -jonathan -killer1 -liverpool1 -mickey -ghbdtn -purple1 -mercedes -patrick -11223344 -diamond -456789 -victoria -asshole -taylor -qwertyu -andrew1 -red123 -lucky1 -eminem -12345qwert -111222tianya -yellow -william1 -bailey -angel -chicken1 -richard -0000 -banana -0000000000 -jasmine1 -benjamin -welcome1 -starwars -hunter1 -cheese -melissa -angela -christian -1234554321 -oliver -chocolate1 -butterfly1 -peanut -55555 -hockey -mylove -natasha -NULL -mommy1 -1234561 -q1w2e3r4t5 -america -252525 -monster -school -456123 -james1 -slipknot -hannah1 -zaq12wsx -chicken -147852369 -gabriel -elizabeth -cookie1 -Status -87654321 -robert1 -ferrari -nathan -1password -buddy1 -1314520 -america1 -metallica -chelsea1 -zzzzzz -prince -adidas -jackson -morgan -rainbow -silver -1234567a -angels -iw14Fi9j -loveme1 -juventus -jennifer1 -!~!1 -bubbles -samuel -fuckoff -lovers -cheese1 -0123456 -123asd -999999999 -madison -elizabeth1 -music -buster1 -lauren -david1 -tigger1 -123qweasd -taylor1 -carlos -tinkerbell -samantha1 -Sojdlg123aljg -joshua1 -poop -stella -myspace123 -asdasd5 -freedom1 -whatever1 -xxxxxx -00000 -valentina -a1b2c3 -741852963 -austin -monica -qaz123 -lovely1 -music1 -harley1 -family1 -spongebob1 -steven -nirvana -1234abcd -hellokitty -thomas1 -7654321 -madison1 -daddy1 -summer1 -cocacola -nicholas -zxc123 -123456m -qwertyui -spiderman -vanessa -diamond1 -142536 -danielle -badoo -7758521 -bandit -pokemon1 -mustang1 -1qaz2wsx3edc -alexis -loulou -justinbieb -yamaha -qwert1 -scooter -rachel -tennis -ronaldo -i -mexico1 -friends1 -victor -maggie1 -asdfasdf -qwerty12345 -lover1 -jesus -123hfjdk147 -nicolas -batman1 -weed420 -password123 -loser1 -123456j -iloveyou! -pepper1 -fuckoff1 -555666 -iloveu2 -sabrina -pussy1 -bubbles1 -098765 -master1 -smokey -a1b2c3d4 -123456789q -qwaszx -heather -jasper -booboo -heather1 -4815162342 -peanut1 -chester -123456s -123456b -google -edward -yankees1 -canada -Exigent -destiny -success -nigger1 -135790 -asdfghjkl1 -124578 -casper -lalala -mother1 -sexy123 -qazxsw -naruto1 -1q2w3e4r5t6y -david -money -yellow1 -patrick1 -flower1 -12121212 -alexander1 -raiders1 -Password1 -sebastian -134679 -zxcvbnm1 -dennis -852456 -hahaha -daniela -ginger1 -olivia -melissa1 -010101 -slipknot1 -spiderman1 -cowboys1 -0000000 -rebecca -741852 -jeremy -a1234567 -dakota -123456d -1a2b3c -apple1 -november -alexandra -159951 -iloveu1 -veronica -fuckme1 -baby123 -yankees -stupid1 -cristina -newyork1 -jackson1 -playboy -friend -iloveyou12 -sammy1 -pimpin1 -phoenix -PolniyPizdec0211 -rocky1 -password! -joseph1 -753951 -p -a838hfiD -richard1 -beautiful1 -mickey1 -carolina -j123456 -202020 -newyork -patricia -charles -stephanie -orange1 -m123456 -421uiopy258 -myspace2 -cameron -spider -barbie -woaini -vincent -mexico -scorpion -monster1 -aaaaa -elephant -asdf123 -963852741 -zk.: -guitar -fucker1 -destiny1 -hotmail -johnny -doudou -q123456 -bailey1 -asdfgh1 -fucker -louise -sparky -sweety -123456abc -shorty1 -booboo1 -december -9876543210 -manchester -midnight -246810 -jessie -dallas -austin1 -s123456 -pass -12345678a -claudia -пїЅпїЅпїЅпїЅпїЅпїЅпїЅ -kristina -lakers -lovelove -crazy1 -tiger1 -thunder -dolphin -a -gangsta1 -jackie -151515 -charlotte -scooter1 -caroline -fuck -merlin -junior1 -super123 -scooby -marseille -aaaa -metallica1 -kitty1 -chris -beautiful -black1 -danielle1 -blessed1 -skater1 -1029384756 -qazwsx123 -456456 -b123456 -genius -guitar1 -tyler1 -peaches -california -sakura -tigers -soleil -lauren1 -green1 -smokey1 -cooper -520520 -muffin -christian1 -love13 -fucku2 -arsenal1 -lucky7 -diablo -apples -george1 -babyboy1 -crystal -1122334455 -player1 -aa123456 -vfhbyf -forever1 -Password -winston -chivas1 -sexy -hockey1 -1a2b3c4d -pussy -playboy1 -stalker -cherry -tweety -toyota -creative -gemini -pretty1 -пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ -maverick -brittany1 -nathan1 -letmein1 -cameron1 -secret1 -google1 -heaven -martina -murphy -spongebob -uQA9Ebw445 -fernando -pretty -startfinding -softball -dolphin1 -fuckme -test123 -qwerty1234 -kobe24 -alejandro -adrian -september -aaaaaa1 -bubba1 -isabella -abc123456 -password3 -jason1 -abcdefg123 -loveyou1 -shannon -100200 -manuel -leonardo -molly1 -flowers -123456z -007007 -password. -321321 -miguel -samsung1 -sergey -sweet1 -abc1234 -windows -qwert123 -vfrcbv -poohbear -d123456 -school1 -badboy -951753 -123456c -111 -steven1 -snoopy1 -garfield -YAgjecc826 -compaq -candy1 -sarah1 -qwerty123456 -123456l -eminem1 -141414 -789789 -maria -steelers -iloveme1 -morgan1 -winner -boomer -lolita -nastya -alexis1 -carmen -angelo -nicholas1 -portugal -precious -jackass1 -jonathan1 -yfnfif -bitch -tiffany -rabbit -rainbow1 -angel123 -popcorn -barbara -brandy -fuckyou! -starwars1 -barney -natalia -hiphop -tiffany1 -shorty -poohbear1 -simone -albert -marlboro -hardcore -cowboys -sydney -alex -scorpio -1234512345 -q12345 -qq123456 -onelove -bond007 -abcdefg1 -eagles -crystal1 -azertyuiop -winter -sexy12 -angelina -james -svetlana -fatima -123456k -icecream -popcorn1 -121314 -john316 -qazwsx1 -victoria1 -twilight -iloveme -9379992 -pass123 -dancer -brittany -beauty -bonjour -maxwell -coffee -dexter -454545 -qazqaz -snickers -love11 -samson -aaaaaaaa -swordfish -fyfcnfcbz -abcd123 -aaa111 -natalie -hottie -passion -alyssa -rockstar1 -lovers1 -florida -alicia -happy -blue123 -123456t -ranger -yourmom1 -pumpkin -denise -edward1 -tweety1 -christine -august -54321 -bella1 -marie1 -seven7 -steelers1 -aaaaa1 -shannon1 -amber1 -cutie1 -peaches1 -florida1 -bonnie -stephanie1 -lollipop -cassie -k. -rachel1 -greenday1 -krishna -teresa -october -iverson3 -motorola -rockstar -hahaha1 -police -lakers24 -fylhtq -andrey -loveme2 -turtle -southside1 -baby -bismillah -pa55word -blessed -emmanuel -666999 -012345 -fluffy -5555555555 -stupid -karina -fishing -musica -password11 -love4ever -melanie -greenday -isabelle -nothing -abcd -chicago -cowboy -mnbvcxz -andrea1 -242424 -babygurl1 -santiago -ssssss -kevin1 -lakers1 -chester1 -321654 -kimberly -carlos1 -z123456 -daisy1 -jackass -m -5555555 -zoosk -boston -happy123 -55555555 -satan666 -111111a -pamela -090909 -francesco -horses -456852 -qwer -vanessa1 -redsox -pookie -a12345678 -110110 -tucker -marley -corvette -778899 -realmadrid -raiders -rangers -people -1123581321 -soccer12 -sayang -shelby -christ -12345t -fktrcfylh -kitten -player -c123456 -qwert12345 -baby12 -trinity -1v7Upjw3nT -p@ssw0rd -thunder1 -zxcvbnm123 -midnight1 -lebron23 -golden -strawberry -orlando -love1234 -lucky13 -asdfg1 -marine -soccer10123456 -password -12345678 -1234 -pussy -12345 -dragon -qwerty -696969 -mustang -letmein -baseball -master -michael -football -shadow -monkey -abc123 -pass -fuckme -6969 -jordan -harley -ranger -iwantu -jennifer -hunter -fuck -2000 -test -batman -trustno1 -thomas -tigger -robert -access -love -buster -1234567 -soccer -hockey -killer -george -sexy -andrew -charlie -superman -asshole -fuckyou -dallas -jessica -panties -pepper -1111 -austin -william -daniel -golfer -summer -heather -hammer -yankees -joshua -maggie -biteme -enter -ashley -thunder -cowboy -silver -richard -fucker -orange -merlin -michelle -corvette -bigdog -cheese -matthew -121212 -patrick -martin -freedom -ginger -blowjob -nicole -sparky -yellow -camaro -secret -dick -falcon -taylor -111111 -131313 -123123 -bitch -hello -scooter -please -porsche -guitar -chelsea -black -diamond -nascar -jackson -cameron -654321 -computer -amanda -wizard -xxxxxxxx -money -phoenix -mickey -bailey -knight -iceman -tigers -purple -andrea -horny -dakota -aaaaaa -player -sunshine -morgan -starwars -boomer -cowboys -edward -charles -girls -booboo -coffee -xxxxxx -bulldog -ncc1701 -rabbit -peanut -john -johnny -gandalf -spanky -winter -brandy -compaq -carlos -tennis -james -mike -brandon -fender -anthony -blowme -ferrari -cookie -chicken -maverick -chicago -joseph -diablo -sexsex -hardcore -666666 -willie -welcome -chris -panther -yamaha -justin -banana -driver -marine -angels -fishing -david -maddog -hooters -wilson -butthead -dennis -fucking -captain -bigdick -chester -smokey -xavier -steven -viking -snoopy -blue -eagles -winner -samantha -house -miller -flower -jack -firebird -butter -united -turtle -steelers -tiffany -zxcvbn -tomcat -golf -bond007 -bear -tiger -doctor -gateway -gators -angel -junior -thx1138 -porno -badboy -debbie -spider -melissa -booger -1212 -flyers -fish -porn -matrix -teens -scooby -jason -walter -cumshot -boston -braves -yankee -lover -barney -victor -tucker -princess -mercedes -5150 -doggie -zzzzzz -gunner -horney -bubba -2112 -fred -johnson -xxxxx -tits -member -boobs -donald -bigdaddy -bronco -penis -voyager -rangers -birdie -trouble -white -topgun -bigtits -bitches -green -super -qazwsx -magic -lakers -rachel -slayer -scott -2222 -asdf -video -london -7777 -marlboro -srinivas -internet -action -carter -jasper -monster -teresa -jeremy -11111111 -bill -crystal -peter -pussies -cock -beer -rocket -theman -oliver -prince -beach -amateur -7777777 -muffin -redsox -star -testing -shannon -murphy -frank -hannah -dave -eagle1 -11111 -mother -nathan -raiders -steve -forever -angela -viper -ou812 -jake -lovers -suckit -gregory -buddy -whatever -young -nicholas -lucky -helpme -jackie -monica -midnight -college -baby -cunt -brian -mark -startrek -sierra -leather -232323 -4444 -beavis -bigcock -happy -sophie -ladies -naughty -giants -booty -blonde -fucked -golden -0 -fire -sandra -pookie -packers -einstein -dolphins -chevy -winston -warrior -sammy -slut -8675309 -zxcvbnm -nipples -power -victoria -asdfgh -vagina -toyota -travis -hotdog -paris -rock -xxxx -extreme -redskins -erotic -dirty -ford -freddy -arsenal -access14 -wolf -nipple -iloveyou -alex -florida -eric -legend -movie -success -rosebud -jaguar -great -cool -cooper -1313 -scorpio -mountain -madison -987654 -brazil -lauren -japan -naked -squirt -stars -apple -alexis -aaaa -bonnie -peaches -jasmine -kevin -matt -qwertyui -danielle -beaver -4321 -4128 -runner -swimming -dolphin -gordon -casper -stupid -shit -saturn -gemini -apples -august -3333 -canada -blazer -cumming -hunting -kitty -rainbow -112233 -arthur -cream -calvin -shaved -surfer -samson -kelly -paul -mine -king -racing -5555 -eagle -hentai -newyork -little -redwings -smith -sticky -cocacola -animal -broncos -private -skippy -marvin -blondes -enjoy -girl -apollo -parker -qwert -time -sydney -women -voodoo -magnum -juice -abgrtyu -777777 -dreams -maxwell -music -rush2112 -russia -scorpion -rebecca -tester -mistress -phantom -billy -6666 -albert \ No newline at end of file diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index b633f82ea..ab141ac05 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -8,7 +8,8 @@ import { AuthMode } from "../../variables"; router.post("/token", validateRequest, authController.getNewToken); -router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1) +router.post( + // TODO endpoint: deprecate (moved to api/v3/auth/login1) "/login1", authLimiter, body("email").exists().trim().notEmpty(), @@ -17,7 +18,8 @@ router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1) authController.login1 ); -router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login2) +router.post( + // TODO endpoint: deprecate (moved to api/v3/auth/login2) "/login2", authLimiter, body("email").exists().trim().notEmpty(), @@ -30,7 +32,7 @@ router.post( "/logout", authLimiter, requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), authController.logout ); @@ -38,24 +40,19 @@ router.post( router.post( "/checkAuth", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), authController.checkAuth ); -router.get( - "/common-passwords", - authLimiter, - authController.getCommonPasswords -); - -router.delete( // TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions) +router.delete( + // TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions) "/sessions", authLimiter, requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), + acceptedAuthModes: [AuthMode.JWT] + }), authController.revokeAllSessions ); -export default router; \ No newline at end of file +export default router; diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 45ae09d0d..0cce660e0 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -8,7 +8,6 @@ import jsrp from "jsrp"; import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; -import { useGetCommonPasswords } from "@app/hooks/api"; import { completeAccountSignup } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import ProjectService from "@app/services/ProjectService"; @@ -47,7 +46,6 @@ type Errors = { specialChar?: string; repeatedChar?: string; isBeachedPassword?: string; - isCommonPassword?: string; }; /** @@ -76,7 +74,6 @@ export default function UserInfoStep({ setAttributionSource, providerAuthToken }: UserInfoStepProps): JSX.Element { - const { data: commonPasswords } = useGetCommonPasswords(); const [nameError, setNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false); @@ -105,7 +102,6 @@ export default function UserInfoStep({ errorCheck = await checkPassword({ password, - commonPasswords, setErrors }); @@ -272,11 +268,10 @@ export default function UserInfoStep({
{ + onChangeHandler={async (pass: string) => { setPassword(pass); - checkPassword({ + await checkPassword({ password: pass, - commonPasswords, setErrors }); }} diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 8ad4c6032..aaa8bac7f 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -9,12 +9,10 @@ type Errors = { specialChar?: string; repeatedChar?: string; isBreachedPassword?: string; - isCommonPassword?: string; }; interface CheckPasswordParams { password: string; - commonPasswords: string[]; setErrors: (value: Errors) => void; } @@ -28,7 +26,6 @@ interface CheckPasswordParams { * - Contains at least 1 special character * - Does not contain 3 repeat, consecutive characters * - Is not in a database of breached passwords - * - Is not in a list of common passwords * * The function returns whether or not the password [password] * passes the minimum requirements above. It sets errors on @@ -38,11 +35,7 @@ interface CheckPasswordParams { * @param {String} obj.password - the password to check * @param {Function} obj.setErrors - set state function to set error object */ -const checkPassword = async ({ - password, - commonPasswords, - setErrors -}: CheckPasswordParams): Promise => { +const checkPassword = async ({ password, setErrors }: CheckPasswordParams): Promise => { const errors: Errors = {}; // tooShort @@ -86,11 +79,6 @@ const checkPassword = async ({ "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password."; } - // commonPassword - if (commonPasswords.includes(password)) { - errors.isCommonPassword = "No common passwords"; - } - setErrors(errors); return Object.keys(errors).length > 0; }; diff --git a/frontend/src/hooks/api/auth/index.tsx b/frontend/src/hooks/api/auth/index.tsx index dbcc77a5a..66208a487 100644 --- a/frontend/src/hooks/api/auth/index.tsx +++ b/frontend/src/hooks/api/auth/index.tsx @@ -1,10 +1,10 @@ export { useGetAuthToken, - useGetCommonPasswords, useResetPassword, - useSendMfaToken, + useSendMfaToken, useSendPasswordResetEmail, useSendVerificationEmail, useVerifyEmailVerificationCode, useVerifyMfaToken, - useVerifyPasswordResetCode} from "./queries" + useVerifyPasswordResetCode +} from "./queries"; diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 094fd4b61..a26297730 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -20,22 +20,22 @@ import { SRPR1Res, VerifyMfaTokenDTO, VerifyMfaTokenRes, - VerifySignupInviteDTO} from "./types"; + VerifySignupInviteDTO +} from "./types"; const authKeys = { - getAuthToken: ["token"] as const, - commonPasswords: ["common-passwords"] as const + getAuthToken: ["token"] as const }; export const login1 = async (loginDetails: Login1DTO) => { const { data } = await apiRequest.post("/api/v3/auth/login1", loginDetails); return data; -} +}; export const login2 = async (loginDetails: Login2DTO) => { const { data } = await apiRequest.post("/api/v3/auth/login2", loginDetails); return data; -} +}; export const useLogin1 = () => { return useMutation({ @@ -47,7 +47,7 @@ export const useLogin1 = () => { return login1(details); } }); -} +}; export const useLogin2 = () => { return useMutation({ @@ -59,22 +59,22 @@ export const useLogin2 = () => { return login2(details); } }); -} +}; export const srp1 = async (details: SRP1DTO) => { const { data } = await apiRequest.post("/api/v1/password/srp1", details); - return data; -} + return data; +}; export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => { const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details); - return data; -} + return data; +}; export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => { const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", details); - return data; -} + return data; +}; export const useCompleteAccountSignup = () => { return useMutation({ @@ -82,7 +82,7 @@ export const useCompleteAccountSignup = () => { return completeAccountSignup(details); } }); -} +}; export const useSendMfaToken = () => { return useMutation<{}, {}, SendMfaTokenDTO>({ @@ -91,22 +91,16 @@ export const useSendMfaToken = () => { return data; } }); -} +}; -export const verifyMfaToken = async ({ - email, - mfaCode -}: { - email: string; - mfaCode: string; -}) => { +export const verifyMfaToken = async ({ email, mfaCode }: { email: string; mfaCode: string }) => { const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", { email, mfaToken: mfaCode }); return data; -} +}; export const useVerifyMfaToken = () => { return useMutation({ @@ -117,87 +111,67 @@ export const useVerifyMfaToken = () => { }); } }); -} +}; export const verifySignupInvite = async (details: VerifySignupInviteDTO) => { const { data } = await apiRequest.post("/api/v1/invite-org/verify", details); return data; -} +}; export const useSendVerificationEmail = () => { return useMutation({ - mutationFn: async ({ - email - }: { - email: string; - }) => { + mutationFn: async ({ email }: { email: string }) => { const { data } = await apiRequest.post("/api/v1/signup/email/signup", { email }); - + return data; } }); -} +}; export const useVerifyEmailVerificationCode = () => { return useMutation({ - mutationFn: async ({ - email, - code - }: { - email: string; - code: string; - }) => { + mutationFn: async ({ email, code }: { email: string; code: string }) => { const { data } = await apiRequest.post("/api/v1/signup/email/verify", { email, code }); - + return data; } }); -} +}; export const useSendPasswordResetEmail = () => { return useMutation({ - mutationFn: async ({ - email - }: { - email: string; - }) => { + mutationFn: async ({ email }: { email: string }) => { const { data } = await apiRequest.post("/api/v1/password/email/password-reset", { email }); - + return data; } }); -} +}; export const useVerifyPasswordResetCode = () => { return useMutation({ - mutationFn: async ({ - email, - code - }: { - email: string; - code: string; - }) => { + mutationFn: async ({ email, code }: { email: string; code: string }) => { const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", { email, code }); - + return data; } }); -} +}; export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => { const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details); return data; -} +}; export const getBackupEncryptedPrivateKey = async ({ verificationToken @@ -207,37 +181,41 @@ export const getBackupEncryptedPrivateKey = async ({ Authorization: `Bearer ${verificationToken}` } }); - + return data.backupPrivateKey; -} +}; export const useResetPassword = () => { return useMutation({ mutationFn: async (details: ResetPasswordDTO) => { - const { data } = await apiRequest.post("/api/v1/password/password-reset", { - protectedKey: details.protectedKey, - protectedKeyIV: details.protectedKeyIV, - protectedKeyTag: details.protectedKeyTag, - encryptedPrivateKey: details.encryptedPrivateKey, - encryptedPrivateKeyIV: details.encryptedPrivateKeyIV, - encryptedPrivateKeyTag: details.encryptedPrivateKeyTag, - salt: details.salt, - verifier: details.verifier - }, { - headers: { - Authorization: `Bearer ${details.verificationToken}` + const { data } = await apiRequest.post( + "/api/v1/password/password-reset", + { + protectedKey: details.protectedKey, + protectedKeyIV: details.protectedKeyIV, + protectedKeyTag: details.protectedKeyTag, + encryptedPrivateKey: details.encryptedPrivateKey, + encryptedPrivateKeyIV: details.encryptedPrivateKeyIV, + encryptedPrivateKeyTag: details.encryptedPrivateKeyTag, + salt: details.salt, + verifier: details.verifier + }, + { + headers: { + Authorization: `Bearer ${details.verificationToken}` + } } - }); - + ); + return data; } }); -} +}; export const changePassword = async (details: ChangePasswordDTO) => { const { data } = await apiRequest.post("/api/v1/password/change-password", details); return data; -} +}; export const useChangePassword = () => { // note: use after srp1 @@ -246,7 +224,7 @@ export const useChangePassword = () => { return changePassword(details); } }); -} +}; // Refresh token is set as cookie when logged in // Using that we fetch the auth bearer token needed for auth calls @@ -263,11 +241,3 @@ export const useGetAuthToken = () => onSuccess: (data) => setAuthToken(data.token), retry: 0 }); - -const fetchCommonPasswords = async () => { - const { data } = await apiRequest.get("/api/v1/auth/common-passwords"); - return data || []; -}; - -export const useGetCommonPasswords = () => - useQuery({ queryKey: authKeys.commonPasswords, queryFn: fetchCommonPasswords }); \ No newline at end of file diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 894c36ea6..7eb478352 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -22,31 +22,23 @@ import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; -import { - useGetCommonPasswords -} from "@app/hooks/api"; -import { - completeAccountSignupInvite, - verifySignupInvite -} from "@app/hooks/api/auth/queries"; +import { completeAccountSignupInvite, verifySignupInvite } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; // eslint-disable-next-line new-cap const client = new jsrp.client(); type Errors = { - length?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, - breachedPassword?: string + length?: string; + upperCase?: string; + lowerCase?: string; + number?: string; + specialChar?: string; + repeatedChar?: string; + breachedPassword?: string; }; export default function SignupInvite() { - const { data: commonPasswords } = useGetCommonPasswords(); - const [password, setPassword] = useState(""); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); @@ -80,10 +72,9 @@ export default function SignupInvite() { } else { setLastNameError(false); } - + errorCheck = await checkPassword({ password, - commonPasswords, setErrors }); @@ -117,7 +108,7 @@ export default function SignupInvite() { if (!derivedKey) throw new Error("Failed to derive key from password"); const key = crypto.randomBytes(32); - + // create encrypted private key by encrypting the private // key with the symmetric key [key] const { @@ -128,7 +119,7 @@ export default function SignupInvite() { text: privateKey, secret: key }); - + // create the protected key by encrypting the symmetric key // [key] with the derived key const { @@ -139,10 +130,8 @@ export default function SignupInvite() { text: key.toString("hex"), secret: Buffer.from(derivedKey.hash) }); - - const { - token: jwtToken - } = await completeAccountSignupInvite({ + + const { token: jwtToken } = await completeAccountSignupInvite({ email, firstName, lastName, @@ -156,20 +145,20 @@ export default function SignupInvite() { salt: result.salt, verifier: result.verifier }); - + // unset temporary signup JWT token and set JWT token SecurityClient.setSignupToken(""); SecurityClient.setToken(jwtToken); saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - privateKey + publicKey, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag, + privateKey }); - const userOrgs = await fetchOrganizations(); + const userOrgs = await fetchOrganizations(); const orgId = userOrgs[0]._id; localStorage.setItem("orgData.id", orgId); @@ -189,12 +178,12 @@ export default function SignupInvite() { // Step 4 of the sign up process (download the emergency kit pdf) const stepConfirmEmail = ( -
-

+

+

Confirm your email

verify email -
+
diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 7eb478352..081dfeadc 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -35,6 +35,7 @@ type Errors = { number?: string; specialChar?: string; repeatedChar?: string; + isEmail?: string; breachedPassword?: string; }; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index 680b0fdbc..a7611f8bf 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -20,6 +20,7 @@ type Errors = { number?: string; specialChar?: string; repeatedChar?: string; + isEmail?: string; isBreachedPassword?: string; }; diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index 493d2b0cc..c159d7784 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -40,6 +40,7 @@ type Errors = { number?: string; specialChar?: string; repeatedChar?: string; + isEmail?: string; isBeachedPassword?: string; }; From 25fc508d5e36fd5b6f0699f53526fd4d6cc4aca8 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Wed, 23 Aug 2023 02:56:03 +1000 Subject: [PATCH 29/80] Fixed spelling --- frontend/src/components/utilities/checks/checkPassword.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 34c09ac9d..b44c47df4 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -78,8 +78,7 @@ const checkPassword = async ({ password, setErrors }: CheckPasswordParams): Prom password ) ) { - errors.specialChar = - "at least 1 special character (emojis and many langauge scripts supported)"; + errors.specialChar = "at least 1 special character (emojis, symbols & non-Latin languages)"; } // repeatedChar From 7ec00475c6a458fa96e18a108ea47750d7308a1b Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Wed, 23 Aug 2023 12:59:00 +1000 Subject: [PATCH 30/80] +maxRetryAttempts, padding & safer error handling. Improved readability & comments. --- .../checks/checkIsPasswordBreached.ts | 92 +++++++++++++------ 1 file changed, 64 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts index 75caf9dcf..abfeac508 100644 --- a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts @@ -1,47 +1,83 @@ import axios from "axios"; import { createHash } from "crypto"; // added types from @types/node -export const checkIsPasswordBreached = async (password: string) => { // see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange // in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table - // if there is a match, that password has been involved in a password breach and should NOT be accepted - + // this hash table is formed from the 5 char hash prefix (ie. 00000-FFFFF) so 16^5 results + // returns a hash table of 800-1000 results + // padding has been added to prevent MiTM attacker determining which hash table was called by the response size + // the last 35 chars of the password hash are compared client-side against the table + // if there is a match, that password has been involved in a password breach (ie. pwnd) and should NOT be accepted // the database consists of ~700 mln breached passwords and is continuously updated, including with law enforcement ingestion // https://www.troyhunt.com/open-source-pwned-passwords-with-fbi-feed-and-225m-new-nca-passwords-is-now-live/ + // The HIBP API follows NIST guidance (pg.14) https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf + // "When processing requests to establish and change memorized secrets, verifiers SHALL compare + // the prospective secrets against a list that contains values known to be commonly-used, expected, + // or compromised. For example, the list MAY include, but is not limited to: + // • Passwords obtained from previous breach corpuses. + // • Dictionary words. + // • Repetitive or sequential characters (e.g. ‘aaaaaa’, ‘1234abcd’). + // • Context-specific words, such as the name of the service, the username, and derivatives + // thereof." + +export const checkIsPasswordBreached = async (password: string) => { const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; // added to CSP + const maxRetryAttempts = 3; try { const textEncoder = new TextEncoder(); const encodedPwd = textEncoder.encode(password); const hash = createHash("sha1").update(encodedPwd).digest(); + const hashedPwd = hash.toString("hex").toUpperCase(); + const hashedPwdToSend = hashedPwd.slice(0, 5); // ONLY the first five hash chars are sent + const rangedHashTableUri = `${dataBreachCheckAPIBaseURL}${hashedPwdToSend}`; + + let response; + let retryAttempt = 0; - const hashedPwd = Array.from(new Uint8Array(hash)) - .map((byte) => byte.toString(16).padStart(2, "0")) - .join("") - .toUpperCase(); + while (retryAttempt < maxRetryAttempts) { + try { + response = await axios.get(rangedHashTableUri, { + headers: { + "Add-Padding": "true", // see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/ + "Content-Type": "text/plain", + }, + }); - // ONLY the first five SHA-1 hash chars need to be sent (must be over HTTPS) - const hashedPwdToSend = hashedPwd.slice(0, 5); - - const response = await axios.get(`${dataBreachCheckAPIBaseURL}${hashedPwdToSend}`); - const responseData = response.data.toUpperCase(); - - // compare against the API's ranged db's hash table - const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40)); - - // Clear the hashed password from memory as a precaution - const zeroBuffer = new Uint8Array(encodedPwd.length); - encodedPwd.set(zeroBuffer); - - return isBreachedPassword; // boolean: true indicates the password has been involved in a data breach - } catch (err: any) { - if (axios.isAxiosError(err) && err.response && err.response.status === 429) { - console.error("Received a 429 response from the Pwnd Passwords API"); - // Handle the 429 error here (not 100% sure what the rate limits are for the password API but looks like <10 calls/min) - // an error here should probably not cause the setting/resetting/changing password to fail (unless desired) - } else { - console.error(err); + if (response.status === 200) { + break; + } else { + retryAttempt++; + } + } catch (err) { + if (!axios.isAxiosError(err)) { + throw err; + } + retryAttempt++; + } } + + if (response && response.status === 200) { + const responseData = response.data.toUpperCase(); + // compare last 35 hash chars to the returned ranged hash table + // returns a boolean: true indicates the password has been involved in a data breach (ie. pwnd) + const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40)); + + // Clear the hashed password from memory as a precaution + const zeroBuffer = new Uint8Array(encodedPwd.length); + encodedPwd.set(zeroBuffer); + + return isBreachedPassword; + } + + console.error( + `Received a non-200 response (${response ? response.status : "unknown"}) from the Pwnd Passwords API` + ); + return false; // better to return a safe response if no breach can be determined + } catch (err: any) { + console.error("An unexpected error has occurred:", err.message); + return false; // Return a safe response in case of unexpected errors + // the HIBP API could return 400 (empty string supplied), 429 or 503 if Cloudflare edge node is down) } }; From 8b381b2b80487b59eb8f11b107b598b1061ad35c Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 23 Aug 2023 16:30:42 +0700 Subject: [PATCH 31/80] Checkpoint add metadata to secret and secret version data structure --- backend/src/controllers/v2/secretsController.ts | 10 +++++++++- backend/src/ee/models/secretVersion.ts | 6 ++++++ backend/src/helpers/secrets.ts | 10 ++++++---- backend/src/models/secret.ts | 6 ++++++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index c87b3756a..c513415a6 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -739,6 +739,7 @@ export const createSecrets = async (req: Request, res: Response) => { * @returns */ export const getSecrets = async (req: Request, res: Response) => { + console.log("getSecrets"); /* #swagger.summary = 'Read secrets' #swagger.description = 'Read secrets from a project and environment' @@ -966,8 +967,13 @@ export const getSecrets = async (req: Request, res: Response) => { ); const postHogClient = await TelemetryService.getPostHogClient(); + + console.log("the fetched secrets: ", secrets); + console.log("postHogClient: ", postHogClient); + if (postHogClient) { - postHogClient.capture({ + console.log("should capture!"); + const test = postHogClient.capture({ event: "secrets pulled", distinctId: await TelemetryService.getDistinctId({ authData: req.authData @@ -981,6 +987,8 @@ export const getSecrets = async (req: Request, res: Response) => { userAgent: req.headers?.["user-agent"] } }); + + console.log("test: ", test); } return res.status(200).send({ diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index d63f05cf7..8fec54515 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -28,6 +28,9 @@ export interface ISecretVersion { createdAt: string; folder?: string; tags?: string[]; + metadata?: { + [key: string]: string; + } } const secretVersionSchema = new Schema( @@ -118,6 +121,9 @@ const secretVersionSchema = new Schema( type: [Schema.Types.ObjectId], default: [], }, + metadata: { + type: Schema.Types.Mixed + } }, { timestamps: true, diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index d4544d32b..dc91560fb 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -393,9 +393,10 @@ export const createSecretHelper = async ({ secretCommentTag, folder: folderId, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 + keyEncoding: ENCODING_SCHEME_UTF8, + metadata }).save(); - + const secretVersion = new SecretVersion({ secret: secret._id, version: secret.version, @@ -413,9 +414,10 @@ export const createSecretHelper = async ({ secretValueIV, secretValueTag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 + keyEncoding: ENCODING_SCHEME_UTF8, + metadata }); - + // (EE) add version for new secret await EESecretService.addSecretVersions({ secretVersions: [secretVersion] diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index 34a4d7501..4ef3de456 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -31,6 +31,9 @@ export interface ISecret { keyEncoding: "utf8" | "base64"; tags?: string[]; folder?: string; + metadata?: { + [key: string]: string; + } } const secretSchema = new Schema( @@ -131,6 +134,9 @@ const secretSchema = new Schema( type: String, default: "root", }, + metadata: { + type: Schema.Types.Mixed + } }, { timestamps: true, From c342b22d4915d01b624b0c01566bcb7f8480c3aa Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 23 Aug 2023 17:37:01 +0700 Subject: [PATCH 32/80] Fix telemetry issue for signup secrets --- backend/src/controllers/v2/secretsController.ts | 12 ++++-------- backend/src/helpers/secrets.ts | 6 ++++-- frontend/src/hooks/api/secrets/queries.tsx | 3 ++- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index c513415a6..473512390 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -234,6 +234,9 @@ export const batchSecrets = async (req: Request, res: Response) => { $inc: { version: 1 }, + $unset: { + 'metadata.source': true as true + }, ...u, _id: new Types.ObjectId(u._id) } @@ -739,7 +742,6 @@ export const createSecrets = async (req: Request, res: Response) => { * @returns */ export const getSecrets = async (req: Request, res: Response) => { - console.log("getSecrets"); /* #swagger.summary = 'Read secrets' #swagger.description = 'Read secrets from a project and environment' @@ -968,12 +970,8 @@ export const getSecrets = async (req: Request, res: Response) => { const postHogClient = await TelemetryService.getPostHogClient(); - console.log("the fetched secrets: ", secrets); - console.log("postHogClient: ", postHogClient); - if (postHogClient) { - console.log("should capture!"); - const test = postHogClient.capture({ + postHogClient.capture({ event: "secrets pulled", distinctId: await TelemetryService.getDistinctId({ authData: req.authData @@ -987,8 +985,6 @@ export const getSecrets = async (req: Request, res: Response) => { userAgent: req.headers?.["user-agent"] } }); - - console.log("test: ", test); } return res.status(200).send({ diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index dc91560fb..08a748baf 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -568,15 +568,17 @@ export const getSecretsHelper = async ({ ); const postHogClient = await TelemetryService.getPostHogClient(); + + const numberOfSignupSecrets = (secrets.filter((secret) => secret?.metadata?.source === "signup")).length; - if (postHogClient) { + if (postHogClient && (secrets.length - numberOfSignupSecrets > 0)) { postHogClient.capture({ event: "secrets pulled", distinctId: await TelemetryService.getDistinctId({ authData }), properties: { - numberOfSecrets: secrets.length, + numberOfSecrets: secrets.length - numberOfSignupSecrets, environment, workspaceId, folderId, diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index e1c3e10bf..bd9fee048 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -38,7 +38,7 @@ const fetchProjectEncryptedSecrets = async ( folderId?: string, secretPath?: string ) => { - const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", { + const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v3/secrets", { params: { environment: env, workspaceId, @@ -46,6 +46,7 @@ const fetchProjectEncryptedSecrets = async ( secretPath } }); + return data.secrets; }; From 2d7c7f075ed3b9579fb8c9ad870146c0cd7089c2 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 23 Aug 2023 17:47:25 +0700 Subject: [PATCH 33/80] Remove metadata from SecretVersion schema --- backend/src/ee/models/secretVersion.ts | 6 ------ backend/src/helpers/secrets.ts | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index 8fec54515..84174ac87 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -28,9 +28,6 @@ export interface ISecretVersion { createdAt: string; folder?: string; tags?: string[]; - metadata?: { - [key: string]: string; - } } const secretVersionSchema = new Schema( @@ -120,9 +117,6 @@ const secretVersionSchema = new Schema( ref: "Tag", type: [Schema.Types.ObjectId], default: [], - }, - metadata: { - type: Schema.Types.Mixed } }, { diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 08a748baf..2947d2a8e 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -414,8 +414,7 @@ export const createSecretHelper = async ({ secretValueIV, secretValueTag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - metadata + keyEncoding: ENCODING_SCHEME_UTF8 }); // (EE) add version for new secret From ac66834daa6b722038f68ad216d5ebb87556424e Mon Sep 17 00:00:00 2001 From: Ebezer Igbinoba Date: Wed, 23 Aug 2023 16:36:48 +0100 Subject: [PATCH 34/80] chore: fixed error with typings --- .../AddTagPopoverContent.tsx | 78 ++++++ .../src/components/v2/Checkbox/Checkbox.tsx | 6 +- frontend/src/components/v2/Tag/Tag.tsx | 14 +- frontend/src/const.ts | 36 +-- frontend/src/hooks/api/tags/queries.tsx | 14 +- frontend/src/hooks/api/tags/types.ts | 10 +- .../src/views/DashboardPage/DashboardPage.tsx | 2 + .../DashboardPage/DashboardPage.utils.ts | 3 +- .../CreateTagModal/CreateTagModal.tsx | 238 +++++++++--------- .../DesignTagModal/DesignTagModal.tsx | 138 ---------- .../components/DesignTagModal/index.tsx | 1 - .../SecretInputRow/SecretInputRow.tsx | 194 +++++--------- 12 files changed, 297 insertions(+), 437 deletions(-) create mode 100644 frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx delete mode 100644 frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx delete mode 100644 frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx diff --git a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx new file mode 100644 index 000000000..a8b965269 --- /dev/null +++ b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx @@ -0,0 +1,78 @@ + +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Checkbox, PopoverContent } from "@app/components/v2"; + +import { WsTag } from "../../hooks/api/tags/types"; + +interface Props { + wsTags: WsTag[] | undefined; + secKey: string; + selectedTagIds: Record; + handleSelectTag: (wsTag: WsTag) => void; + handleTagOnMouseEnter: (wsTag: WsTag) => void; + handleTagOnMouseLeave: () => void; + checkIfTagIsVisible: (wsTag: WsTag) => boolean; + handleOnCreateTagOpen: () => void +} + +const AddTagPopoverContent = ({ + wsTags, + secKey, + selectedTagIds, + handleSelectTag, + handleTagOnMouseEnter, + handleTagOnMouseLeave, + checkIfTagIsVisible, + handleOnCreateTagOpen +}: Props) => { + return ( + +
+ Add tags to {secKey || "this secret"} +
+
+
+ {wsTags?.map((wsTag: WsTag) => ( +
handleSelectTag(wsTag)} + onMouseEnter={() => handleTagOnMouseEnter(wsTag)} + onMouseLeave={() => handleTagOnMouseLeave()} + tabIndex={0} role="button" + onKeyDown={() => { }}> + { + + (checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && + } +
+
+ + {wsTag.slug} + +
+
+ ))} +
handleOnCreateTagOpen()} + tabIndex={0} role="button" + onKeyDown={() => { }}> + + Add new tag +
+
+ + ) +} + +export default AddTagPopoverContent \ No newline at end of file diff --git a/frontend/src/components/v2/Checkbox/Checkbox.tsx b/frontend/src/components/v2/Checkbox/Checkbox.tsx index 3c546f067..64ec0a54a 100644 --- a/frontend/src/components/v2/Checkbox/Checkbox.tsx +++ b/frontend/src/components/v2/Checkbox/Checkbox.tsx @@ -8,11 +8,12 @@ export type CheckboxProps = Omit< CheckboxPrimitive.CheckboxProps, "checked" | "disabled" | "required" > & { - children: ReactNode; + children?: ReactNode; id: string; isDisabled?: boolean; isChecked?: boolean; isRequired?: boolean; + checkIndicatorBg?: string | undefined; }; export const Checkbox = ({ @@ -22,6 +23,7 @@ export const Checkbox = ({ isChecked, isDisabled, isRequired, + checkIndicatorBg, ...props }: CheckboxProps): JSX.Element => { return ( @@ -39,7 +41,7 @@ export const Checkbox = ({ {...props} id={id} > - + diff --git a/frontend/src/components/v2/Tag/Tag.tsx b/frontend/src/components/v2/Tag/Tag.tsx index a2de200ef..10c1d6246 100644 --- a/frontend/src/components/v2/Tag/Tag.tsx +++ b/frontend/src/components/v2/Tag/Tag.tsx @@ -1,17 +1,10 @@ import { ReactNode } from "react"; -import { faClose } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { cva, VariantProps } from "cva"; import { twMerge } from "tailwind-merge"; type Props = { children: ReactNode; className?: string; - onClose?: () => void; - color?: string; - styles?: Record - isDisabled?: boolean; - tagColor: string; } & VariantProps; const tagVariants = cva( @@ -34,12 +27,7 @@ export const Tag = ({ children, className, colorSchema = "gray", - color, - isDisabled, - size = "sm", - onClose, - styles = {} -}: Props) => ( + size = "sm" }: Props) => (
diff --git a/frontend/src/const.ts b/frontend/src/const.ts index 7f24f8fc5..267d80ccd 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -55,65 +55,65 @@ export const leaveConfirmDefaultMessage = "Your changes will be lost if you leav export const secretTagsColors = [ { id: 1, - hex: '#bec2c8', + hex: "#bec2c8", rgba: "rgb(128,128,128, 0.8)", - name: 'Grey', + name: "Grey", selected: true }, { id: 2, - hex: '#95a2b3', + hex: "#95a2b3", rgba: "rgb(0,0,255, 0.8)", - name: 'blue', + name: "blue", selected: false }, { id: 3, - hex: '#5e6ad2', + hex: "#5e6ad2", rgba: "rgb(128,0,128, 0.8)", - name: 'Purple', + name: "Purple", selected: false }, { id: 4, - hex: '#26b5ce', + hex: "#26b5ce", rgba: "rgb(0,128,128, 0.8)", - name: 'Teal', + name: "Teal", selected: false }, { id: 5, - hex: '#4cb782', + hex: "#4cb782", rgba: "rgb(0,128,0, 0.8)", - name: 'Green', + name: "Green", selected: false }, { id: 6, - hex: '#f2c94c', + hex: "#f2c94c", rgba: "rgb(255,255,0, 0.8)", - name: 'Yellow', + name: "Yellow", selected: false }, { id: 7, - hex: '#f2994a', + hex: "#f2994a", rgba: "rgb(128,128,0, 0.8)", - name: 'Orange', + name: "Orange", selected: false }, { id: 8, - hex: '#f7c8c1', + hex: "#f7c8c1", rgba: "rgb(128,0,0, 0.8)", - name: 'Pink', + name: "Pink", selected: false }, { id: 9, - hex: '#eb5757', + hex: "#eb5757", rgba: "rgb(255,0,0, 0.8)", - name: 'Red', + name: "Red", selected: false }, ] \ No newline at end of file diff --git a/frontend/src/hooks/api/tags/queries.tsx b/frontend/src/hooks/api/tags/queries.tsx index b0216828c..0b7fb7c17 100644 --- a/frontend/src/hooks/api/tags/queries.tsx +++ b/frontend/src/hooks/api/tags/queries.tsx @@ -7,9 +7,7 @@ import { CreateTagRes, DeleteTagDTO, DeleteWsTagRes, - QueryTag, UserWsTags, - WsTag } from "./types"; const workspaceTags = { @@ -32,33 +30,35 @@ export const useGetWsTags = (workspaceID: string) => { }); } + export const useCreateWsTag = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ workspaceID, tagName, tagColor, tagSlug }: QueryTag) => { + mutationFn: async ({ workspaceID, tagName, tagColor, tagSlug }) => { const { data } = await apiRequest.post(`/api/v2/workspace/${workspaceID}/tags`, { name: tagName, - tagColor: tagColor, + tagColor, slug: tagSlug }) return data; }, - onSuccess: (tagData: WsTag) => { + onSuccess: (tagData) => { queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace)); } }); }; + export const useDeleteWsTag = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ tagID }: {tagID: string}) => { + mutationFn: async ({ tagID }) => { const { data } = await apiRequest.delete(`/api/v2/workspace/tags/${tagID}`); return data }, - onSuccess: (tagData: WsTag) => { + onSuccess: (tagData) => { queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace)); } }); diff --git a/frontend/src/hooks/api/tags/types.ts b/frontend/src/hooks/api/tags/types.ts index db162415e..de09f5ac4 100644 --- a/frontend/src/hooks/api/tags/types.ts +++ b/frontend/src/hooks/api/tags/types.ts @@ -4,6 +4,7 @@ export type WsTag = { _id: string; name: string; slug: string; + tagColor?: string; workspace: string; createdAt: string; updatedAt: string; @@ -16,6 +17,7 @@ export type CreateTagDTO = { workspaceID: string; tagSlug: string; tagName: string; + tagColor: string; }; export type CreateTagRes = { @@ -23,6 +25,7 @@ export type CreateTagRes = { slug: string; workspace: string; createdAt: string; + tagColor?: string; user: string; _id: string; }; @@ -51,11 +54,4 @@ export type TagColor = { rgba: string name: string selected: boolean -} - -export type QueryTag = { - workspaceID: string; - tagName: string; - tagColor: string; - tagSlug: string } \ No newline at end of file diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index 4c84aab84..8c9706c24 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -297,6 +297,8 @@ export const DashboardPage = () => { resolver: yupResolver(schema) }); + console.log("300 => secrets", secrets) + const { register, control, diff --git a/frontend/src/views/DashboardPage/DashboardPage.utils.ts b/frontend/src/views/DashboardPage/DashboardPage.utils.ts index a8e63f89f..bd0131e8e 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.utils.ts +++ b/frontend/src/views/DashboardPage/DashboardPage.utils.ts @@ -58,7 +58,8 @@ const secretSchema = yup.object({ yup.object({ _id: yup.string().required(), name: yup.string().required(), - slug: yup.string().required() + slug: yup.string().required(), + tagColor: yup.string().nullable(), }) ), overrideAction: yup.string().notRequired().oneOf(Object.values(SecretActionType)), diff --git a/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx index c378bf54c..e725249ed 100644 --- a/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx +++ b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx @@ -1,17 +1,17 @@ -import { useState, useEffect } from 'react'; +import { useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; -import { secretTagsColors } from "~/const" import { faCheck } 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, FormControl, Input, ModalClose, Tooltip } from "@app/components/v2"; -import { isValidHexColor } from "~/components/utilities/isValidHexColor"; -import { TagColor } from '~/hooks/api/tags/types'; + +import { isValidHexColor } from "../../../../components/utilities/isValidHexColor"; +import { secretTagsColors } from "../../../../const" +import { TagColor } from "../../../../hooks/api/tags/types"; type Props = { @@ -33,8 +33,8 @@ export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => { resolver: yupResolver(createTagSchema) }); - const [tagsColors, setTagsColors] = useState(secretTagsColors) - const [selectedTagColor, setSelectedTagColor] = useState({}) + const [tagsColors] = useState(secretTagsColors) + const [selectedTagColor, setSelectedTagColor] = useState(tagsColors[0]) const [showHexInput, setShowHexInput] = useState(false) const [tagColor, setTagColor] = useState("") @@ -46,12 +46,11 @@ export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => { useEffect(() => { const clonedTagColors = [...tagsColors] - for (const tagColor of clonedTagColors) { - if (tagColor.selected) { - setSelectedTagColor(tagColor) - setTagColor(tagColor.hex) - break - } + const selectedTagBgColor = clonedTagColors.find($tagColor => $tagColor.selected); + + if (selectedTagBgColor) { + setSelectedTagColor(selectedTagBgColor); + setTagColor(selectedTagBgColor.hex); } }, []) @@ -60,124 +59,133 @@ export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => { const tagsHexWrapper = document.querySelector(".tags-hex-wrapper") if (showHexInput) { - tagsList?.classList.add('hide-tags') - tagsList?.classList.remove('show-tags') - tagsHexWrapper?.classList.add('show-hex-input') - tagsHexWrapper?.classList.remove('hide-hex-input') + tagsList?.classList.add("hide-tags") + tagsList?.classList.remove("show-tags") + tagsHexWrapper?.classList.add("show-hex-input") + tagsHexWrapper?.classList.remove("hide-hex-input") } else { - tagsList?.classList.remove('hide-tags') - tagsList?.classList.add('show-tags') - tagsHexWrapper?.classList.remove('show-hex-input') - tagsHexWrapper?.classList.add('hide-hex-input') + tagsList?.classList.remove("hide-tags") + tagsList?.classList.add("show-tags") + tagsHexWrapper?.classList.remove("show-hex-input") + tagsHexWrapper?.classList.add("hide-hex-input") } }, [showHexInput]) - const handleColorChange = (tagColor: TagColor) => { - const clonedTagColors = [...tagsColors] - const tagColorIndex = clonedTagColors.findIndex(_tagColor => _tagColor.id === tagColor.id) - const _selectedTagColor = clonedTagColors[tagColorIndex] - clonedTagColors.forEach(tagColor => { - tagColor.selected = false - }) - if (selectedTagColor.id !== tagColor.id) { - _selectedTagColor.selected = !_selectedTagColor.selected - setSelectedTagColor(_selectedTagColor) - setTagColor(_selectedTagColor.hex) + + const handleColorChange = (clickedTagColor: TagColor) => { + const updatedTagColors = [...tagsColors]; + const clickedTagColorIndex = updatedTagColors.findIndex(($tagColor) => $tagColor.id === clickedTagColor.id); + const updatedClickedTagColor = updatedTagColors[clickedTagColorIndex]; + + updatedTagColors.forEach((tgColor) => { + // eslint-disable-next-line no-param-reassign + tgColor.selected = false; + }); + + if (selectedTagColor.id !== clickedTagColor.id) { + updatedClickedTagColor.selected = !updatedClickedTagColor.selected; + setSelectedTagColor(updatedClickedTagColor); + setTagColor(updatedClickedTagColor.hex); } - } + }; return ( - <> -
- ( - - - - )} - /> + + ( + + + + )} + /> -
- -
-
-
+
+
Tag color
+
+
+
+
+ +
+
+ { + tagsColors.map(($tagColor: TagColor) => { + return ( +
+ +
handleColorChange($tagColor)} + tabIndex={0} role="button" + onKeyDown={() => { }} + > + { + $tagColor.selected && + } +
+
+
+ ) + }) + }
-
- { - ( -
- { - tagsColors.map((tagColor: TagColor) => { - return ( - -
handleColorChange(tagColor)}> - { - tagColor.selected && - } -
-
- ) - }) - } -
- ) - } +
+
+ { + isValidHexColor(tagColor) && ( +
+ +
+ ) + } -
-
- { - isValidHexColor(tagColor) && ( -
- -
- ) - } - - { - !isValidHexColor(tagColor) && ( -
- ) - } -
-
- ) => setTagColor(e.target.value)} - /> -
+ { + !isValidHexColor(tagColor) && ( +
+ ) + }
+
+ ) => setTagColor(e.target.value)} + /> +
+
-
-
-
setShowHexInput((prev) => !prev)} style={{ border: '1px solid rgba(220, 216, 254, 0.376)' }}> - { - !showHexInput && # - } -
+
+
+
setShowHexInput((prev) => !prev)} style={{ border: "1px solid rgba(220, 216, 254, 0.376)" }} + tabIndex={0} role="button" + onKeyDown={() => { }}> + { + !showHexInput && # + }
+
-
- + + - - - -
- - + +
+ ); }; diff --git a/frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx b/frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx deleted file mode 100644 index 6993471e9..000000000 --- a/frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { Controller, useForm } from "react-hook-form"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; - -import { Button, FormControl, Input, ModalClose, Tooltip, IconButton, Tag } from "@app/components/v2"; -import { useToggle } from "@app/hooks"; -import { TagDesign } from "~/hooks/api/tags/types"; - -import { - faEye, - faEyeSlash -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useState } from 'react'; -import { WsTag } from '../../../../hooks/api/tags/types'; - -type TagData = { - tagBackground: string; - tagLabel: string -} - -type Props = { - onDesignTag: (tagData: TagData) => void; - selectedTag: WsTag -}; - -const designTagSchema = yup.object({ - tagBackground: yup.string().required().trim().label("Tag Background"), - tagLabel: yup.string().required().trim().label("Tag Label"), -}); -type FormData = yup.InferType; - - -export const DesignTagModal = ({ onDesignTag, selectedTag }: Props): JSX.Element => { - const [tagDesignObj, setTagDesignObj] = useState({ - tagColor: { - bg: "", - text: "" - } - }) - - const { - control, - reset, - formState, - handleSubmit, - setValue - } = useForm({ - resolver: yupResolver(designTagSchema) - }); - - const onFormSubmit = ({ tagBackground, tagLabel }: FormData) => { - onDesignTag({ tagBackground, tagLabel }); - reset(); - }; - - const [previewTag, setPreviewTag] = useToggle(false); - - const handleInputChange = (e: React.ChangeEvent, type: string) => { - setTagDesignObj((prev: { tagColor: { bg: string, text: string }; }) => ({ - tagColor: { - ...prev.tagColor, - [type]: e.target.value - } - })) - if (type === 'bg') { - setValue('tagBackground', e.target.value) - } else { - setValue('tagLabel', e.target.value) - } - } - - return ( -
-
- { - return ( - <> - - ) => handleInputChange(e, 'bg')} /> - - - ) - }} - /> - {/* */} -
- setPreviewTag.toggle()} className="absolute top-[2px] left-[127px] cursor-pointer" /> - {previewTag && ( - void (0)} - key={selectedTag._id} - className="absolute top-[-5px] right-[-5px] cursor-pointer" - > - {selectedTag.slug} - - )} - -
- {/*
*/} -
- - { - return ( - <> - - ) => handleInputChange(e, 'text')} value={tagDesignObj.tagColor.text} /> - - - ) - }} - /> -
- - - - -
- - ); -}; diff --git a/frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx b/frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx deleted file mode 100644 index 165e8e3a3..000000000 --- a/frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export {DesignTagModal} from "./DesignTagModal" \ No newline at end of file diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index 9b70b936e..cdb95864a 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-no-useless-fragment */ -import { memo, useEffect, useRef, useState } from "react"; +import { memo, useEffect,useRef, useState } from "react"; import { Control, Controller, @@ -15,7 +15,6 @@ import { faCopy, faEllipsis, faInfoCircle, - faPlus, faTags, faXmark } from "@fortawesome/free-solid-svg-icons"; @@ -24,41 +23,21 @@ import { cx } from "cva"; import { twMerge } from "tailwind-merge"; import { - Button, - Checkbox, - FormControl, HoverCard, HoverCardContent, HoverCardTrigger, IconButton, Input, Popover, - PopoverContent, PopoverTrigger, SecretInput, Tag, - TextArea, - Tooltip, - Modal, - ModalContent, -} from "@app/components/v2"; - + Tooltip} from "@app/components/v2"; +import { useToggle } from "@app/hooks"; import { WsTag } from "@app/hooks/api/types"; +import AddTagPopoverContent from "../../../../components/AddTagPopoverContent/AddTagPopoverContent"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; -import { SecretTags } from "~/hooks/api/tags/types"; -import { useToggle } from "@app/hooks"; - -const tagColors = [ - { bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" }, - { bg: "bg-[#cb1c8d]/40", text: "text-[#f2c6e3]/70" }, - { bg: "bg-[#badc58]/40", text: "text-[#eef6d5]/70" }, - { bg: "bg-[#ff5400]/40", text: "text-[#ffddcc]/70" }, - { bg: "bg-[#3AB0FF]/40", text: "text-[#f0fffd]/70" }, - { bg: "bg-[#6F1AB6]/40", text: "text-[#FFE5F1]/70" }, - { bg: "bg-[#C40B13]/40", text: "text-[#FFDEDE]/70" }, - { bg: "bg-[#332FD0]/40", text: "text-[#DFF6FF]/70" } -]; type Props = { index: number; @@ -77,7 +56,6 @@ type Props = { // tag props wsTags?: WsTag[]; onCreateTagOpen: () => void; - onDesignTagOpen: (selectedTag: WsTag, selectedFieldIndex: number) => void; // rhf specific functions, dont put this using useFormContext. This is passed as props to avoid re-rendering control: Control; register: UseFormRegister; @@ -97,11 +75,10 @@ export const SecretInputRow = memo( isAddOnly, wsTags, onCreateTagOpen, - onDesignTagOpen, onSecretDelete, searchTerm, control, - register, + // register, setValue, isKeyError, keyError, @@ -116,8 +93,6 @@ export const SecretInputRow = memo( append } = useFieldArray({ control, name: `secrets.${index}.tags` }); - const tagColorByTagId = new Map((wsTags || []).map((wsTag, i) => [wsTag._id, tagColors[i % tagColors.length]])) - // display the tags in alphabetical order secretTags.sort((a, b) => a?.name?.localeCompare(b?.name)) @@ -152,7 +127,19 @@ export const SecretInputRow = memo( const isOverridden = overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; + const [editorRef, setEditorRef] = useState(isOverridden ? secValueOverride : secValue); + const [hoveredTag, setHoveredTag] = useState(null); + + const handleTagOnMouseEnter = (wsTag: WsTag) => { + setHoveredTag(wsTag); + } + + const handleTagOnMouseLeave = () => { + setHoveredTag(null); + } + + const checkIfTagIsVisible = (wsTag: WsTag) => wsTag._id === hoveredTag?._id; const secId = useWatch({ control, name: `secrets.${index}._id`, exact: true }); const tags = useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || []; @@ -164,6 +151,7 @@ export const SecretInputRow = memo( const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false); + useEffect(() => { let timer: NodeJS.Timeout; if (isInviteLinkCopied) { @@ -172,6 +160,7 @@ export const SecretInputRow = memo( return () => clearTimeout(timer); }, [isInviteLinkCopied]); + useEffect(() => { setEditorRef(isOverridden ? secValueOverride : secValue); }, [isOverridden]); @@ -202,7 +191,8 @@ export const SecretInputRow = memo( const onSelectTag = (selectedTag: WsTag) => { const shouldAppend = !selectedTagIds[selectedTag.slug]; if (shouldAppend) { - append(selectedTag); + const {_id: id, name, slug, tagColor} = selectedTag + append({_id: id, name, slug, tagColor}); } else { const pos = tags.findIndex(({ slug }: { slug: string }) => selectedTag.slug === slug); remove(pos); @@ -229,8 +219,6 @@ export const SecretInputRow = memo( return <>; } - - return ( @@ -335,67 +323,37 @@ export const SecretInputRow = memo(
- {secretTags.map(({ id, _id, slug, tagColor }: SecretTags, i: number) => { + {secretTags.map(({ id, slug, tagColor}) => { return ( - + <> +
remove(i)} + // isDisabled={isReadOnly || isAddOnly || isRollbackMode} + // onClose={() => remove(i)} key={id} className="cursor-pointer" > -
-
+
+
{slug}
-
- -
- Add tags to {secKey || "this secret"} -
-
- {wsTags?.map((wsTag) => ( - - ))} - -
-
+ onSelectTag(wsTag)} + handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)} + handleTagOnMouseLeave={() => handleTagOnMouseLeave()} + checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)} + handleOnCreateTagOpen={() => onCreateTagOpen()} + /> - + ) })}
@@ -428,46 +386,16 @@ export const SecretInputRow = memo(
- -
- Add tags to {secKey || "this secret"} -
-
- {wsTags?.map((wsTag) => ( - - ))} - -
-
+ onSelectTag(wsTag)} + handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)} + handleTagOnMouseLeave={() => handleTagOnMouseLeave()} + checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)} + handleOnCreateTagOpen={() => onCreateTagOpen()} + />
)} @@ -511,20 +439,16 @@ export const SecretInputRow = memo( - - -