Move password utilities to their own file (#27366)
* Move password utilities to their own file * Move passwordErrors to utils/password * Update file path in test * Actually commit the whole thing
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
cca0db7e32
Коммит
7d06ac8812
@@ -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<string|MessageDescriptor|[MessageDescripto
|
||||
messages.attemptDescription,
|
||||
];
|
||||
|
||||
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.'},
|
||||
});
|
||||
|
||||
function getPasswordErrorsMessage(lowercase?: boolean, uppercase?: boolean, number?: boolean, symbol?: boolean) {
|
||||
type KeyType = keyof typeof passwordErrors;
|
||||
|
||||
|
||||
@@ -6,21 +6,17 @@ import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {updateUserPassword} from 'mattermost-redux/actions/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getPasswordConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getPasswordConfig} from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ResetPasswordModal from './reset_password_modal';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const config = getConfig(state);
|
||||
|
||||
return {
|
||||
currentUserId: getCurrentUserId(state),
|
||||
passwordConfig: getPasswordConfig(config),
|
||||
passwordConfig: getPasswordConfig(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import * as Utils from 'utils/utils';
|
||||
import {isValidPassword} from 'utils/password';
|
||||
|
||||
interface PasswordConfig {
|
||||
minimumLength: number;
|
||||
@@ -83,7 +83,7 @@ export default class ResetPasswordModal extends React.PureComponent<Props, State
|
||||
|
||||
const password = (this.passwordRef.current as HTMLInputElement).value;
|
||||
|
||||
const {valid, error} = Utils.isValidPassword(password, this.props.passwordConfig);
|
||||
const {valid, error} = isValidPassword(password, this.props.passwordConfig);
|
||||
if (!valid && error) {
|
||||
this.setState({serverErrorNewPass: error});
|
||||
return;
|
||||
|
||||
@@ -12,7 +12,8 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import LoginMfa from 'components/login/login_mfa';
|
||||
|
||||
import {ClaimErrors} from 'utils/constants';
|
||||
import {isValidPassword, localizeMessage} from 'utils/utils';
|
||||
import {isValidPassword} from 'utils/password';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
|
||||
import type {SubmitOptions} from './email_to_ldap';
|
||||
import ErrorLabel from './error_label';
|
||||
|
||||
@@ -7,11 +7,13 @@ import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import type {AuthChangeResponse} from '@mattermost/types/users';
|
||||
|
||||
import type {PasswordConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {oauthToEmail} from 'actions/admin_actions.jsx';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {isValidPassword, localizeMessage, toTitleCase} from 'utils/utils';
|
||||
import type {getPasswordConfig} from 'utils/utils';
|
||||
import {isValidPassword} from 'utils/password';
|
||||
import {localizeMessage, toTitleCase} from 'utils/utils';
|
||||
|
||||
import ErrorLabel from './error_label';
|
||||
|
||||
@@ -19,7 +21,7 @@ type Props = {
|
||||
currentType: string | null;
|
||||
email: string | null;
|
||||
siteName?: string;
|
||||
passwordConfig?: ReturnType<typeof getPasswordConfig>;
|
||||
passwordConfig?: PasswordConfig;
|
||||
}
|
||||
|
||||
const OAuthToEmail = (props: Props) => {
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<typeof Utils.getPasswordConfig>,
|
||||
passwordConfig: {} as PasswordConfig,
|
||||
militaryTime: false,
|
||||
intl: {
|
||||
formatMessage: jest.fn(({id, defaultMessage}) => defaultMessage || id),
|
||||
|
||||
@@ -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<typeof Utils.getPasswordConfig>;
|
||||
passwordConfig: PasswordConfig;
|
||||
militaryTime: boolean;
|
||||
actions: Actions;
|
||||
intl: IntlShape;
|
||||
@@ -135,7 +136,7 @@ export class SecurityTab extends React.PureComponent<Props, State> {
|
||||
return;
|
||||
}
|
||||
|
||||
const {valid, error} = Utils.isValidPassword(
|
||||
const {valid, error} = isValidPassword(
|
||||
newPassword,
|
||||
this.props.passwordConfig,
|
||||
);
|
||||
|
||||
@@ -21,6 +21,28 @@ export function getFeatureFlagValue(state: GlobalState, key: keyof FeatureFlags)
|
||||
return getConfig(state)?.[`FeatureFlag${key}` as keyof Partial<ClientConfig>];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
197
webapp/channels/src/utils/password.test.tsx
Обычный файл
197
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
104
webapp/channels/src/utils/password.tsx
Обычный файл
104
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,
|
||||
},
|
||||
)
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id={errorId}
|
||||
defaultMessage='Must be {min}-{max} characters long.'
|
||||
values={{
|
||||
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.'},
|
||||
});
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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<ClientConfig>) {
|
||||
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<typeof getPasswordConfig>, 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,
|
||||
},
|
||||
)
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id={errorId}
|
||||
defaultMessage='Must be {min}-{max} characters long.'
|
||||
values={{
|
||||
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) {
|
||||
|
||||
Ссылка в новой задаче
Block a user