[MM-53124] Add optional Forgot Password custom link to override the default flow on the login page (#23831)

* [MM-53124] Add optional Forgot Password custom link to override the default flow on the login page

* Fix i18n

* Fix test

* Added the enable/disable flag

* Fix test

* Fix e2e

* Add blockable link for the Customization navigation
Этот коммит содержится в:
Devin Binnie
2023-07-11 09:04:39 -04:00
коммит произвёл GitHub
родитель b59cba7e63
Коммит 116728424c
10 изменённых файлов: 161 добавлений и 48 удалений

Просмотреть файл

@@ -44,6 +44,7 @@ const onPremServerConfig = (): Partial<TestAdminConfig> => {
Number: false,
Uppercase: false,
Symbol: false,
EnableForgotLink: true,
},
PluginSettings: {
EnableUploads: true,
@@ -265,6 +266,7 @@ const defaultServerConfig: AdminConfig = {
Number: false,
Uppercase: false,
Symbol: false,
EnableForgotLink: true,
},
FileSettings: {
EnableFileAttachments: true,
@@ -345,6 +347,7 @@ const defaultServerConfig: AdminConfig = {
AboutLink: 'https://docs.mattermost.com/pl/about-mattermost',
HelpLink: 'https://mattermost.com/pl/help/',
ReportAProblemLink: 'https://mattermost.com/pl/report-a-bug',
ForgotPasswordLink: '',
SupportEmail: '',
CustomTermsOfServiceEnabled: false,
CustomTermsOfServiceReAcceptancePeriod: 365,

Просмотреть файл

@@ -272,6 +272,7 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m
props["AboutLink"] = *c.SupportSettings.AboutLink
props["HelpLink"] = *c.SupportSettings.HelpLink
props["ReportAProblemLink"] = *c.SupportSettings.ReportAProblemLink
props["ForgotPasswordLink"] = *c.SupportSettings.ForgotPasswordLink
props["SupportEmail"] = *c.SupportSettings.SupportEmail
props["EnableAskCommunityLink"] = strconv.FormatBool(*c.SupportSettings.EnableAskCommunityLink)
@@ -295,6 +296,7 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m
props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase)
props["PasswordRequireNumber"] = strconv.FormatBool(*c.PasswordSettings.Number)
props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol)
props["PasswordEnableForgotLink"] = strconv.FormatBool(*c.PasswordSettings.EnableForgotLink)
// Set default values for all options that require a license.
props["EnableCustomBrand"] = "false"

Просмотреть файл

@@ -1487,11 +1487,12 @@ func (s *NotificationLogSettings) GetAdvancedLoggingConfig() []byte {
}
type PasswordSettings struct {
MinimumLength *int `access:"authentication_password"`
Lowercase *bool `access:"authentication_password"`
Number *bool `access:"authentication_password"`
Uppercase *bool `access:"authentication_password"`
Symbol *bool `access:"authentication_password"`
MinimumLength *int `access:"authentication_password"`
Lowercase *bool `access:"authentication_password"`
Number *bool `access:"authentication_password"`
Uppercase *bool `access:"authentication_password"`
Symbol *bool `access:"authentication_password"`
EnableForgotLink *bool `access:"authentication_password"`
}
func (s *PasswordSettings) SetDefaults() {
@@ -1514,6 +1515,10 @@ func (s *PasswordSettings) SetDefaults() {
if s.Symbol == nil {
s.Symbol = NewBool(false)
}
if s.EnableForgotLink == nil {
s.EnableForgotLink = NewBool(true)
}
}
type FileSettings struct {
@@ -1882,6 +1887,7 @@ type SupportSettings struct {
AboutLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
HelpLink *string `access:"site_customization"`
ReportAProblemLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
ForgotPasswordLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
SupportEmail *string `access:"site_notifications"`
CustomTermsOfServiceEnabled *bool `access:"compliance_custom_terms_of_service"`
CustomTermsOfServiceReAcceptancePeriod *int `access:"compliance_custom_terms_of_service"`
@@ -1929,6 +1935,14 @@ func (s *SupportSettings) SetDefaults() {
s.ReportAProblemLink = NewString(SupportSettingsDefaultReportAProblemLink)
}
if !isSafeLink(s.ForgotPasswordLink) {
*s.ForgotPasswordLink = ""
}
if s.ForgotPasswordLink == nil {
s.ForgotPasswordLink = NewString("")
}
if s.SupportEmail == nil {
s.SupportEmail = NewString(SupportSettingsDefaultSupportEmail)
}

Просмотреть файл

@@ -115,7 +115,8 @@
"Lowercase": false,
"Number": false,
"Uppercase": false,
"Symbol": false
"Symbol": false,
"EnableForgotLink": true
},
"FileSettings": {
"EnableFileAttachments": true,
@@ -187,6 +188,7 @@
"AboutLink": "https://mattermost.com/default-about/",
"HelpLink": "https://mattermost.com/pl/help/",
"ReportAProblemLink": "https://mattermost.com/pl/report-a-bug",
"ForgotPasswordLink": "",
"SupportEmail": "feedback@mattermost.com"
},
"AnnouncementSettings": {

Просмотреть файл

@@ -55,7 +55,7 @@ import TeamSettings from './team_channel_settings/team';
import TeamDetails from './team_channel_settings/team/details';
import ChannelSettings from './team_channel_settings/channel';
import ChannelDetails from './team_channel_settings/channel/details';
import PasswordSettings from './password_settings.jsx';
import PasswordSettings from './password_settings';
import PushNotificationsSettings from './push_settings';
import DataRetentionSettings from './data_retention_settings';
import GlobalDataRetentionForm from './data_retention_settings/global_policy_form';
@@ -2135,6 +2135,16 @@ const AdminDefinition = {
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)),
isHidden: it.configIsTrue('ExperimentalSettings', 'RestrictSystemAdmin'),
},
{
type: Constants.SettingsTypes.TYPE_TEXT,
key: 'SupportSettings.ForgotPasswordLink',
label: t('admin.support.forgotPasswordTitle'),
label_default: 'Forgot Password Custom Link:',
help_text: t('admin.support.forgotPasswordDesc'),
help_text_default: 'The URL for the Forgot Password link on the Mattermost login page. If this field is empty the Forgot Password link takes users to the Password Reset page.',
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)),
isHidden: it.configIsTrue('ExperimentalSettings', 'RestrictSystemAdmin'),
},
{
type: Constants.SettingsTypes.TYPE_TEXT,
key: 'SupportSettings.ReportAProblemLink',

Просмотреть файл

@@ -4,17 +4,38 @@
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {AdminConfig} from '@mattermost/types/config';
import {DeepPartial} from '@mattermost/types/utilities';
import Constants from 'utils/constants';
import * as Utils from 'utils/utils';
import {t} from 'utils/i18n';
import AdminSettings from './admin_settings';
import AdminSettings, {BaseProps, BaseState} from './admin_settings';
import BooleanSetting from './boolean_setting';
import Setting from './setting';
import SettingsGroup from './settings_group';
import TextSetting from './text_setting';
import BlockableLink from './blockable_link';
export default class PasswordSettings extends AdminSettings {
constructor(props) {
type Props = BaseProps & {
config: AdminConfig;
};
type State = BaseState & {
passwordMinimumLength?: string;
passwordLowercase?: boolean;
passwordNumber?: boolean;
passwordUppercase?: boolean;
passwordSymbol?: boolean;
passwordEnableForgotLink?: boolean;
maximumLoginAttempts?: string;
};
export default class PasswordSettings extends AdminSettings<Props, State> {
sampleErrorMsg: React.ReactNode;
constructor(props: Props) {
super(props);
this.state = Object.assign(this.state, {
@@ -23,6 +44,7 @@ export default class PasswordSettings extends AdminSettings {
passwordNumber: props.config.PasswordSettings.Number,
passwordUppercase: props.config.PasswordSettings.Uppercase,
passwordSymbol: props.config.PasswordSettings.Symbol,
passwordEnableForgotLink: props.config.PasswordSettings.EnableForgotLink,
maximumLoginAttempts: props.config.ServiceSettings.MaximumLoginAttempts,
});
@@ -59,7 +81,7 @@ export default class PasswordSettings extends AdminSettings {
this.sampleErrorMsg = (
<FormattedMessage
id={sampleErrorMsgId}
default='Your password must contain between {min} and {max} characters.'
defaultMessage='Your password must contain between {min} and {max} characters.'
values={{
min: (this.state.passwordMinimumLength || Constants.MIN_PASSWORD_LENGTH),
max: Constants.MAX_PASSWORD_LENGTH,
@@ -68,26 +90,32 @@ export default class PasswordSettings extends AdminSettings {
);
}
getConfigFromState = (config) => {
config.PasswordSettings.MinimumLength = this.parseIntNonZero(this.state.passwordMinimumLength, Constants.MIN_PASSWORD_LENGTH);
config.PasswordSettings.Lowercase = this.state.passwordLowercase;
config.PasswordSettings.Uppercase = this.state.passwordUppercase;
config.PasswordSettings.Number = this.state.passwordNumber;
config.PasswordSettings.Symbol = this.state.passwordSymbol;
getConfigFromState = (config: DeepPartial<AdminConfig>) => {
if (config.PasswordSettings) {
config.PasswordSettings.MinimumLength = this.parseIntNonZero(this.state.passwordMinimumLength ?? '', Constants.MIN_PASSWORD_LENGTH);
config.PasswordSettings.Lowercase = this.state.passwordLowercase;
config.PasswordSettings.Uppercase = this.state.passwordUppercase;
config.PasswordSettings.Number = this.state.passwordNumber;
config.PasswordSettings.Symbol = this.state.passwordSymbol;
config.PasswordSettings.EnableForgotLink = this.state.passwordEnableForgotLink;
}
config.ServiceSettings.MaximumLoginAttempts = this.parseIntNonZero(this.state.maximumLoginAttempts, Constants.MAXIMUM_LOGIN_ATTEMPTS_DEFAULT);
if (config.ServiceSettings) {
config.ServiceSettings.MaximumLoginAttempts = this.parseIntNonZero(this.state.maximumLoginAttempts ?? '', Constants.MAXIMUM_LOGIN_ATTEMPTS_DEFAULT);
}
return config;
};
getStateFromConfig(config) {
getStateFromConfig(config: DeepPartial<AdminConfig>) {
return {
passwordMinimumLength: config.PasswordSettings.MinimumLength,
passwordLowercase: config.PasswordSettings.Lowercase,
passwordNumber: config.PasswordSettings.Number,
passwordUppercase: config.PasswordSettings.Uppercase,
passwordSymbol: config.PasswordSettings.Symbol,
maximumLoginAttempts: config.ServiceSettings.MaximumLoginAttempts,
passwordMinimumLength: String(config.PasswordSettings?.MinimumLength),
passwordLowercase: config.PasswordSettings?.Lowercase,
passwordNumber: config.PasswordSettings?.Number,
passwordUppercase: config.PasswordSettings?.Uppercase,
passwordSymbol: config.PasswordSettings?.Symbol,
passwordEnableForgotLink: config.PasswordSettings?.EnableForgotLink,
maximumLoginAttempts: String(config.ServiceSettings?.MaximumLoginAttempts),
};
}
@@ -96,7 +124,7 @@ export default class PasswordSettings extends AdminSettings {
return (
<FormattedMessage
id='user.settings.security.passwordMinLength'
default='Invalid minimum length, cannot show preview.'
defaultMessage='Invalid minimum length, cannot show preview.'
/>
);
}
@@ -116,7 +144,7 @@ export default class PasswordSettings extends AdminSettings {
return (
<FormattedMessage
id={sampleErrorMsgId}
default='Your password must contain between {min} and {max} characters.'
defaultMessage='Your password must contain between {min} and {max} characters.'
values={{
min: (this.state.passwordMinimumLength || Constants.MIN_PASSWORD_LENGTH),
max: Constants.MAX_PASSWORD_LENGTH,
@@ -125,13 +153,9 @@ export default class PasswordSettings extends AdminSettings {
);
};
handlePasswordLengthChange = (id, value) => {
this.handleChange(id, value);
};
handleCheckboxChange = (id) => {
return ({target: {checked}}) => {
this.handleChange(id, checked);
handleCheckboxChange = (id: string) => {
return (event: React.ChangeEvent<HTMLInputElement>) => {
this.handleChange(id, event.target.checked);
};
};
@@ -167,10 +191,11 @@ export default class PasswordSettings extends AdminSettings {
}}
/>
}
value={this.state.passwordMinimumLength}
onChange={this.handlePasswordLengthChange}
value={this.state.passwordMinimumLength ?? ''}
onChange={this.handleChange}
setByEnv={this.isSetByEnv('PasswordSettings.MinimumLength')}
disabled={this.props.isDisabled}
type='input'
/>
<Setting
label={
@@ -184,7 +209,6 @@ export default class PasswordSettings extends AdminSettings {
<label className='checkbox-inline'>
<input
type='checkbox'
ref={this.lowercase}
defaultChecked={this.state.passwordLowercase}
name='admin.password.lowercase'
disabled={this.props.isDisabled}
@@ -200,7 +224,6 @@ export default class PasswordSettings extends AdminSettings {
<label className='checkbox-inline'>
<input
type='checkbox'
ref={this.uppercase}
defaultChecked={this.state.passwordUppercase}
name='admin.password.uppercase'
disabled={this.props.isDisabled}
@@ -216,7 +239,6 @@ export default class PasswordSettings extends AdminSettings {
<label className='checkbox-inline'>
<input
type='checkbox'
ref={this.number}
defaultChecked={this.state.passwordNumber}
name='admin.password.number'
disabled={this.props.isDisabled}
@@ -232,7 +254,6 @@ export default class PasswordSettings extends AdminSettings {
<label className='checkbox-inline'>
<input
type='checkbox'
ref={this.symbol}
defaultChecked={this.state.passwordSymbol}
name='admin.password.symbol'
disabled={this.props.isDisabled}
@@ -274,13 +295,40 @@ export default class PasswordSettings extends AdminSettings {
defaultMessage='Login attempts allowed before user is locked out and required to reset password via email.'
/>
}
value={this.state.maximumLoginAttempts}
value={this.state.maximumLoginAttempts ?? ''}
onChange={this.handleChange}
setByEnv={this.isSetByEnv('ServiceSettings.MaximumLoginAttempts')}
disabled={this.props.isDisabled}
type='input'
/>
)
}
<BooleanSetting
id='passwordEnableForgotLink'
label={
<FormattedMessage
id='admin.password.enableForgotLink.title'
defaultMessage='Enable Forgot Password Link:'
/>
}
helpText={
<FormattedMessage
id='admin.password.enableForgotLink.description'
defaultMessage='When true, “Forgot password” link appears on the Mattermost login page, which allows users to reset their password. When false, the link is hidden from users. This link can be customized to redirect to a URL of your choice from <a>Site Configuration > Customization.</a>'
values={{
a: (chunks) => (
<BlockableLink to='/admin_console/site_config/customization'>
{chunks}
</BlockableLink>
),
}}
/>
}
value={this.state.passwordEnableForgotLink ?? false}
setByEnv={false}
onChange={this.handleChange}
disabled={this.props.isDisabled}
/>
</SettingsGroup>
);
};

Просмотреть файл

@@ -125,6 +125,7 @@ describe('components/login/Login', () => {
CustomDescriptionText: '',
SiteName: 'Mattermost',
ExperimentalPrimaryTeam: '',
PasswordEnableForgotLink: 'true',
};
});

Просмотреть файл

@@ -59,6 +59,7 @@ import {setCSRFFromCookie} from 'utils/utils';
import LoginMfa from './login_mfa';
import './login.scss';
import ExternalLink from 'components/external_link';
const MOBILE_SCREEN_WIDTH = 1200;
@@ -98,6 +99,8 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
CustomDescriptionText,
SiteName,
ExperimentalPrimaryTeam,
ForgotPasswordLink,
PasswordEnableForgotLink,
} = useSelector(getConfig);
const {IsLicensed} = useSelector(getLicense);
const initializing = useSelector((state: GlobalState) => state.requests.users.logout.status === RequestStatus.SUCCESS || !state.storage.initialized);
@@ -703,6 +706,34 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
);
};
const getResetPasswordLink = () => {
if (!PasswordEnableForgotLink || PasswordEnableForgotLink === 'false') {
return null;
}
if (ForgotPasswordLink) {
return (
<div className='login-body-card-form-link'>
<ExternalLink href={ForgotPasswordLink}>
{formatMessage({id: 'login.forgot', defaultMessage: 'Forgot your password?'})}
</ExternalLink>
</div>
);
}
if (enableSignInWithUsername || enableSignInWithEmail) {
return (
<div className='login-body-card-form-link'>
<Link to='/reset_password'>
{formatMessage({id: 'login.forgot', defaultMessage: 'Forgot your password?'})}
</Link>
</div>
);
}
return null;
};
const getContent = () => {
if (showMfa) {
return (
@@ -802,13 +833,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
hasError={hasError}
disabled={isWaiting}
/>
{(enableSignInWithUsername || enableSignInWithEmail) && (
<div className='login-body-card-form-link'>
<Link to='/reset_password'>
{formatMessage({id: 'login.forgot', defaultMessage: 'Forgot your password?'})}
</Link>
</div>
)}
{getResetPasswordLink()}
<SaveButton
extraClasses='login-body-card-form-button-submit large'
saving={isWaiting}

Просмотреть файл

@@ -1505,6 +1505,8 @@
"admin.openIdConvert.help": "Learn more",
"admin.openIdConvert.message": "You can now convert your OAuth2.0 configuration to OpenID Connect.",
"admin.openIdConvert.text": "Convert to OpenID Connect",
"admin.password.enableForgotLink.description": "When true, “Forgot password” link appears on the Mattermost login page, which allows users to reset their password. When false, the link is hidden from users. This link can be customized to redirect to a URL of your choice from <a>Site Configuration > Customization.</a>",
"admin.password.enableForgotLink.title": "Enable Forgot Password Link:",
"admin.password.lowercase": "At least one lowercase letter",
"admin.password.minimumLength": "Minimum Password Length:",
"admin.password.minimumLengthDescription": "Minimum number of characters required for a valid password. Must be a whole number greater than or equal to {min} and less than or equal to {max}.",
@@ -2357,6 +2359,8 @@
"admin.support.enableAskCommunityTitle": "Enable Ask Community Link:",
"admin.support.enableTermsOfServiceHelp": "When true, new users must accept the terms of service before accessing any Mattermost teams on desktop, web or mobile. Existing users must accept them after login or a page refresh.\n \nTo update terms of service link displayed in account creation and login pages, go to [Site Configuration > Customization](../site_config/customization).",
"admin.support.enableTermsOfServiceTitle": "Enable Custom Terms of Service:",
"admin.support.forgotPasswordDesc": "The URL for the Forgot Password link on the Mattermost login page. If this field is empty the Forgot Password link takes users to the Password Reset page.",
"admin.support.forgotPasswordTitle": "Forgot Password Custom Link:",
"admin.support.helpDesc": "The URL for the Help link on the Mattermost login page, sign-up pages, and Help Menu. If this field is empty, the Help link is hidden from users.",
"admin.support.helpTitle": "Help Link:",
"admin.support.privacyDesc": "The URL for the Privacy link on the login and sign-up pages. If this field is empty, the Privacy link is hidden from users.",

Просмотреть файл

@@ -121,6 +121,7 @@ export type ClientConfig = {
FeatureFlagBoardsProduct: string;
FeatureFlagCallsEnabled: string;
FeatureFlagGraphQL: string;
ForgotPasswordLink: string;
GfycatAPIKey: string;
GfycatAPISecret: string;
GoogleDeveloperKey: string;
@@ -154,6 +155,7 @@ export type ClientConfig = {
GitLabButtonColor: string;
OpenIdButtonText: string;
OpenIdButtonColor: string;
PasswordEnableForgotLink: string;
PasswordMinimumLength: string;
PasswordRequireLowercase: string;
PasswordRequireNumber: string;
@@ -477,6 +479,7 @@ export type PasswordSettings = {
Number: boolean;
Uppercase: boolean;
Symbol: boolean;
EnableForgotLink: boolean;
};
export type FileSettings = {
@@ -562,6 +565,7 @@ export type SupportSettings = {
AboutLink: string;
HelpLink: string;
ReportAProblemLink: string;
ForgotPasswordLink: string;
SupportEmail: string;
CustomTermsOfServiceEnabled: boolean;
CustomTermsOfServiceReAcceptancePeriod: number;