* feat: add shared server password to server setup * feat: allow editing the sever * refactor: changed password -> secret, styling and tests * e2e: draft e2e tests * chore: lint fix * feat: also send preauth secret header when using native share * fix: removed unused server database migration credentials are being stored in the keychain * i18n: added missing english translations * test(e2e): simplified connection tests * test(e2e): rework * refactor: remove setBearerToken * chore: restore migrations the way it was * chore: reverted file to original state * chore: removed unneeded test and renamed password to secret * chore: function version * chore: updated forms i18n keys * chore: remove if from test * chore: unneeded variable * fix: add missing key on object list * refactor: swift keychain access to retrieve all credentials in one call * revert: edit server screen * refactor: credentials use getGenericCredential * fix: objc code calling old method * fix: added scroll to login screen * chore: variable names * fix: avoid inline styles * fix: Improved appVersion positioning * Update app/screens/server/form.tsx Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * feat: show error message on 403 * Revert "feat: show error message on 403" This reverts commit f41630c767e10211adf1885321ceefd8a0931e32. --------- Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>
389 lines
14 KiB
TypeScript
389 lines
14 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||
// See LICENSE.txt for license information.
|
||
|
||
import NetInfo from '@react-native-community/netinfo';
|
||
import {defineMessages, type IntlShape} from 'react-intl';
|
||
import {Alert, DeviceEventEmitter, Platform, type AlertButton} from 'react-native';
|
||
|
||
import {Database, Events} from '@constants';
|
||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||
import DatabaseManager from '@database/manager';
|
||
import PushNotifications from '@init/push_notifications';
|
||
import NetworkManager from '@managers/network_manager';
|
||
import WebsocketManager from '@managers/websocket_manager';
|
||
import {getDeviceToken} from '@queries/app/global';
|
||
import {getServerDisplayName} from '@queries/app/servers';
|
||
import {getCurrentUserId, getExpiredSession} from '@queries/servers/system';
|
||
import {getCurrentUser} from '@queries/servers/user';
|
||
import EphemeralStore from '@store/ephemeral_store';
|
||
import {getFullErrorMessage, isErrorWithStatusCode, isErrorWithUrl} from '@utils/errors';
|
||
import {logWarning, logError, logDebug} from '@utils/log';
|
||
import {scheduleExpiredNotification} from '@utils/notification';
|
||
import {getCSRFFromCookie} from '@utils/security';
|
||
|
||
import {loginEntry} from './entry';
|
||
|
||
import type {LoginArgs} from '@typings/database/database';
|
||
|
||
const HTTP_UNAUTHORIZED = 401;
|
||
|
||
const logoutMessages = defineMessages({
|
||
title: {
|
||
id: 'logout.fail.title',
|
||
defaultMessage: 'Logout not complete',
|
||
},
|
||
bodyForced: {
|
||
id: 'logout.fail.message.forced',
|
||
defaultMessage: 'We could not log you out of the server. Some data may continue to be accessible to this device once the device goes back online.',
|
||
},
|
||
body: {
|
||
id: 'logout.fail.message',
|
||
defaultMessage: 'You’re not fully logged out. Some data may continue to be accessible to this device once the device goes back online. What do you want to do?',
|
||
},
|
||
cancel: {
|
||
id: 'logout.fail.cancel',
|
||
defaultMessage: 'Cancel',
|
||
},
|
||
continue: {
|
||
id: 'logout.fail.continue_anyway',
|
||
defaultMessage: 'Continue Anyway',
|
||
},
|
||
ok: {
|
||
id: 'logout.fail.ok',
|
||
defaultMessage: 'OK',
|
||
},
|
||
});
|
||
|
||
export const addPushProxyVerificationStateFromLogin = async (serverUrl: string) => {
|
||
try {
|
||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||
|
||
const systems: IdValue[] = [];
|
||
|
||
// Set push proxy verification
|
||
const ppVerification = EphemeralStore.getPushProxyVerificationState(serverUrl);
|
||
if (ppVerification) {
|
||
systems.push({id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: ppVerification});
|
||
}
|
||
|
||
if (systems.length) {
|
||
await operator.handleSystem({systems, prepareRecordsOnly: false});
|
||
}
|
||
|
||
return {};
|
||
} catch (error) {
|
||
logDebug('error setting the push proxy verification state on login', error);
|
||
return {error};
|
||
}
|
||
};
|
||
export const forceLogoutIfNecessary = async (serverUrl: string, err: unknown) => {
|
||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||
if (!database) {
|
||
return {error: `${serverUrl} database not found`, logout: false};
|
||
}
|
||
|
||
const currentUserId = await getCurrentUserId(database);
|
||
|
||
if (isErrorWithStatusCode(err) && err.status_code === HTTP_UNAUTHORIZED && isErrorWithUrl(err) && err.url?.indexOf('/login') === -1 && currentUserId) {
|
||
await logout(serverUrl, undefined, {skipServerLogout: true});
|
||
return {error: null, logout: true};
|
||
}
|
||
|
||
return {error: null, logout: false};
|
||
};
|
||
|
||
export const fetchSessions = async (serverUrl: string, currentUserId: string) => {
|
||
let client;
|
||
try {
|
||
client = NetworkManager.getClient(serverUrl);
|
||
} catch {
|
||
return undefined;
|
||
}
|
||
|
||
try {
|
||
return await client.getSessions(currentUserId);
|
||
} catch (error) {
|
||
logDebug('error on fetchSessions', getFullErrorMessage(error));
|
||
await forceLogoutIfNecessary(serverUrl, error);
|
||
}
|
||
|
||
return undefined;
|
||
};
|
||
|
||
export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaToken, password, config, serverDisplayName}: LoginArgs): Promise<LoginActionResponse> => {
|
||
let deviceToken;
|
||
let user: UserProfile;
|
||
|
||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||
if (!appDatabase) {
|
||
return {error: 'App database not found.', failed: true};
|
||
}
|
||
|
||
try {
|
||
const client = NetworkManager.getClient(serverUrl);
|
||
deviceToken = await getDeviceToken();
|
||
user = await client.login(
|
||
loginId,
|
||
password,
|
||
mfaToken,
|
||
deviceToken,
|
||
ldapOnly,
|
||
);
|
||
|
||
const server = await DatabaseManager.createServerDatabase({
|
||
config: {
|
||
dbName: serverUrl,
|
||
serverUrl,
|
||
identifier: config.DiagnosticId,
|
||
displayName: serverDisplayName,
|
||
},
|
||
});
|
||
|
||
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
||
await server?.operator.handleSystem({
|
||
systems: [{
|
||
id: Database.SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
|
||
value: user.id,
|
||
}],
|
||
prepareRecordsOnly: false,
|
||
});
|
||
const csrfToken = await getCSRFFromCookie(serverUrl);
|
||
client.setCSRFToken(csrfToken);
|
||
} catch (error) {
|
||
logDebug('error on login', getFullErrorMessage(error));
|
||
return {error, failed: true};
|
||
}
|
||
|
||
try {
|
||
await addPushProxyVerificationStateFromLogin(serverUrl);
|
||
const {error} = await loginEntry({serverUrl});
|
||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||
return {error, failed: false};
|
||
} catch (error) {
|
||
return {error, failed: false};
|
||
}
|
||
};
|
||
|
||
type LogoutOptions = {
|
||
skipServerLogout?: boolean;
|
||
removeServer?: boolean;
|
||
skipEvents?: boolean;
|
||
logoutOnAlert?: boolean;
|
||
};
|
||
|
||
export const logout = async (
|
||
serverUrl: string,
|
||
intl: IntlShape | undefined,
|
||
{
|
||
skipServerLogout = false,
|
||
removeServer = false,
|
||
skipEvents = false,
|
||
logoutOnAlert = false,
|
||
}: LogoutOptions = {}) => {
|
||
if (!skipServerLogout) {
|
||
let loggedOut = false;
|
||
try {
|
||
const client = NetworkManager.getClient(serverUrl);
|
||
const response = await client.logout();
|
||
if (response.status === 'OK') {
|
||
loggedOut = true;
|
||
}
|
||
} catch (error) {
|
||
// We want to log the user even if logging out from the server failed
|
||
logWarning('An error occurred logging out from the server', serverUrl, getFullErrorMessage(error));
|
||
}
|
||
|
||
if (!loggedOut) {
|
||
const title = intl?.formatMessage(logoutMessages.title) || logoutMessages.title.defaultMessage;
|
||
|
||
const bodyMessage = logoutOnAlert ? logoutMessages.bodyForced : logoutMessages.body;
|
||
const confirmMessage = logoutOnAlert ? logoutMessages.ok : logoutMessages.continue;
|
||
const body = intl?.formatMessage(bodyMessage) || bodyMessage.defaultMessage;
|
||
const cancel = intl?.formatMessage(logoutMessages.cancel) || logoutMessages.cancel.defaultMessage;
|
||
const confirm = intl?.formatMessage(confirmMessage) || confirmMessage.defaultMessage;
|
||
|
||
const buttons: AlertButton[] = logoutOnAlert ? [] : [{text: cancel, style: 'cancel'}];
|
||
buttons.push({
|
||
text: confirm,
|
||
onPress: logoutOnAlert ? undefined : () => {
|
||
logout(serverUrl, intl, {skipEvents, removeServer, logoutOnAlert, skipServerLogout: true});
|
||
},
|
||
});
|
||
Alert.alert(
|
||
title,
|
||
body,
|
||
buttons,
|
||
);
|
||
|
||
if (!logoutOnAlert) {
|
||
return {data: false};
|
||
}
|
||
}
|
||
}
|
||
|
||
WebsocketManager.getClient(serverUrl)?.close(true);
|
||
if (!skipEvents) {
|
||
DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl, removeServer});
|
||
}
|
||
|
||
return {data: true};
|
||
};
|
||
|
||
export const cancelSessionNotification = async (serverUrl: string) => {
|
||
try {
|
||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||
const expiredSession = await getExpiredSession(database);
|
||
const rechable = (await NetInfo.fetch()).isInternetReachable;
|
||
|
||
if (expiredSession?.notificationId && rechable) {
|
||
PushNotifications.cancelScheduleNotification(parseInt(expiredSession.notificationId, 10));
|
||
operator.handleSystem({
|
||
systems: [{
|
||
id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION,
|
||
value: '',
|
||
}],
|
||
prepareRecordsOnly: false,
|
||
});
|
||
}
|
||
|
||
return {};
|
||
} catch (e) {
|
||
logError('cancelSessionNotification', e);
|
||
return {error: e};
|
||
}
|
||
};
|
||
|
||
export const scheduleSessionNotification = async (serverUrl: string) => {
|
||
try {
|
||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||
const sessions = await fetchSessions(serverUrl, 'me');
|
||
const user = await getCurrentUser(database);
|
||
const serverName = await getServerDisplayName(serverUrl);
|
||
|
||
await cancelSessionNotification(serverUrl);
|
||
|
||
if (sessions) {
|
||
const session = await findSession(serverUrl, sessions);
|
||
|
||
if (session) {
|
||
const sessionId = session.id;
|
||
const notificationId = scheduleExpiredNotification(serverUrl, session, serverName, user?.locale);
|
||
operator.handleSystem({
|
||
systems: [{
|
||
id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION,
|
||
value: {
|
||
id: sessionId,
|
||
notificationId,
|
||
expiresAt: session.expires_at,
|
||
},
|
||
}],
|
||
prepareRecordsOnly: false,
|
||
});
|
||
}
|
||
}
|
||
return {};
|
||
} catch (e) {
|
||
logError('scheduleExpiredNotification', e);
|
||
await forceLogoutIfNecessary(serverUrl, e);
|
||
return {error: e};
|
||
}
|
||
};
|
||
|
||
export const sendPasswordResetEmail = async (serverUrl: string, email: string) => {
|
||
try {
|
||
const client = NetworkManager.getClient(serverUrl);
|
||
const response = await client.sendPasswordResetEmail(email);
|
||
return {status: response.status};
|
||
} catch (error) {
|
||
logDebug('error on sendPasswordResetEmail', getFullErrorMessage(error));
|
||
return {error};
|
||
}
|
||
};
|
||
|
||
export const ssoLogin = async (serverUrl: string, serverDisplayName: string, serverIdentifier: string, bearerToken: string, csrfToken: string, preauthSecret?: string): Promise<LoginActionResponse> => {
|
||
const database = DatabaseManager.appDatabase?.database;
|
||
if (!database) {
|
||
return {error: 'App database not found', failed: true};
|
||
}
|
||
|
||
try {
|
||
const client = NetworkManager.getClient(serverUrl);
|
||
|
||
client.setClientCredentials(bearerToken, preauthSecret);
|
||
client.setCSRFToken(csrfToken);
|
||
|
||
// Setting up active database for this SSO login flow
|
||
const server = await DatabaseManager.createServerDatabase({
|
||
config: {
|
||
dbName: serverUrl,
|
||
serverUrl,
|
||
identifier: serverIdentifier,
|
||
displayName: serverDisplayName,
|
||
},
|
||
});
|
||
const user = await client.getMe();
|
||
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
||
await server?.operator.handleSystem({
|
||
systems: [{
|
||
id: Database.SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
|
||
value: user.id,
|
||
}],
|
||
prepareRecordsOnly: false,
|
||
});
|
||
} catch (error) {
|
||
logDebug('error on ssoLogin', getFullErrorMessage(error));
|
||
return {error, failed: true};
|
||
}
|
||
|
||
try {
|
||
await addPushProxyVerificationStateFromLogin(serverUrl);
|
||
const {error} = await loginEntry({serverUrl});
|
||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||
return {error, failed: false};
|
||
} catch (error) {
|
||
return {error, failed: false};
|
||
}
|
||
};
|
||
|
||
export async function findSession(serverUrl: string, sessions: Session[]) {
|
||
try {
|
||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||
const expiredSession = await getExpiredSession(database);
|
||
const deviceToken = await getDeviceToken();
|
||
|
||
// First try and find the session by the given identifier hyqddef7jjdktqiyy36gxa8sqy
|
||
let session = sessions.find((s) => s.id === expiredSession?.id);
|
||
if (session) {
|
||
return session;
|
||
}
|
||
|
||
// Next try and find the session by deviceId
|
||
if (deviceToken) {
|
||
session = sessions.find((s) => s.device_id === deviceToken);
|
||
if (session) {
|
||
return session;
|
||
}
|
||
}
|
||
|
||
// Next try and find the session by the CSRF token
|
||
const csrfToken = await getCSRFFromCookie(serverUrl);
|
||
if (csrfToken) {
|
||
session = sessions.find((s) => s.props?.csrf === csrfToken);
|
||
if (session) {
|
||
return session;
|
||
}
|
||
}
|
||
|
||
// Next try and find the session based on the OS
|
||
// if multiple sessions exists with the same os type this can be inaccurate
|
||
session = sessions.find((s) => s.props?.os.toLowerCase() === Platform.OS);
|
||
if (session) {
|
||
return session;
|
||
}
|
||
} catch (e) {
|
||
logError('findSession', e);
|
||
}
|
||
|
||
// At this point we did not find the session
|
||
return undefined;
|
||
}
|