diff --git a/webapp/channels/src/components/admin_console/password_settings.tsx b/webapp/channels/src/components/admin_console/password_settings.tsx index 4c30afa13e..b2ceecd1fd 100644 --- a/webapp/channels/src/components/admin_console/password_settings.tsx +++ b/webapp/channels/src/components/admin_console/password_settings.tsx @@ -9,6 +9,7 @@ import type {AdminConfig} from '@mattermost/types/config'; import type {DeepPartial} from '@mattermost/types/utilities'; import Constants from 'utils/constants'; +import {passwordErrors} from 'utils/password'; import AdminSettings from './admin_settings'; import type {BaseProps, BaseState} from './admin_settings'; @@ -63,25 +64,6 @@ export const searchableStrings: Array; + passwordConfig?: PasswordConfig; } const OAuthToEmail = (props: Props) => { diff --git a/webapp/channels/src/components/claim/index.ts b/webapp/channels/src/components/claim/index.ts index b692da6087..b5db96ef5e 100644 --- a/webapp/channels/src/components/claim/index.ts +++ b/webapp/channels/src/components/claim/index.ts @@ -8,9 +8,7 @@ import type {Dispatch} from 'redux'; import type {GlobalState} from '@mattermost/types/store'; import {switchLdapToEmail} from 'mattermost-redux/actions/users'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; - -import {getPasswordConfig} from 'utils/utils'; +import {getConfig, getPasswordConfig} from 'mattermost-redux/selectors/entities/general'; import ClaimController from './claim_controller'; @@ -22,7 +20,7 @@ function mapStateToProps(state: GlobalState) { return { siteName, ldapLoginFieldName, - passwordConfig: getPasswordConfig(config), + passwordConfig: getPasswordConfig(state), }; } diff --git a/webapp/channels/src/components/signup/signup.tsx b/webapp/channels/src/components/signup/signup.tsx index 78d13ecff0..f78336292b 100644 --- a/webapp/channels/src/components/signup/signup.tsx +++ b/webapp/channels/src/components/signup/signup.tsx @@ -15,7 +15,7 @@ import type {UserProfile} from '@mattermost/types/users'; import {getTeamInviteInfo} from 'mattermost-redux/actions/teams'; import {createUser, loadMe} from 'mattermost-redux/actions/users'; import {Client4} from 'mattermost-redux/client'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {getConfig, getLicense, getPasswordConfig} from 'mattermost-redux/selectors/entities/general'; import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {isEmail} from 'mattermost-redux/utils/helpers'; @@ -54,8 +54,9 @@ import type {CustomMessageInputType} from 'components/widgets/inputs/input/input import PasswordInput from 'components/widgets/inputs/password_input/password_input'; import {Constants, HostedCustomerLinks, ItemStatus, ValidationErrors} from 'utils/constants'; +import {isValidPassword} from 'utils/password'; import {isDesktopApp} from 'utils/user_agent'; -import {isValidUsername, isValidPassword, getPasswordConfig, getRoleFromTrackFlow, getMediumFromTrackFlow} from 'utils/utils'; +import {isValidUsername, getRoleFromTrackFlow, getMediumFromTrackFlow} from 'utils/utils'; import type {GlobalState} from 'types/store'; @@ -148,7 +149,8 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableLDAP || enableSAML; const hasError = Boolean(emailError || nameError || passwordError || serverError || alertBanner); const canSubmit = Boolean(email && name && password) && !hasError && !loading; - const {error: passwordInfo} = isValidPassword('', getPasswordConfig(config), intl); + const passwordConfig = useSelector(getPasswordConfig); + const {error: passwordInfo} = isValidPassword('', passwordConfig, intl); const [desktopLoginLink, setDesktopLoginLink] = useState(''); @@ -553,7 +555,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { } const providedPassword = passwordInput.current?.value ?? ''; - const {error, telemetryErrorIds} = isValidPassword(providedPassword, getPasswordConfig(config), intl); + const {error, telemetryErrorIds} = isValidPassword(providedPassword, passwordConfig, intl); if (error) { setPasswordError(error as string); diff --git a/webapp/channels/src/components/user_settings/security/index.ts b/webapp/channels/src/components/user_settings/security/index.ts index 649ee065d8..84cf02913a 100644 --- a/webapp/channels/src/components/user_settings/security/index.ts +++ b/webapp/channels/src/components/user_settings/security/index.ts @@ -10,12 +10,11 @@ import type {UserProfile} from '@mattermost/types/users'; import {getAuthorizedOAuthApps, deauthorizeOAuthApp} from 'mattermost-redux/actions/integrations'; import {getMe, updateUserPassword} from 'mattermost-redux/actions/users'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {getConfig, getPasswordConfig} from 'mattermost-redux/selectors/entities/general'; import {getBool} from 'mattermost-redux/selectors/entities/preferences'; import * as UserUtils from 'mattermost-redux/utils/user_utils'; import {Preferences} from 'utils/constants'; -import {getPasswordConfig} from 'utils/utils'; import SecurityTab from './user_settings_security'; @@ -55,7 +54,7 @@ function mapStateToProps(state: GlobalState, ownProps: Props) { enableSaml, enableSignUpWithOffice365, experimentalEnableAuthenticationTransfer, - passwordConfig: getPasswordConfig(config), + passwordConfig: getPasswordConfig(state), militaryTime: getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false), }; } diff --git a/webapp/channels/src/components/user_settings/security/user_settings_security.test.tsx b/webapp/channels/src/components/user_settings/security/user_settings_security.test.tsx index 1095648421..6af87fc03e 100644 --- a/webapp/channels/src/components/user_settings/security/user_settings_security.test.tsx +++ b/webapp/channels/src/components/user_settings/security/user_settings_security.test.tsx @@ -7,14 +7,15 @@ import React from 'react'; import type {OAuthApp} from '@mattermost/types/integrations'; import type {UserProfile} from '@mattermost/types/users'; +import type {PasswordConfig} from 'mattermost-redux/selectors/entities/general'; + import type {MockIntl} from 'tests/helpers/intl-test-helper'; import Constants from 'utils/constants'; -import type * as Utils from 'utils/utils'; import {SecurityTab} from './user_settings_security'; -jest.mock('utils/utils', () => { - const original = jest.requireActual('utils/utils'); +jest.mock('utils/password', () => { + const original = jest.requireActual('utils/password'); return {...original, isValidPassword: () => ({valid: true})}; }); @@ -46,7 +47,7 @@ describe('components/user_settings/display/UserSettingsDisplay', () => { enableSaml: true, enableSignUpWithOffice365: false, experimentalEnableAuthenticationTransfer: true, - passwordConfig: {} as ReturnType, + passwordConfig: {} as PasswordConfig, militaryTime: false, intl: { formatMessage: jest.fn(({id, defaultMessage}) => defaultMessage || id), diff --git a/webapp/channels/src/components/user_settings/security/user_settings_security.tsx b/webapp/channels/src/components/user_settings/security/user_settings_security.tsx index 88ad8d1720..017dab485b 100644 --- a/webapp/channels/src/components/user_settings/security/user_settings_security.tsx +++ b/webapp/channels/src/components/user_settings/security/user_settings_security.tsx @@ -11,6 +11,7 @@ import {Link} from 'react-router-dom'; import type {OAuthApp} from '@mattermost/types/integrations'; import type {UserProfile} from '@mattermost/types/users'; +import type {PasswordConfig} from 'mattermost-redux/selectors/entities/general'; import type {ActionResult} from 'mattermost-redux/types/actions'; import AccessHistoryModal from 'components/access_history_modal'; @@ -22,7 +23,7 @@ import ToggleModalButton from 'components/toggle_modal_button'; import icon50 from 'images/icon50x50.png'; import Constants from 'utils/constants'; -import * as Utils from 'utils/utils'; +import {isValidPassword} from 'utils/password'; import MfaSection from './mfa_section'; import UserAccessTokenSection from './user_access_token_section'; @@ -64,7 +65,7 @@ type Props = { enableSaml: boolean; enableSignUpWithOffice365: boolean; experimentalEnableAuthenticationTransfer: boolean; - passwordConfig: ReturnType; + passwordConfig: PasswordConfig; militaryTime: boolean; actions: Actions; intl: IntlShape; @@ -135,7 +136,7 @@ export class SecurityTab extends React.PureComponent { return; } - const {valid, error} = Utils.isValidPassword( + const {valid, error} = isValidPassword( newPassword, this.props.passwordConfig, ); diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/general.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/general.ts index 9b8358eb59..171c4e1bb0 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/general.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/general.ts @@ -21,6 +21,28 @@ export function getFeatureFlagValue(state: GlobalState, key: keyof FeatureFlags) return getConfig(state)?.[`FeatureFlag${key}` as keyof Partial]; } +export type PasswordConfig = { + minimumLength: number; + requireLowercase: boolean; + requireUppercase: boolean; + requireNumber: boolean; + requireSymbol: boolean; +}; + +export const getPasswordConfig: (state: GlobalState) => PasswordConfig = createSelector( + 'getPasswordConfig', + getConfig, + (config) => { + return { + minimumLength: parseInt(config.PasswordMinimumLength!, 10), + requireLowercase: config.PasswordRequireLowercase === 'true', + requireUppercase: config.PasswordRequireUppercase === 'true', + requireNumber: config.PasswordRequireNumber === 'true', + requireSymbol: config.PasswordRequireSymbol === 'true', + }; + }, +); + export function getLicense(state: GlobalState): ClientLicense { return state.entities.general.license; } diff --git a/webapp/channels/src/utils/password.test.tsx b/webapp/channels/src/utils/password.test.tsx new file mode 100644 index 0000000000..4f490e2d89 --- /dev/null +++ b/webapp/channels/src/utils/password.test.tsx @@ -0,0 +1,197 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {isValidPassword} from './password'; + +describe('isValidPassword', () => { + test('Minimum length enforced', () => { + for (const data of [ + { + password: 'tooshort', + config: { + minimumLength: 10, + requireLowercase: false, + requireUppercase: false, + requireNumber: false, + requireSymbol: false, + }, + valid: false, + }, + { + password: 'longenoughpassword', + config: { + minimumLength: 10, + requireLowercase: false, + requireUppercase: false, + requireNumber: false, + requireSymbol: false, + }, + valid: true, + }, + ]) { + const {valid} = isValidPassword(data.password, data.config); + expect(data.valid).toEqual(valid); + } + }); + + test('Maximum length enforced', () => { + for (const data of [ + { + password: 'justright', + config: { + minimumLength: 8, + requireLowercase: false, + requireUppercase: false, + requireNumber: false, + requireSymbol: false, + }, + valid: true, + }, + { + password: 'iamaverylongstringthathas72charactersandwillpasswithoutanyissuesthiscall', + config: { + minimumLength: 8, + requireLowercase: false, + requireUppercase: false, + requireNumber: false, + requireSymbol: false, + }, + valid: true, + }, + { + password: 'iamaverylongstringthathas73charactersandwontpassthisvalidationatall!!!:-(', + config: { + minimumLength: 8, + requireLowercase: false, + requireUppercase: false, + requireNumber: false, + requireSymbol: false, + }, + valid: false, + }, + ]) { + const {valid} = isValidPassword(data.password, data.config); + expect(data.valid).toEqual(valid); + } + }); + + test('Require lowercase enforced', () => { + for (const data of [ + { + password: 'UPPERCASE', + config: { + minimumLength: 5, + requireLowercase: true, + requireUppercase: false, + requireNumber: false, + requireSymbol: false, + }, + valid: false, + }, + { + password: 'SOMELowercase', + config: { + minimumLength: 5, + requireLowercase: true, + requireUppercase: false, + requireNumber: false, + requireSymbol: false, + }, + valid: true, + }, + ]) { + const {valid} = isValidPassword(data.password, data.config); + expect(data.valid).toEqual(valid); + } + }); + + test('Require uppercase enforced', () => { + for (const data of [ + { + password: 'lowercase', + config: { + minimumLength: 5, + requireLowercase: false, + requireUppercase: true, + requireNumber: false, + requireSymbol: false, + }, + valid: false, + }, + { + password: 'SOMEUppercase', + config: { + minimumLength: 5, + requireLowercase: false, + requireUppercase: true, + requireNumber: false, + requireSymbol: false, + }, + valid: true, + }, + ]) { + const {valid} = isValidPassword(data.password, data.config); + expect(data.valid).toEqual(valid); + } + }); + + test('Require number enforced', () => { + for (const data of [ + { + password: 'NoNumbers', + config: { + minimumLength: 5, + requireLowercase: true, + requireUppercase: true, + requireNumber: true, + requireSymbol: false, + }, + valid: false, + }, + { + password: 'S0m3Numb3rs', + config: { + minimumLength: 5, + requireLowercase: true, + requireUppercase: true, + requireNumber: true, + requireSymbol: false, + }, + valid: true, + }, + ]) { + const {valid} = isValidPassword(data.password, data.config); + expect(data.valid).toEqual(valid); + } + }); + + test('Require symbol enforced', () => { + for (const data of [ + { + password: 'N0Symb0ls', + config: { + minimumLength: 5, + requireLowercase: true, + requireUppercase: true, + requireNumber: true, + requireSymbol: true, + }, + valid: false, + }, + { + password: 'S0m3Symb0!s', + config: { + minimumLength: 5, + requireLowercase: true, + requireUppercase: true, + requireNumber: true, + requireSymbol: true, + }, + valid: true, + }, + ]) { + const {valid} = isValidPassword(data.password, data.config); + expect(data.valid).toEqual(valid); + } + }); +}); diff --git a/webapp/channels/src/utils/password.tsx b/webapp/channels/src/utils/password.tsx new file mode 100644 index 0000000000..b7e53389cb --- /dev/null +++ b/webapp/channels/src/utils/password.tsx @@ -0,0 +1,104 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import type {IntlShape} from 'react-intl'; +import {FormattedMessage, defineMessages} from 'react-intl'; + +import type {PasswordConfig} from 'mattermost-redux/selectors/entities/general'; + +import Constants from 'utils/constants'; + +export function isValidPassword(password: string, passwordConfig: PasswordConfig, intl?: IntlShape) { + let errorId = passwordErrors.passwordError.id; + const telemetryErrorIds = []; + let valid = true; + const minimumLength = passwordConfig.minimumLength || Constants.MIN_PASSWORD_LENGTH; + + if (password.length < minimumLength || password.length > Constants.MAX_PASSWORD_LENGTH) { + valid = false; + telemetryErrorIds.push({field: 'password', rule: 'error_length'}); + } + + if (passwordConfig.requireLowercase) { + if (!password.match(/[a-z]/)) { + valid = false; + } + + errorId += 'Lowercase'; + telemetryErrorIds.push({field: 'password', rule: 'lowercase'}); + } + + if (passwordConfig.requireUppercase) { + if (!password.match(/[A-Z]/)) { + valid = false; + } + + errorId += 'Uppercase'; + telemetryErrorIds.push({field: 'password', rule: 'uppercase'}); + } + + if (passwordConfig.requireNumber) { + if (!password.match(/[0-9]/)) { + valid = false; + } + + errorId += 'Number'; + telemetryErrorIds.push({field: 'password', rule: 'number'}); + } + + if (passwordConfig.requireSymbol) { + if (!password.match(/[ !"\\#$%&'()*+,-./:;<=>?@[\]^_`|~]/)) { + valid = false; + } + + errorId += 'Symbol'; + telemetryErrorIds.push({field: 'password', rule: 'symbol'}); + } + + let error; + if (!valid) { + error = intl ? ( + intl.formatMessage( + { + id: errorId, + defaultMessage: 'Must be {min}-{max} characters long.', + }, + { + min: minimumLength, + max: Constants.MAX_PASSWORD_LENGTH, + }, + ) + ) : ( + + ); + } + + return {valid, error, telemetryErrorIds}; +} + +export const passwordErrors = defineMessages({ + passwordError: {id: 'user.settings.security.passwordError', defaultMessage: 'Must be {min}-{max} characters long.'}, + passwordErrorLowercase: {id: 'user.settings.security.passwordErrorLowercase', defaultMessage: 'Must be {min}-{max} characters long and include lowercase letters.'}, + passwordErrorLowercaseNumber: {id: 'user.settings.security.passwordErrorLowercaseNumber', defaultMessage: 'Must be {min}-{max} characters long and include lowercase letters and numbers.'}, + passwordErrorLowercaseNumberSymbol: {id: 'user.settings.security.passwordErrorLowercaseNumberSymbol', defaultMessage: 'Must be {min}-{max} characters long and include lowercase letters, numbers, and special characters.'}, + passwordErrorLowercaseSymbol: {id: 'user.settings.security.passwordErrorLowercaseSymbol', defaultMessage: 'Must be {min}-{max} characters long and include lowercase letters and special characters.'}, + passwordErrorLowercaseUppercase: {id: 'user.settings.security.passwordErrorLowercaseUppercase', defaultMessage: 'Must be {min}-{max} characters long and include both lowercase and uppercase letters.'}, + passwordErrorLowercaseUppercaseNumber: {id: 'user.settings.security.passwordErrorLowercaseUppercaseNumber', defaultMessage: 'Must be {min}-{max} characters long and include both lowercase and uppercase letters, and numbers.'}, + passwordErrorLowercaseUppercaseNumberSymbol: {id: 'user.settings.security.passwordErrorLowercaseUppercaseNumberSymbol', defaultMessage: 'Must be {min}-{max} characters long and include both lowercase and uppercase letters, numbers, and special characters.'}, + passwordErrorLowercaseUppercaseSymbol: {id: 'user.settings.security.passwordErrorLowercaseUppercaseSymbol', defaultMessage: 'Must be {min}-{max} characters long and include both lowercase and uppercase letters, and special characters.'}, + passwordErrorNumber: {id: 'user.settings.security.passwordErrorNumber', defaultMessage: 'Must be {min}-{max} characters long and include numbers.'}, + passwordErrorNumberSymbol: {id: 'user.settings.security.passwordErrorNumberSymbol', defaultMessage: 'Must be {min}-{max} characters long and include numbers and special characters.'}, + passwordErrorSymbol: {id: 'user.settings.security.passwordErrorSymbol', defaultMessage: 'Must be {min}-{max} characters long and include special characters.'}, + passwordErrorUppercase: {id: 'user.settings.security.passwordErrorUppercase', defaultMessage: 'Must be {min}-{max} characters long and include uppercase letters.'}, + passwordErrorUppercaseNumber: {id: 'user.settings.security.passwordErrorUppercaseNumber', defaultMessage: 'Must be {min}-{max} characters long and include uppercase letters, and numbers.'}, + passwordErrorUppercaseNumberSymbol: {id: 'user.settings.security.passwordErrorUppercaseNumberSymbol', defaultMessage: 'Must be {min}-{max} characters long and include uppercase letters, numbers, and special characters.'}, + passwordErrorUppercaseSymbol: {id: 'user.settings.security.passwordErrorUppercaseSymbol', defaultMessage: 'Must be {min}-{max} characters long and include uppercase letters, and special characters.'}, +}); diff --git a/webapp/channels/src/utils/utils.test.tsx b/webapp/channels/src/utils/utils.test.tsx index 95fccb02d3..207cdab002 100644 --- a/webapp/channels/src/utils/utils.test.tsx +++ b/webapp/channels/src/utils/utils.test.tsx @@ -93,199 +93,6 @@ describe('Utils.getDisplayNameByUser', () => { }); }); -describe('Utils.isValidPassword', () => { - test('Minimum length enforced', () => { - for (const data of [ - { - password: 'tooshort', - config: { - minimumLength: 10, - requireLowercase: false, - requireUppercase: false, - requireNumber: false, - requireSymbol: false, - }, - valid: false, - }, - { - password: 'longenoughpassword', - config: { - minimumLength: 10, - requireLowercase: false, - requireUppercase: false, - requireNumber: false, - requireSymbol: false, - }, - valid: true, - }, - ]) { - const {valid} = Utils.isValidPassword(data.password, data.config); - expect(data.valid).toEqual(valid); - } - }); - - test('Maximum length enforced', () => { - for (const data of [ - { - password: 'justright', - config: { - minimumLength: 8, - requireLowercase: false, - requireUppercase: false, - requireNumber: false, - requireSymbol: false, - }, - valid: true, - }, - { - password: 'iamaverylongstringthathas72charactersandwillpasswithoutanyissuesthiscall', - config: { - minimumLength: 8, - requireLowercase: false, - requireUppercase: false, - requireNumber: false, - requireSymbol: false, - }, - valid: true, - }, - { - password: 'iamaverylongstringthathas73charactersandwontpassthisvalidationatall!!!:-(', - config: { - minimumLength: 8, - requireLowercase: false, - requireUppercase: false, - requireNumber: false, - requireSymbol: false, - }, - valid: false, - }, - ]) { - const {valid} = Utils.isValidPassword(data.password, data.config); - expect(data.valid).toEqual(valid); - } - }); - - test('Require lowercase enforced', () => { - for (const data of [ - { - password: 'UPPERCASE', - config: { - minimumLength: 5, - requireLowercase: true, - requireUppercase: false, - requireNumber: false, - requireSymbol: false, - }, - valid: false, - }, - { - password: 'SOMELowercase', - config: { - minimumLength: 5, - requireLowercase: true, - requireUppercase: false, - requireNumber: false, - requireSymbol: false, - }, - valid: true, - }, - ]) { - const {valid} = Utils.isValidPassword(data.password, data.config); - expect(data.valid).toEqual(valid); - } - }); - - test('Require uppercase enforced', () => { - for (const data of [ - { - password: 'lowercase', - config: { - minimumLength: 5, - requireLowercase: false, - requireUppercase: true, - requireNumber: false, - requireSymbol: false, - }, - valid: false, - }, - { - password: 'SOMEUppercase', - config: { - minimumLength: 5, - requireLowercase: false, - requireUppercase: true, - requireNumber: false, - requireSymbol: false, - }, - valid: true, - }, - ]) { - const {valid} = Utils.isValidPassword(data.password, data.config); - expect(data.valid).toEqual(valid); - } - }); - - test('Require number enforced', () => { - for (const data of [ - { - password: 'NoNumbers', - config: { - minimumLength: 5, - requireLowercase: true, - requireUppercase: true, - requireNumber: true, - requireSymbol: false, - }, - valid: false, - }, - { - password: 'S0m3Numb3rs', - config: { - minimumLength: 5, - requireLowercase: true, - requireUppercase: true, - requireNumber: true, - requireSymbol: false, - }, - valid: true, - }, - ]) { - const {valid} = Utils.isValidPassword(data.password, data.config); - expect(data.valid).toEqual(valid); - } - }); - - test('Require symbol enforced', () => { - for (const data of [ - { - password: 'N0Symb0ls', - config: { - minimumLength: 5, - requireLowercase: true, - requireUppercase: true, - requireNumber: true, - requireSymbol: true, - }, - valid: false, - }, - { - password: 'S0m3Symb0!s', - config: { - minimumLength: 5, - requireLowercase: true, - requireUppercase: true, - requireNumber: true, - requireSymbol: true, - }, - valid: true, - }, - ]) { - const {valid} = Utils.isValidPassword(data.password, data.config); - expect(data.valid).toEqual(valid); - } - }); -}); - describe('Utils.isValidUsername', () => { const tests = [ { diff --git a/webapp/channels/src/utils/utils.tsx b/webapp/channels/src/utils/utils.tsx index 27e6f6f658..6bba4364c2 100644 --- a/webapp/channels/src/utils/utils.tsx +++ b/webapp/channels/src/utils/utils.tsx @@ -9,12 +9,9 @@ import isNil from 'lodash/isNil'; import moment from 'moment'; import React from 'react'; import type {LinkHTMLAttributes} from 'react'; -import {FormattedMessage} from 'react-intl'; -import type {IntlShape} from 'react-intl'; import type {Channel} from '@mattermost/types/channels'; import type {Address} from '@mattermost/types/cloud'; -import type {ClientConfig} from '@mattermost/types/config'; import type {FileInfo} from '@mattermost/types/files'; import type {Group} from '@mattermost/types/groups'; import type {GlobalState} from '@mattermost/types/store'; @@ -1224,92 +1221,6 @@ export function mod(a: number, b: number): number { export const REACTION_PATTERN = /^(\+|-):([^:\s]+):\s*$/; -export function getPasswordConfig(config: Partial) { - return { - minimumLength: parseInt(config.PasswordMinimumLength!, 10), - requireLowercase: config.PasswordRequireLowercase === 'true', - requireUppercase: config.PasswordRequireUppercase === 'true', - requireNumber: config.PasswordRequireNumber === 'true', - requireSymbol: config.PasswordRequireSymbol === 'true', - }; -} - -export function isValidPassword(password: string, passwordConfig: ReturnType, intl?: IntlShape) { - // The translation strings used by this function are defined in admin_console/password_settings - let errorId = 'user.settings.security.passwordError'; - const telemetryErrorIds = []; - let valid = true; - const minimumLength = passwordConfig.minimumLength || Constants.MIN_PASSWORD_LENGTH; - - if (password.length < minimumLength || password.length > Constants.MAX_PASSWORD_LENGTH) { - valid = false; - telemetryErrorIds.push({field: 'password', rule: 'error_length'}); - } - - if (passwordConfig.requireLowercase) { - if (!password.match(/[a-z]/)) { - valid = false; - } - - errorId += 'Lowercase'; - telemetryErrorIds.push({field: 'password', rule: 'lowercase'}); - } - - if (passwordConfig.requireUppercase) { - if (!password.match(/[A-Z]/)) { - valid = false; - } - - errorId += 'Uppercase'; - telemetryErrorIds.push({field: 'password', rule: 'uppercase'}); - } - - if (passwordConfig.requireNumber) { - if (!password.match(/[0-9]/)) { - valid = false; - } - - errorId += 'Number'; - telemetryErrorIds.push({field: 'password', rule: 'number'}); - } - - if (passwordConfig.requireSymbol) { - if (!password.match(/[ !"\\#$%&'()*+,-./:;<=>?@[\]^_`|~]/)) { - valid = false; - } - - errorId += 'Symbol'; - telemetryErrorIds.push({field: 'password', rule: 'symbol'}); - } - - let error; - if (!valid) { - error = intl ? ( - intl.formatMessage( - { - id: errorId, - defaultMessage: 'Must be {min}-{max} characters long.', - }, - { - min: minimumLength, - max: Constants.MAX_PASSWORD_LENGTH, - }, - ) - ) : ( - - ); - } - - return {valid, error, telemetryErrorIds}; -} - function isChannelOrPermalink(link: string) { let match = (/\/([a-z0-9\-_]+)\/channels\/([a-z0-9\-__][a-z0-9\-__.]+)/).exec(link); if (match) {