[MM-37984] Allow Desktop App to authenticate via external providers outside of the app on supported servers (#23795)
* WIP * Add rate limiting for desktop token API * Missing mocks * Style fixes * Update snapshots * Maybe use an actual redirect link :P * Refactoring for tests * Add tests for server * Fix lint issue * Fix tests * Fix lint * Add front-end screen component * Component logic * Style changes * Quick style fix * Lint fixes * Initial PR feedback * Enable logging into the browser as well when completing the login process * Refactor to push more logic to the other component * Remove unnecessary helper code * Fix i18n --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
5e3c03a0a8
Коммит
abdf4e58c3
@@ -45,6 +45,40 @@ export function login(loginId: string, password: string, mfaToken = ''): ActionF
|
||||
};
|
||||
}
|
||||
|
||||
export function loginWithDesktopToken(token: string): ActionFunc {
|
||||
return async (dispatch: DispatchFunc) => {
|
||||
dispatch({type: UserTypes.LOGIN_REQUEST, data: null});
|
||||
|
||||
try {
|
||||
// This is partial user profile we recieved when we login. We still need to make getMe for complete user profile.
|
||||
const loggedInUserProfile = await Client4.loginWithDesktopToken(token);
|
||||
|
||||
dispatch(
|
||||
batchActions([
|
||||
{
|
||||
type: UserTypes.LOGIN_SUCCESS,
|
||||
},
|
||||
{
|
||||
type: UserTypes.RECEIVED_ME,
|
||||
data: loggedInUserProfile,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
dispatch(loadRolesIfNeeded(loggedInUserProfile.roles.split(' ')));
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: UserTypes.LOGIN_FAILURE,
|
||||
error,
|
||||
});
|
||||
dispatch(logError(error as ServerError));
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function loginById(id: string, password: string): ActionFunc {
|
||||
return async (dispatch: DispatchFunc) => {
|
||||
dispatch({type: UserTypes.LOGIN_REQUEST, data: null});
|
||||
|
||||
38
webapp/channels/src/components/desktop_auth_token.scss
Обычный файл
38
webapp/channels/src/components/desktop_auth_token.scss
Обычный файл
@@ -0,0 +1,38 @@
|
||||
.DesktopAuthToken {
|
||||
display: flex;
|
||||
width: 540px;
|
||||
height: 80vh;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.DesktopAuthToken__main {
|
||||
display: flex;
|
||||
margin-top: auto;
|
||||
margin-bottom: 14px;
|
||||
color: var(--sidebar-header-bg);
|
||||
font-size: 40px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
.DesktopAuthToken__sub {
|
||||
margin-bottom: 24px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.72);
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
|
||||
&.complete {
|
||||
max-width: 442px;
|
||||
}
|
||||
}
|
||||
|
||||
.DesktopAuthToken__bottom {
|
||||
margin-top: auto;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
240
webapp/channels/src/components/desktop_auth_token.tsx
Обычный файл
240
webapp/channels/src/components/desktop_auth_token.tsx
Обычный файл
@@ -0,0 +1,240 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {useHistory, useLocation} from 'react-router-dom';
|
||||
|
||||
import crypto from 'crypto';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {loginWithDesktopToken} from 'actions/views/login';
|
||||
|
||||
import './desktop_auth_token.scss';
|
||||
|
||||
const BOTTOM_MESSAGE_TIMEOUT = 10000;
|
||||
const POLLING_INTERVAL = 2000;
|
||||
|
||||
enum DesktopAuthStatus {
|
||||
None,
|
||||
Polling,
|
||||
Expired,
|
||||
Complete,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
href: string;
|
||||
onLogin: (userProfile: UserProfile) => void;
|
||||
}
|
||||
|
||||
const DesktopAuthToken: React.FC<Props> = ({href, onLogin}: Props) => {
|
||||
const dispatch = useDispatch<DispatchFunc>();
|
||||
const history = useHistory();
|
||||
const {search} = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
|
||||
const [status, setStatus] = useState(query.get('desktopAuthComplete') ? DesktopAuthStatus.Complete : DesktopAuthStatus.None);
|
||||
const [token, setToken] = useState('');
|
||||
const [showBottomMessage, setShowBottomMessage] = useState<React.ReactNode>();
|
||||
|
||||
const interval = useRef<NodeJS.Timer>();
|
||||
|
||||
const {SiteURL} = useSelector(getConfig);
|
||||
|
||||
const tryDesktopLogin = async () => {
|
||||
const {data: userProfile, error: loginError} = await dispatch(loginWithDesktopToken(token));
|
||||
|
||||
if (loginError && loginError.server_error_id && loginError.server_error_id.length !== 0) {
|
||||
if (loginError.server_error_id === 'app.desktop_token.validate.expired') {
|
||||
clearInterval(interval.current as unknown as number);
|
||||
setStatus(DesktopAuthStatus.Expired);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(interval.current as unknown as number);
|
||||
setStatus(DesktopAuthStatus.Complete);
|
||||
await onLogin(userProfile as UserProfile);
|
||||
};
|
||||
|
||||
const getExternalLoginURL = () => {
|
||||
const parsedURL = new URL(href);
|
||||
|
||||
const params = new URLSearchParams(parsedURL.searchParams);
|
||||
params.set('desktop_token', token);
|
||||
|
||||
return `${parsedURL.origin}${parsedURL.pathname}?${params.toString()}`;
|
||||
};
|
||||
|
||||
const openDesktopApp = () => {
|
||||
if (!SiteURL) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(SiteURL);
|
||||
const redirectTo = query.get('redirect_to');
|
||||
if (redirectTo) {
|
||||
url.pathname += redirectTo;
|
||||
}
|
||||
url.protocol = 'mattermost';
|
||||
window.location.href = url.toString();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setShowBottomMessage(false);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setShowBottomMessage(true);
|
||||
}, BOTTOM_MESSAGE_TIMEOUT) as unknown as number;
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const url = getExternalLoginURL();
|
||||
window.open(url);
|
||||
|
||||
setStatus(DesktopAuthStatus.Polling);
|
||||
interval.current = setInterval(tryDesktopLogin, POLLING_INTERVAL);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval.current as unknown as number);
|
||||
};
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === DesktopAuthStatus.Complete) {
|
||||
openDesktopApp();
|
||||
return;
|
||||
}
|
||||
|
||||
setToken(crypto.randomBytes(32).toString('hex'));
|
||||
}, []);
|
||||
|
||||
let mainMessage;
|
||||
let subMessage;
|
||||
let bottomMessage;
|
||||
|
||||
if (status === DesktopAuthStatus.Polling) {
|
||||
mainMessage = (
|
||||
<FormattedMessage
|
||||
id='desktop_auth_token.polling.redirectingToBrowser'
|
||||
defaultMessage='Redirecting to browser...'
|
||||
/>
|
||||
);
|
||||
subMessage = (
|
||||
<FormattedMessage
|
||||
id='desktop_auth_token.polling.awaitingToken'
|
||||
defaultMessage='Authenticating in the browser, awaiting valid token.'
|
||||
/>
|
||||
);
|
||||
|
||||
bottomMessage = (
|
||||
<FormattedMessage
|
||||
id='desktop_auth_token.polling.isComplete'
|
||||
defaultMessage='Authentication complete? <a>Check token now</a>'
|
||||
values={{
|
||||
a: (chunks: React.ReactNode) => {
|
||||
return (
|
||||
<a onClick={tryDesktopLogin}>
|
||||
{chunks}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === DesktopAuthStatus.Complete) {
|
||||
mainMessage = (
|
||||
<FormattedMessage
|
||||
id='desktop_auth_token.complete.youAreNowLoggedIn'
|
||||
defaultMessage='You are now logged in'
|
||||
/>
|
||||
);
|
||||
subMessage = (
|
||||
<FormattedMessage
|
||||
id='desktop_auth_token.complete.openMattermost'
|
||||
defaultMessage='Click on <b>Open Mattermost</b> in the browser prompt to <a>launch the desktop app</a>'
|
||||
values={{
|
||||
a: (chunks: React.ReactNode) => {
|
||||
return (
|
||||
<a onClick={openDesktopApp}>
|
||||
{chunks}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
b: (chunks: React.ReactNode) => (<b>{chunks}</b>),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
bottomMessage = (
|
||||
<FormattedMessage
|
||||
id='desktop_auth_token.complete.havingTrouble'
|
||||
defaultMessage='Having trouble logging in? <a>Open Mattermost in your browser</a>'
|
||||
values={{
|
||||
a: (chunks: React.ReactNode) => {
|
||||
return (
|
||||
<a onClick={() => history.push('/')}>
|
||||
{chunks}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === DesktopAuthStatus.Expired) {
|
||||
mainMessage = (
|
||||
<FormattedMessage
|
||||
id='desktop_auth_token.expired.somethingWentWrong'
|
||||
defaultMessage='Something went wrong'
|
||||
/>
|
||||
);
|
||||
subMessage = (
|
||||
<FormattedMessage
|
||||
id='desktop_auth_token.expired.restartFlow'
|
||||
defaultMessage={'Click <a>here</a> to try again.'}
|
||||
values={{
|
||||
a: (chunks: React.ReactNode) => {
|
||||
return (
|
||||
<a onClick={() => history.push('/')}>
|
||||
{chunks}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
bottomMessage = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='DesktopAuthToken'>
|
||||
<h1 className='DesktopAuthToken__main'>
|
||||
{mainMessage}
|
||||
</h1>
|
||||
<p className={classNames('DesktopAuthToken__sub', {complete: status === DesktopAuthStatus.Complete})}>
|
||||
{subMessage}
|
||||
</p>
|
||||
<div className='DesktopAuthToken__bottom'>
|
||||
{showBottomMessage ? bottomMessage : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesktopAuthToken;
|
||||
@@ -13,6 +13,7 @@ export type ExternalLoginButtonType = {
|
||||
label: string;
|
||||
style?: React.CSSProperties;
|
||||
direction?: 'row' | 'column';
|
||||
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
};
|
||||
|
||||
const ExternalLoginButton = ({
|
||||
@@ -22,12 +23,14 @@ const ExternalLoginButton = ({
|
||||
label,
|
||||
style,
|
||||
direction = 'row',
|
||||
onClick,
|
||||
}: ExternalLoginButtonType) => (
|
||||
<a
|
||||
id={id}
|
||||
className={classNames('external-login-button', {'direction-column': direction === 'column'}, id)}
|
||||
href={url}
|
||||
style={style}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon}
|
||||
<span className='external-login-button-label'>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React, {useState, useEffect, useRef, useCallback, FormEvent} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Link, useLocation, useHistory} from 'react-router-dom';
|
||||
import {Link, useLocation, useHistory, Route} from 'react-router-dom';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import throttle from 'lodash/throttle';
|
||||
@@ -32,7 +32,9 @@ import {setNeedsLoggedInLimitReachedCheck} from 'actions/views/admin';
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import AlertBanner, {ModeType, AlertBannerProps} from 'components/alert_banner';
|
||||
import DesktopAuthToken from 'components/desktop_auth_token';
|
||||
import ExternalLoginButton, {ExternalLoginButtonType} from 'components/external_login_button/external_login_button';
|
||||
import ExternalLink from 'components/external_link';
|
||||
import AlternateLinkLayout from 'components/header_footer_route/content_layouts/alternate_link';
|
||||
import ColumnLayout from 'components/header_footer_route/content_layouts/column';
|
||||
import {CustomizeHeaderType} from 'components/header_footer_route/header_footer_route';
|
||||
@@ -54,12 +56,12 @@ import {GlobalState} from 'types/store';
|
||||
import Constants from 'utils/constants';
|
||||
import {showNotification} from 'utils/notifications';
|
||||
import {t} from 'utils/i18n';
|
||||
import {isDesktopApp} from 'utils/user_agent';
|
||||
import {setCSRFFromCookie} from 'utils/utils';
|
||||
|
||||
import LoginMfa from './login_mfa';
|
||||
|
||||
import './login.scss';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
const MOBILE_SCREEN_WIDTH = 1200;
|
||||
|
||||
@@ -148,6 +150,8 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
const query = new URLSearchParams(search);
|
||||
const redirectTo = query.get('redirect_to');
|
||||
|
||||
const [desktopLoginLink, setDesktopLoginLink] = useState('');
|
||||
|
||||
const getExternalLoginOptions = () => {
|
||||
const externalLoginOptions: ExternalLoginButtonType[] = [];
|
||||
|
||||
@@ -156,55 +160,76 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
}
|
||||
|
||||
if (enableSignUpWithGitLab) {
|
||||
const url = `${Client4.getOAuthRoute()}/gitlab/login${search}`;
|
||||
externalLoginOptions.push({
|
||||
id: 'gitlab',
|
||||
url: `${Client4.getOAuthRoute()}/gitlab/login${search}`,
|
||||
url,
|
||||
icon: <LoginGitlabIcon/>,
|
||||
label: GitLabButtonText || formatMessage({id: 'login.gitlab', defaultMessage: 'GitLab'}),
|
||||
style: {color: GitLabButtonColor, borderColor: GitLabButtonColor},
|
||||
onClick: desktopExternalAuth(url),
|
||||
});
|
||||
}
|
||||
|
||||
if (enableSignUpWithGoogle) {
|
||||
const url = `${Client4.getOAuthRoute()}/google/login${search}`;
|
||||
externalLoginOptions.push({
|
||||
id: 'google',
|
||||
url: `${Client4.getOAuthRoute()}/google/login${search}`,
|
||||
url,
|
||||
icon: <LoginGoogleIcon/>,
|
||||
label: formatMessage({id: 'login.google', defaultMessage: 'Google'}),
|
||||
onClick: desktopExternalAuth(url),
|
||||
});
|
||||
}
|
||||
|
||||
if (enableSignUpWithOffice365) {
|
||||
const url = `${Client4.getOAuthRoute()}/office365/login${search}`;
|
||||
externalLoginOptions.push({
|
||||
id: 'office365',
|
||||
url: `${Client4.getOAuthRoute()}/office365/login${search}`,
|
||||
url,
|
||||
icon: <LoginOffice365Icon/>,
|
||||
label: formatMessage({id: 'login.office365', defaultMessage: 'Office 365'}),
|
||||
onClick: desktopExternalAuth(url),
|
||||
});
|
||||
}
|
||||
|
||||
if (enableSignUpWithOpenId) {
|
||||
const url = `${Client4.getOAuthRoute()}/openid/login${search}`;
|
||||
externalLoginOptions.push({
|
||||
id: 'openid',
|
||||
url: `${Client4.getOAuthRoute()}/openid/login${search}`,
|
||||
url,
|
||||
icon: <LoginOpenIDIcon/>,
|
||||
label: OpenIdButtonText || formatMessage({id: 'login.openid', defaultMessage: 'Open ID'}),
|
||||
style: {color: OpenIdButtonColor, borderColor: OpenIdButtonColor},
|
||||
onClick: desktopExternalAuth(url),
|
||||
});
|
||||
}
|
||||
|
||||
if (enableSignUpWithSaml) {
|
||||
const url = `${Client4.getUrl()}/login/sso/saml${search}`;
|
||||
externalLoginOptions.push({
|
||||
id: 'saml',
|
||||
url: `${Client4.getUrl()}/login/sso/saml${search}`,
|
||||
url,
|
||||
icon: <LockIcon/>,
|
||||
label: SamlLoginButtonText || formatMessage({id: 'login.saml', defaultMessage: 'SAML'}),
|
||||
onClick: desktopExternalAuth(url),
|
||||
});
|
||||
}
|
||||
|
||||
return externalLoginOptions;
|
||||
};
|
||||
|
||||
const desktopExternalAuth = (href: string) => {
|
||||
return (event: React.MouseEvent) => {
|
||||
if (isDesktopApp()) {
|
||||
event.preventDefault();
|
||||
|
||||
setDesktopLoginLink(href);
|
||||
history.push(`/login/desktop${search}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const dismissAlert = () => {
|
||||
setAlertBanner(null);
|
||||
setHasError(false);
|
||||
@@ -378,6 +403,11 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
}, [onCustomizeHeader, search, showMfa, isMobileView, getAlternateLink]);
|
||||
|
||||
useEffect(() => {
|
||||
// We don't want to redirect outside of this route if we're doing Desktop App auth
|
||||
if (query.get('desktopAuthComplete')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentUser) {
|
||||
if (redirectTo && redirectTo.match(/^\/([^/]|$)/)) {
|
||||
history.push(redirectTo);
|
||||
@@ -596,6 +626,10 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
await postSubmit(userProfile);
|
||||
};
|
||||
|
||||
const postSubmit = async (userProfile: UserProfile) => {
|
||||
if (graphQLEnabled) {
|
||||
await dispatch(loadMe());
|
||||
} else {
|
||||
@@ -754,6 +788,20 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (desktopLoginLink || query.get('desktopAuthComplete')) {
|
||||
return (
|
||||
<Route
|
||||
path={'/login/desktop'}
|
||||
render={() => (
|
||||
<DesktopAuthToken
|
||||
href={desktopLoginLink}
|
||||
onLogin={postSubmit}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
||||
@@ -142,6 +142,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
||||
id="gitlab"
|
||||
key="gitlab"
|
||||
label="GitLab"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"borderColor": "",
|
||||
@@ -315,6 +316,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
||||
id="gitlab"
|
||||
key="gitlab"
|
||||
label="GitLab"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"borderColor": "",
|
||||
@@ -328,6 +330,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
||||
id="google"
|
||||
key="google"
|
||||
label="Google"
|
||||
onClick={[Function]}
|
||||
url="/oauth/google/signup"
|
||||
/>
|
||||
<ExternalLoginButton
|
||||
@@ -335,6 +338,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
||||
id="office365"
|
||||
key="office365"
|
||||
label="Office 365"
|
||||
onClick={[Function]}
|
||||
url="/oauth/office365/signup"
|
||||
/>
|
||||
<ExternalLoginButton
|
||||
@@ -342,6 +346,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
||||
id="openid"
|
||||
key="openid"
|
||||
label="Open ID"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"borderColor": "",
|
||||
@@ -355,6 +360,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
||||
id="ldap"
|
||||
key="ldap"
|
||||
label="AD/LDAP Credentials"
|
||||
onClick={[Function]}
|
||||
url="/login?extra=create_ldap"
|
||||
/>
|
||||
<ExternalLoginButton
|
||||
@@ -362,6 +368,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
||||
id="saml"
|
||||
key="saml"
|
||||
label="SAML"
|
||||
onClick={[Function]}
|
||||
url="/login/sso/saml?action=signup"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import React, {useState, useEffect, useRef, useCallback, FocusEvent} from 'react';
|
||||
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useLocation, useHistory} from 'react-router-dom';
|
||||
import {useLocation, useHistory, Route} from 'react-router-dom';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import throttle from 'lodash/throttle';
|
||||
@@ -54,9 +54,11 @@ import useCWSAvailabilityCheck from 'components/common/hooks/useCWSAvailabilityC
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
import {Constants, HostedCustomerLinks, ItemStatus, ValidationErrors} from 'utils/constants';
|
||||
import {isDesktopApp} from 'utils/user_agent';
|
||||
import {isValidUsername, isValidPassword, getPasswordConfig, getRoleFromTrackFlow, getMediumFromTrackFlow} from 'utils/utils';
|
||||
|
||||
import './signup.scss';
|
||||
import DesktopAuthToken from 'components/desktop_auth_token';
|
||||
|
||||
const MOBILE_SCREEN_WIDTH = 1200;
|
||||
|
||||
@@ -148,6 +150,8 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
const canSubmit = Boolean(email && name && password) && !hasError && !loading;
|
||||
const {error: passwordInfo} = isValidPassword('', getPasswordConfig(config), intl);
|
||||
|
||||
const [desktopLoginLink, setDesktopLoginLink] = useState('');
|
||||
|
||||
const subscribeToSecurityNewsletterFunc = () => {
|
||||
try {
|
||||
Client4.subscribeToNewsletter({email, subscribed_content: 'security_newsletter'});
|
||||
@@ -165,40 +169,48 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
}
|
||||
|
||||
if (enableSignUpWithGitLab) {
|
||||
const url = `${Client4.getOAuthRoute()}/gitlab/signup${search}`;
|
||||
externalLoginOptions.push({
|
||||
id: 'gitlab',
|
||||
url: `${Client4.getOAuthRoute()}/gitlab/signup${search}`,
|
||||
url,
|
||||
icon: <LoginGitlabIcon/>,
|
||||
label: GitLabButtonText || formatMessage({id: 'login.gitlab', defaultMessage: 'GitLab'}),
|
||||
style: {color: GitLabButtonColor, borderColor: GitLabButtonColor},
|
||||
onClick: desktopExternalAuth(url),
|
||||
});
|
||||
}
|
||||
|
||||
if (isLicensed && enableSignUpWithGoogle) {
|
||||
const url = `${Client4.getOAuthRoute()}/google/signup${search}`;
|
||||
externalLoginOptions.push({
|
||||
id: 'google',
|
||||
url: `${Client4.getOAuthRoute()}/google/signup${search}`,
|
||||
url,
|
||||
icon: <LoginGoogleIcon/>,
|
||||
label: formatMessage({id: 'login.google', defaultMessage: 'Google'}),
|
||||
onClick: desktopExternalAuth(url),
|
||||
});
|
||||
}
|
||||
|
||||
if (isLicensed && enableSignUpWithOffice365) {
|
||||
const url = `${Client4.getOAuthRoute()}/office365/signup${search}`;
|
||||
externalLoginOptions.push({
|
||||
id: 'office365',
|
||||
url: `${Client4.getOAuthRoute()}/office365/signup${search}`,
|
||||
url,
|
||||
icon: <LoginOffice365Icon/>,
|
||||
label: formatMessage({id: 'login.office365', defaultMessage: 'Office 365'}),
|
||||
onClick: desktopExternalAuth(url),
|
||||
});
|
||||
}
|
||||
|
||||
if (isLicensed && enableSignUpWithOpenId) {
|
||||
const url = `${Client4.getOAuthRoute()}/openid/signup${search}`;
|
||||
externalLoginOptions.push({
|
||||
id: 'openid',
|
||||
url: `${Client4.getOAuthRoute()}/openid/signup${search}`,
|
||||
url,
|
||||
icon: <LoginOpenIDIcon/>,
|
||||
label: OpenIdButtonText || formatMessage({id: 'login.openid', defaultMessage: 'Open ID'}),
|
||||
style: {color: OpenIdButtonColor, borderColor: OpenIdButtonColor},
|
||||
onClick: desktopExternalAuth(url),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,6 +223,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
url: `${Client4.getUrl()}/login?${newSearchParam.toString()}`,
|
||||
icon: <LockIcon/>,
|
||||
label: LdapLoginFieldName || formatMessage({id: 'signup.ldap', defaultMessage: 'AD/LDAP Credentials'}),
|
||||
onClick: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,11 +231,13 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
const newSearchParam = new URLSearchParams(search);
|
||||
newSearchParam.set('action', 'signup');
|
||||
|
||||
const url = `${Client4.getUrl()}/login/sso/saml?${newSearchParam.toString()}`;
|
||||
externalLoginOptions.push({
|
||||
id: 'saml',
|
||||
url: `${Client4.getUrl()}/login/sso/saml?${newSearchParam.toString()}`,
|
||||
url,
|
||||
icon: <LockIcon/>,
|
||||
label: SamlLoginButtonText || formatMessage({id: 'login.saml', defaultMessage: 'SAML'}),
|
||||
onClick: desktopExternalAuth(url),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,6 +310,17 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
setIsMobileView(window.innerWidth < MOBILE_SCREEN_WIDTH);
|
||||
}, 100);
|
||||
|
||||
const desktopExternalAuth = (href: string) => {
|
||||
return (event: React.MouseEvent) => {
|
||||
if (isDesktopApp()) {
|
||||
event.preventDefault();
|
||||
|
||||
setDesktopLoginLink(href);
|
||||
history.push(`/signup_user_complete/desktop${search}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(removeGlobalItem('team'));
|
||||
trackEvent('signup', 'signup_user_01_welcome', {...getRoleFromTrackFlow(), ...getMediumFromTrackFlow()});
|
||||
@@ -448,6 +474,12 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
await postSignupSuccess();
|
||||
};
|
||||
|
||||
const postSignupSuccess = async () => {
|
||||
const redirectTo = (new URLSearchParams(search)).get('redirect_to');
|
||||
|
||||
if (graphQLEnabled) {
|
||||
await dispatch(loadMe());
|
||||
} else {
|
||||
@@ -695,6 +727,20 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (desktopLoginLink) {
|
||||
return (
|
||||
<Route
|
||||
path={'/signup_user_complete/desktop'}
|
||||
render={() => (
|
||||
<DesktopAuthToken
|
||||
href={desktopLoginLink}
|
||||
onLogin={postSignupSuccess}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let emailCustomLabelForInput: CustomMessageInputType = parsedEmail ? {
|
||||
type: ItemStatus.INFO,
|
||||
value: formatMessage(
|
||||
|
||||
@@ -3288,6 +3288,14 @@
|
||||
"demote_to_user_modal.demote": "Demote",
|
||||
"demote_to_user_modal.desc": "This action demotes the user {username} to a guest. It will restrict the user's ability to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to demote user {username} to guest?",
|
||||
"demote_to_user_modal.title": "Demote User {username} to Guest",
|
||||
"desktop_auth_token.complete.havingTrouble": "Having trouble logging in? <a>Open Mattermost in your browser</a>",
|
||||
"desktop_auth_token.complete.openMattermost": "Click on <b>Open Mattermost</b> in the browser prompt to <a>launch the desktop app</a>",
|
||||
"desktop_auth_token.complete.youAreNowLoggedIn": "You are now logged in",
|
||||
"desktop_auth_token.expired.restartFlow": "Click <a>here</a> to try again.",
|
||||
"desktop_auth_token.expired.somethingWentWrong": "Something went wrong",
|
||||
"desktop_auth_token.polling.awaitingToken": "Authenticating in the browser, awaiting valid token.",
|
||||
"desktop_auth_token.polling.isComplete": "Authentication complete? <a>Check token now</a>",
|
||||
"desktop_auth_token.polling.redirectingToBrowser": "Redirecting to browser...",
|
||||
"device_icons.android": "Android Icon",
|
||||
"device_icons.apple": "Apple Icon",
|
||||
"device_icons.linux": "Linux Icon",
|
||||
|
||||
@@ -756,6 +756,18 @@ export default class Client4 {
|
||||
return profile;
|
||||
};
|
||||
|
||||
loginWithDesktopToken = async (token: string) => {
|
||||
const body: any = {
|
||||
token,
|
||||
deviceId: '',
|
||||
};
|
||||
|
||||
return await this.doFetch<UserProfile>(
|
||||
`${this.getUsersRoute()}/login/desktop_token`,
|
||||
{method: 'post', body: JSON.stringify(body)},
|
||||
);
|
||||
};
|
||||
|
||||
loginById = (id: string, password: string, token = '') => {
|
||||
this.trackEvent('api', 'api_users_login');
|
||||
const body: any = {
|
||||
|
||||
Ссылка в новой задаче
Block a user