diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index c066bf793..a8d2bae85 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -10,7 +10,10 @@ import { clearTokens } from '../../helpers'; import { TokenService } from '../../services'; -import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables'; +import { + TOKEN_EMAIL_PASSWORD_RESET, + AUTH_MODE_JWT +} from '../../variables'; import { BadRequestError } from '../../utils/errors'; import { getSiteURL, @@ -231,7 +234,9 @@ export const changePassword = async (req: Request, res: Response) => { } ); - await clearTokens(user._id); + if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) { + await clearTokens(req.authData.tokenVersionId) + } // clear httpOnly cookie diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 314e1e13a..4c9ef4048 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -2,7 +2,7 @@ import crypto from 'crypto'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import jsrp from 'jsrp'; import nacl from 'tweetnacl'; @@ -13,7 +13,7 @@ import getOrganizations from '@app/pages/api/organization/getOrgs'; import ProjectService from '@app/services/ProjectService'; import InputField from '../basic/InputField'; -import passwordCheck from '../utilities/checks/PasswordCheck'; +import checkPassword from '../utilities/checks/checkPassword'; import Aes256Gcm from '../utilities/cryptography/aes-256-gcm'; import { deriveArgonKey } from '../utilities/cryptography/crypto'; import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage'; @@ -37,6 +37,15 @@ interface UserInfoStepProps { providerAuthToken?: string; } +type Errors = { + length?: string, + upperCase?: string, + lowerCase?: string, + number?: string, + specialChar?: string, + repeatedChar?: string, +}; + /** * This is the step of the sign up flow where people provife their name/surname and password * @param {object} obj @@ -69,6 +78,8 @@ export default function UserInfoStep({ const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); + const [errors, setErrors] = useState({}); + const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); @@ -89,12 +100,10 @@ export default function UserInfoStep({ } else { setOrganizationNameError(false); } - errorCheck = passwordCheck({ + + errorCheck = checkPassword({ password, - setPasswordErrorLength, - setPasswordErrorNumber, - setPasswordErrorLowerCase, - errorCheck + setErrors }); if (!errorCheck) { @@ -248,12 +257,9 @@ export default function UserInfoStep({ label={t('section.password.password')} onChangeHandler={(pass: string) => { setPassword(pass); - passwordCheck({ + checkPassword({ password: pass, - setPasswordErrorLength, - setPasswordErrorNumber, - setPasswordErrorLowerCase, - errorCheck: false + setErrors }); }} type="password" @@ -263,44 +269,29 @@ export default function UserInfoStep({ autoComplete="new-password" id="new-password" /> - {passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( + {Object.keys(errors).length > 0 && (
-
{t('section.password.validate-base')}
-
- {passwordErrorLength ? ( - - ) : ( - - )} -
- {t('section.password.validate-length')} -
-
-
- {passwordErrorLowerCase ? ( - - ) : ( - - )} -
- {t('section.password.validate-case')} -
-
-
- {passwordErrorNumber ? ( - - ) : ( - - )} -
- {t('section.password.validate-number')} -
-
+
{t('section.password.validate-base')}
+ {Object.keys(errors).map((key) => { + if (errors[key as keyof Errors]) { + return ( +
+
+ +
+

+ {errors[key as keyof Errors]} +

+
+ ); + } + + return null; + })}
- ) : ( -
)}
diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts index e84cba761..5fb9dfe2c 100644 --- a/frontend/src/components/utilities/checks/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/PasswordCheck.ts @@ -17,6 +17,7 @@ const passwordCheck = ({ setPasswordErrorLowerCase, errorCheck }: PasswordCheckProps) => { + if (!password || password.length < 14) { setPasswordErrorLength(true); errorCheck = true; diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts new file mode 100644 index 000000000..e4c5ccbbb --- /dev/null +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -0,0 +1,65 @@ +type Errors = { + length?: string, + upperCase?: string, + lowerCase?: string, + number?: string, + specialChar?: string, + repeatedChar?: string, + }; + +interface CheckPasswordParams { + password: string; + setErrors: (value: Errors) => void; +} + +/** + * 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) + * - 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 + * 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 = ({ + password, + setErrors +}: CheckPasswordParams): boolean => { + let errors: Errors = {}; + + if (password.length < 8) { + errors.length = "8 characters"; + } + + if (!/[A-Z]/.test(password)) { + errors.upperCase = "1 uppercase character (A-Z)"; + } + + if (!/[a-z]/.test(password)) { + errors.lowerCase = "1 lowercase character (a-z)"; + } + + if (!/[0-9]/.test(password)) { + errors.number = "1 number (0-9)"; + } + + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + errors.specialChar = "1 special character (!@#$%^&*(),.?)"; + } + + if (/([A-Za-z0-9])\1\1\1/.test(password)) { + errors.repeatedChar = "No 3 repeat, consecutive characters"; + } + + setErrors(errors); + return Object.keys(errors).length > 0; +} + +export default checkPassword; \ No newline at end of file diff --git a/frontend/src/pages/settings/personal/[id].tsx b/frontend/src/pages/settings/personal/[id].tsx index a1b7703f2..c7f7296cd 100644 --- a/frontend/src/pages/settings/personal/[id].tsx +++ b/frontend/src/pages/settings/personal/[id].tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import Head from 'next/head'; import { useRouter } from 'next/router'; -import { faCheck, faPlus, faX, faBan } from '@fortawesome/free-solid-svg-icons'; +import { faCheck, faPlus, faXmark, faBan } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Button from '@app/components/basic/buttons/Button'; @@ -10,7 +10,7 @@ import InputField from '@app/components/basic/InputField'; import ListBox from '@app/components/basic/Listbox'; import ApiKeyTable from '@app/components/basic/table/ApiKeyTable'; import NavHeader from '@app/components/navigation/NavHeader'; -import passwordCheck from '@app/components/utilities/checks/PasswordCheck'; +import checkPassword from '@app/components/utilities/checks/checkPassword'; import changePassword from '@app/components/utilities/cryptography/changePassword'; import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey'; import { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection'; @@ -22,12 +22,18 @@ import { useRevokeAllSessions } from '@app/hooks/api'; +type Errors = { + length?: string, + upperCase?: string, + lowerCase?: string, + number?: string, + specialChar?: string, + repeatedChar?: string, +}; + export default function PersonalSettings() { const [personalEmail, setPersonalEmail] = useState(''); const [personalName, setPersonalName] = useState(''); - const [passwordErrorLength, setPasswordErrorLength] = useState(false); - const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); - const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); const [currentPasswordError, setCurrentPasswordError] = useState(false); const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); @@ -37,6 +43,7 @@ export default function PersonalSettings() { const [backupKeyError, setBackupKeyError] = useState(false); const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false); const [apiKeys, setApiKeys] = useState([]); + const [errors, setErrors] = useState({}); const revokeAllSessions = useRevokeAllSessions(); @@ -159,78 +166,52 @@ export default function PersonalSettings() { label={t('section.password.new') as string} onChangeHandler={(password) => { setNewPassword(password); - passwordCheck({ + checkPassword({ password, - setPasswordErrorLength, - setPasswordErrorNumber, - setPasswordErrorLowerCase, - errorCheck: false + setErrors }); }} type="password" value={newPassword} isRequired - error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber} + error={Object.keys(errors).length > 0} autoComplete="new-password" id="new-password" />
- {passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( -
-
- {t('section.password.validate-base')} -
-
- {passwordErrorLength ? ( - - ) : ( - - )} -
- {t('section.password.validate-length')} -
-
-
- {passwordErrorLowerCase ? ( - - ) : ( - - )} -
- {t('section.password.validate-case')} -
-
-
- {passwordErrorNumber ? ( - - ) : ( - - )} -
- {t('section.password.validate-number')} -
-
+ {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; + })}
- ) : ( -
)}