feat: show pre-auth secret error on field on server create (#9102)
* feat: show potential pre-auth secret error on server create * chroe: address comments * chore: updated message * feat: read response header to check error source * fix: i18n * chore: rename pre-auth to just auth
This commit is contained in:
parent
0835f6e6aa
commit
432cfb08fe
8 changed files with 76 additions and 29 deletions
|
|
@ -15,6 +15,7 @@ import {logDebug} from '@utils/log';
|
|||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type {ClientResponse} from '@mattermost/react-native-network-client';
|
||||
|
||||
async function getDeviceIdForPing(serverUrl: string, checkDeviceId: boolean) {
|
||||
|
|
@ -66,12 +67,21 @@ export const doPing = async (serverUrl: string, verifyPushProxy: boolean, timeou
|
|||
}
|
||||
|
||||
if (!response.ok) {
|
||||
logDebug('Server ping returned not ok response', response);
|
||||
NetworkManager.invalidateClient(serverUrl);
|
||||
if (response.code === 403 && response.headers?.['x-reject-reason'] === 'pre-auth') {
|
||||
return {error: {intl: pingError}, isPreauthError: true};
|
||||
}
|
||||
return {error: {intl: pingError}};
|
||||
}
|
||||
} catch (error) {
|
||||
logDebug('Server ping threw an exception', getFullErrorMessage(error));
|
||||
// Check if this is a 403 with pre-auth header
|
||||
const errorObj = error as ClientError;
|
||||
if (errorObj.status_code === 403) {
|
||||
if (errorObj.headers?.['x-reject-reason'] === 'pre-auth') {
|
||||
return {error: {intl: pingError}, isPreauthError: true};
|
||||
}
|
||||
}
|
||||
|
||||
NetworkManager.invalidateClient(serverUrl);
|
||||
return {error: {intl: pingError}};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export default class ClientError extends Error {
|
|||
server_error_id?: string;
|
||||
status_code?: number;
|
||||
details?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
constructor(baseUrl: string, data: ClientErrorProps) {
|
||||
super(data.message + ': ' + cleanUrlForLogging(baseUrl, data.url));
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ export default class ClientError extends Error {
|
|||
this.server_error_id = data.server_error_id;
|
||||
this.status_code = data.status_code;
|
||||
this.details = data.details;
|
||||
this.headers = data.headers;
|
||||
|
||||
// Ensure message is treated as a property of this class when object spreading. Without this,
|
||||
// copying the object by using `{...error}` would not include the message.
|
||||
|
|
|
|||
|
|
@ -387,6 +387,7 @@ export default class ClientTracking {
|
|||
try {
|
||||
response = await request!(url, this.buildRequestOptions(options));
|
||||
} catch (error) {
|
||||
const response_error = error as ClientError;
|
||||
const status_code = isErrorWithStatusCode(error) ? error.status_code : undefined;
|
||||
throw new ClientError(this.apiClient.baseUrl, {
|
||||
message: 'Received invalid response from the server.',
|
||||
|
|
@ -395,7 +396,8 @@ export default class ClientTracking {
|
|||
defaultMessage: 'Received invalid response from the server.',
|
||||
}),
|
||||
url,
|
||||
details: error,
|
||||
details: response_error,
|
||||
headers: response_error.headers ? response_error.headers : undefined,
|
||||
status_code,
|
||||
});
|
||||
} finally {
|
||||
|
|
@ -433,6 +435,7 @@ export default class ClientTracking {
|
|||
server_error_id: response.data?.id as string,
|
||||
status_code: response.code,
|
||||
url,
|
||||
headers,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type RefObject, useCallback, useRef, useState} from 'react';
|
||||
import React, {type RefObject, useCallback, useRef} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {Keyboard, Pressable, View} from 'react-native';
|
||||
|
||||
|
|
@ -28,6 +28,9 @@ type Props = {
|
|||
handleUrlTextChanged: (text: string) => void;
|
||||
keyboardAwareRef: RefObject<KeyboardAwareScrollView>;
|
||||
preauthSecret?: string;
|
||||
preauthSecretError?: string;
|
||||
setShowAdvancedOptions: (show: boolean) => void;
|
||||
showAdvancedOptions: boolean;
|
||||
theme: Theme;
|
||||
url?: string;
|
||||
urlError?: string;
|
||||
|
|
@ -93,11 +96,15 @@ const messages = defineMessages({
|
|||
},
|
||||
preauthSecret: {
|
||||
id: 'mobile.components.select_server_view.sharedSecret',
|
||||
defaultMessage: 'Pre-authentication secret',
|
||||
defaultMessage: 'Authentication secret',
|
||||
},
|
||||
preauthSecretHelp: {
|
||||
id: 'mobile.components.select_server_view.sharedSecretHelp',
|
||||
defaultMessage: 'The pre-authentication secret shared by the administrator',
|
||||
defaultMessage: 'The authentication secret shared by the administrator',
|
||||
},
|
||||
preauthSecretInvalid: {
|
||||
id: 'mobile.server.preauth_secret.invalid',
|
||||
defaultMessage: 'Authentication secret is invalid. Try again or contact your admin.',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -114,6 +121,9 @@ const ServerForm = ({
|
|||
handleUrlTextChanged,
|
||||
keyboardAwareRef,
|
||||
preauthSecret = '',
|
||||
preauthSecretError,
|
||||
setShowAdvancedOptions,
|
||||
showAdvancedOptions,
|
||||
theme,
|
||||
url = '',
|
||||
urlError,
|
||||
|
|
@ -124,8 +134,6 @@ const ServerForm = ({
|
|||
const urlRef = useRef<FloatingTextInputRef>(null);
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
|
||||
useAvoidKeyboard(keyboardAwareRef);
|
||||
|
||||
const onConnect = useCallback(() => {
|
||||
|
|
@ -138,12 +146,12 @@ const ServerForm = ({
|
|||
}, []);
|
||||
|
||||
const onDisplayNameSubmit = useCallback(() => {
|
||||
if (showAdvancedOptions) {
|
||||
if (showAdvancedOptions || preauthSecretError) {
|
||||
preauthSecretRef.current?.focus();
|
||||
} else {
|
||||
onConnect();
|
||||
}
|
||||
}, [showAdvancedOptions, onConnect]);
|
||||
}, [showAdvancedOptions, preauthSecretError, onConnect]);
|
||||
|
||||
const toggleAdvancedOptions = useCallback(() => {
|
||||
setShowAdvancedOptions(!showAdvancedOptions);
|
||||
|
|
@ -190,7 +198,7 @@ const ServerForm = ({
|
|||
onChangeText={handleDisplayNameTextChanged}
|
||||
onSubmitEditing={onDisplayNameSubmit}
|
||||
ref={displayNameRef}
|
||||
returnKeyType={showAdvancedOptions ? 'next' : 'done'}
|
||||
returnKeyType={showAdvancedOptions || preauthSecretError ? 'next' : 'done'}
|
||||
spellCheck={false}
|
||||
testID='server_form.server_display_name.input'
|
||||
theme={theme}
|
||||
|
|
@ -231,6 +239,7 @@ const ServerForm = ({
|
|||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
error={preauthSecretError}
|
||||
label={formatMessage(messages.preauthSecret)}
|
||||
onChangeText={handlePreauthSecretTextChanged}
|
||||
onSubmitEditing={onConnect}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ const Server = ({
|
|||
const [url, setUrl] = useState<string>('');
|
||||
const [displayNameError, setDisplayNameError] = useState<string | undefined>();
|
||||
const [urlError, setUrlError] = useState<string | undefined>();
|
||||
const [preauthSecretError, setPreauthSecretError] = useState<string | undefined>();
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState<boolean>(false);
|
||||
const styles = getStyleSheet(theme);
|
||||
const {formatMessage} = intl;
|
||||
const disableServerUrl = Boolean(managedConfig?.allowOtherServers === 'false' && managedConfig?.serverUrl);
|
||||
|
|
@ -146,12 +148,12 @@ const Server = ({
|
|||
}, [managedConfig?.allowOtherServers, managedConfig?.serverUrl, managedConfig?.serverName, defaultServerUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (url && displayName) {
|
||||
if (url && displayName && !urlError && !preauthSecretError) {
|
||||
setButtonDisabled(false);
|
||||
} else {
|
||||
setButtonDisabled(true);
|
||||
}
|
||||
}, [url, displayName]);
|
||||
}, [url, displayName, urlError, preauthSecretError]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = {
|
||||
|
|
@ -277,7 +279,15 @@ const Server = ({
|
|||
|
||||
const handlePreauthSecretTextChanged = useCallback((text: string) => {
|
||||
setPreauthSecret(text);
|
||||
}, []);
|
||||
|
||||
// Clear any connection errors when preauth secret is modified
|
||||
if (urlError) {
|
||||
setUrlError(undefined);
|
||||
}
|
||||
if (preauthSecretError) {
|
||||
setPreauthSecretError(undefined);
|
||||
}
|
||||
}, [urlError, preauthSecretError]);
|
||||
|
||||
const isServerUrlValid = (serverUrl?: string) => {
|
||||
const testUrl = sanitizeUrl(serverUrl ?? url);
|
||||
|
|
@ -300,34 +310,42 @@ const Server = ({
|
|||
cancelPing = undefined;
|
||||
};
|
||||
|
||||
const ping = await getServerUrlAfterRedirect(pingUrl, !retryWithHttp, preauthSecret.trim() || undefined);
|
||||
if (!ping.url) {
|
||||
const headRequest = await getServerUrlAfterRedirect(pingUrl, !retryWithHttp, preauthSecret.trim() || undefined);
|
||||
if (!headRequest.url) {
|
||||
cancelPing();
|
||||
if (retryWithHttp) {
|
||||
const nurl = pingUrl.replace('https:', 'http:');
|
||||
pingServer(nurl, false);
|
||||
} else {
|
||||
setUrlError(getErrorMessage(ping.error, intl));
|
||||
setUrlError(getErrorMessage(headRequest.error, intl));
|
||||
setButtonDisabled(true);
|
||||
setConnecting(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const result = await doPing(ping.url, true, managedConfig?.timeout ? parseInt(managedConfig?.timeout, 10) : undefined, preauthSecret.trim() || undefined);
|
||||
const result = await doPing(headRequest.url, true, managedConfig?.timeout ? parseInt(managedConfig?.timeout, 10) : undefined, preauthSecret.trim() || undefined);
|
||||
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
setUrlError(getErrorMessage(result.error, intl));
|
||||
if (result.isPreauthError) {
|
||||
setPreauthSecretError(intl.formatMessage({
|
||||
id: 'mobile.server.preauth_secret.invalid',
|
||||
defaultMessage: 'Authentication secret is invalid. Try again or contact your admin.',
|
||||
}));
|
||||
setShowAdvancedOptions(true);
|
||||
} else {
|
||||
setUrlError(getErrorMessage(result.error, intl));
|
||||
}
|
||||
setButtonDisabled(true);
|
||||
setConnecting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
canReceiveNotifications(ping.url, result.canReceiveNotifications as string, intl);
|
||||
const data = await fetchConfigAndLicense(ping.url, true);
|
||||
canReceiveNotifications(headRequest.url, result.canReceiveNotifications as string, intl);
|
||||
const data = await fetchConfigAndLicense(headRequest.url, true);
|
||||
if (data.error) {
|
||||
setButtonDisabled(true);
|
||||
setUrlError(getErrorMessage(data.error, intl));
|
||||
|
|
@ -345,7 +363,7 @@ const Server = ({
|
|||
}
|
||||
|
||||
if (data.config.MobileJailbreakProtection === 'true') {
|
||||
const isJailbroken = await SecurityManager.isDeviceJailbroken(ping.url, data.config.SiteName);
|
||||
const isJailbroken = await SecurityManager.isDeviceJailbroken(headRequest.url, data.config.SiteName);
|
||||
if (isJailbroken) {
|
||||
setConnecting(false);
|
||||
return;
|
||||
|
|
@ -353,7 +371,7 @@ const Server = ({
|
|||
}
|
||||
|
||||
if (data.config.MobileEnableBiometrics === 'true') {
|
||||
const biometricsResult = await SecurityManager.authenticateWithBiometrics(ping.url, data.config.SiteName);
|
||||
const biometricsResult = await SecurityManager.authenticateWithBiometrics(headRequest.url, data.config.SiteName);
|
||||
if (!biometricsResult) {
|
||||
setConnecting(false);
|
||||
return;
|
||||
|
|
@ -361,7 +379,7 @@ const Server = ({
|
|||
}
|
||||
|
||||
const server = await getServerByIdentifier(data.config.DiagnosticId);
|
||||
const credentials = await getServerCredentials(ping.url);
|
||||
const credentials = await getServerCredentials(headRequest.url);
|
||||
setConnecting(false);
|
||||
|
||||
if (server && server.lastActiveAt > 0 && credentials?.token) {
|
||||
|
|
@ -373,7 +391,7 @@ const Server = ({
|
|||
return;
|
||||
}
|
||||
|
||||
displayLogin(ping.url, data.config!, data.license!);
|
||||
displayLogin(headRequest.url, data.config!, data.license!);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -417,6 +435,9 @@ const Server = ({
|
|||
handleUrlTextChanged={handleUrlTextChanged}
|
||||
keyboardAwareRef={keyboardAwareRef}
|
||||
preauthSecret={preauthSecret}
|
||||
preauthSecretError={preauthSecretError}
|
||||
setShowAdvancedOptions={setShowAdvancedOptions}
|
||||
showAdvancedOptions={showAdvancedOptions}
|
||||
theme={theme}
|
||||
url={url}
|
||||
urlError={urlError}
|
||||
|
|
|
|||
|
|
@ -632,8 +632,8 @@
|
|||
"mobile.components.select_server_view.msg_description": "A server is your team's communication hub accessed using a unique URL",
|
||||
"mobile.components.select_server_view.msg_welcome": "Welcome",
|
||||
"mobile.components.select_server_view.proceed": "Proceed",
|
||||
"mobile.components.select_server_view.sharedSecret": "Pre-authentication secret",
|
||||
"mobile.components.select_server_view.sharedSecretHelp": "The pre-authentication secret shared by the administrator",
|
||||
"mobile.components.select_server_view.sharedSecret": "Authentication secret",
|
||||
"mobile.components.select_server_view.sharedSecretHelp": "The authentication secret shared by the administrator",
|
||||
"mobile.create_channel": "Create",
|
||||
"mobile.create_channel.title": "New channel",
|
||||
"mobile.create_direct_message.max_limit_reached": "Group messages are limited to {maxCount} members",
|
||||
|
|
@ -849,6 +849,7 @@
|
|||
"mobile.server_url.deeplink.emm.denied": "This app is controlled by an EMM and the DeepLink server url does not match the EMM allowed server",
|
||||
"mobile.server_url.empty": "Please enter a valid server URL",
|
||||
"mobile.server_url.invalid_format": "URL must start with http:// or https://",
|
||||
"mobile.server.preauth_secret.invalid": "Authentication secret is invalid. Try again or contact your admin.",
|
||||
"mobile.session_expired_days": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.",
|
||||
"mobile.session_expired_days_hrs": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}} and {hoursCount, number} {hoursCount, plural, one {hour} other {hours}}.",
|
||||
"mobile.session_expired_hrs": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {hoursCount, number} {hoursCount, plural, one {hour} other {hours}}.",
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@ describe('Server Login - Preauth Secret Connection', () => {
|
|||
// * Verify preauth secret field is now visible with correct text and styling
|
||||
await expect(preauthSecretInput).toBeVisible();
|
||||
await expect(preauthSecretHelp).toBeVisible();
|
||||
await expect(element(by.text('Pre-authentication secret'))).toBeVisible();
|
||||
await expect(preauthSecretHelp).toHaveText('The pre-authentication secret shared by the administrator');
|
||||
await expect(element(by.text('Authentication secret'))).toBeVisible();
|
||||
await expect(preauthSecretHelp).toHaveText('The authentication secret shared by the administrator');
|
||||
|
||||
// # Toggle advanced options again to hide
|
||||
await ServerScreen.toggleAdvancedOptions();
|
||||
|
|
|
|||
1
types/api/client.d.ts
vendored
1
types/api/client.d.ts
vendored
|
|
@ -27,5 +27,6 @@ interface ClientErrorProps {
|
|||
url: string;
|
||||
server_error_id?: string;
|
||||
status_code?: number;
|
||||
headers?: Record<string, string>;
|
||||
message: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue