mattermost-mobile/app/actions/remote/session.ts
Daniel Espino García bb7ff622af
Add Playbooks read-only support for mobile devices (#8978)
* Add the channel options to get into playbooks (#8750)

* Add the channel options to get into playbooks

* Use playbook run id instead of playbook id

* i18n

* Fix issues and move playbooks to the products folder

* Address some tests

* Fix test

* Address design issues

* Add requested comment

---------

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Playbooks database (#8802)

* Add lastPlaybookFetchAt to channel table (#8916)

* Add lastPlaybookFetchAt to channel table

* Add missing commit

* Use my_channel table instead

* Fix test

* Address feedback

* First implementation of playbooks API (#8897)

* First implementation of playbooks API

* Add version check

* Address feedback

* Fix test

* Add last fetch at usage and other improvements

* Simplify test

* Add sort_order, update_at and previousReminder columns (#8927)

* Add sort_order, update_at and previousReminder columns

* Remove order from the schema

* Fix tests

* Add tests

* Add websockets for playbooks (#8947)

* Add websocket events for playbooks

* Fix typo

* Add playbook run list (#8761)

* Add the channel options to get into playbooks

* Use playbook run id instead of playbook id

* i18n

* Fix issues and move playbooks to the products folder

* Address some tests

* Fix test

* Add playbook run list

* Add missing texts

* Add back button support and item size to flash list

* Address design issues

* Add requested comment

* Standardize tag and use it in the card

* Fix merge

* Add API related functionality

---------

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Add playbooks run details (#8872)

* Add the channel options to get into playbooks

* Use playbook run id instead of playbook id

* i18n

* Fix issues and move playbooks to the products folder

* Address some tests

* Fix test

* Add playbook run list

* Add missing texts

* Add back button support and item size to flash list

* Address design issues

* Add requested comment

* Standardize tag and use it in the card

* Add playbooks run details

* Fix merge

* Add API related functionality

* Add API related changes

* Order fixes

* Several fixes

* Add error state on playbook run

* i18n-extract

* Fix tests

* Fix test

* Several fixes

* Fixes and add missing UI elements

* i18n-extract

* Fix tests

* Remove files from bad merge

---------

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Add missing tests for playbooks (#8976)

* Add the channel options to get into playbooks

* Use playbook run id instead of playbook id

* i18n

* Fix issues and move playbooks to the products folder

* Address some tests

* Fix test

* Add playbook run list

* Add missing texts

* Add back button support and item size to flash list

* Address design issues

* Add requested comment

* Standardize tag and use it in the card

* Add playbooks run details

* Fix merge

* Add API related functionality

* Add API related changes

* Order fixes

* Several fixes

* Add error state on playbook run

* i18n-extract

* Fix tests

* Fix test

* Several fixes

* Fixes and add missing UI elements

* i18n-extract

* Fix tests

* Remove files from bad merge

* Add tests

* Fix typo

* Add missing strings

* Fix tests and skip some

* Fix test

* Fix typo

---------

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Address feedback

* Address feedback and fix tests

* Address comments and fix tests

* Address feedback

* Address plugin changes and fix bugs

---------

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2025-07-14 09:21:37 +02:00

389 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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: 'Youre 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): Promise<LoginActionResponse> => {
const database = DatabaseManager.appDatabase?.database;
if (!database) {
return {error: 'App database not found', failed: true};
}
try {
const client = NetworkManager.getClient(serverUrl);
client.setBearerToken(bearerToken);
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;
}