// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import NetInfo from '@react-native-community/netinfo'; import {DeviceEventEmitter, Platform} from 'react-native'; import {Database, Events} from '@constants'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; import {getServerCredentials} from '@init/credentials'; 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, getConfig, getLicense} from '@queries/servers/system'; import {getCurrentUser} from '@queries/servers/user'; import EphemeralStore from '@store/ephemeral_store'; import {logWarning, logError} from '@utils/log'; import {scheduleExpiredNotification} from '@utils/notification'; import {getCSRFFromCookie} from '@utils/security'; import {loginEntry} from './entry'; import {fetchDataRetentionPolicy} from './systems'; import type ClientError from '@client/rest/error'; import type {LoginArgs} from '@typings/database/database'; const HTTP_UNAUTHORIZED = 401; export const completeLogin = async (serverUrl: string) => { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return {error: `${serverUrl} database not found`}; } const {database} = operator; const license = await getLicense(database); const config = await getConfig(database); if (!Object.keys(config)?.length || !license || !Object.keys(license)?.length) { return null; } // Data retention if (config?.DataRetentionEnableMessageDeletion === 'true' && license?.IsLicensed === 'true' && license?.DataRetention === 'true') { fetchDataRetentionPolicy(serverUrl); } await DatabaseManager.setActiveServerDatabase(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}); } // Start websocket const credentials = await getServerCredentials(serverUrl); if (credentials?.token) { WebsocketManager.createClient(serverUrl, credentials.token); systems.push({ id: SYSTEM_IDENTIFIERS.WEBSOCKET, value: 0, }); } if (systems.length) { operator.handleSystem({systems, prepareRecordsOnly: false}); } return null; }; export const forceLogoutIfNecessary = async (serverUrl: string, err: ClientErrorProps) => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return {error: `${serverUrl} database not found`}; } const currentUserId = await getCurrentUserId(database); if ('status_code' in err && err.status_code === HTTP_UNAUTHORIZED && err?.url?.indexOf('/login') === -1 && currentUserId) { await logout(serverUrl); } return {error: null}; }; 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 (e) { logError('fetchSessions', e); await forceLogoutIfNecessary(serverUrl, e as ClientError); } return undefined; }; export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaToken, password, config, serverDisplayName}: LoginArgs): Promise => { let deviceToken; let user: UserProfile; const appDatabase = DatabaseManager.appDatabase?.database; if (!appDatabase) { return {error: 'App database not found.', failed: true}; } let client; try { client = NetworkManager.getClient(serverUrl); } catch (error) { return {error: error as Error, failed: true}; } try { 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) { return {error: error as Error, failed: true}; } try { const {error, hasTeams, time} = await loginEntry({serverUrl, user}); completeLogin(serverUrl); return {error: error as ClientError, failed: false, hasTeams, time}; } catch (error) { return {error: error as ClientError, failed: false, time: 0}; } }; export const logout = async (serverUrl: string, skipServerLogout = false, removeServer = false, skipEvents = false) => { if (!skipServerLogout) { try { const client = NetworkManager.getClient(serverUrl); await client.logout(); } 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, error); } } if (!skipEvents) { DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl, removeServer}); } }; 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, }); } } catch (e) { logError('cancelSessionNotification', 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, }); } } } catch (e) { logError('scheduleExpiredNotification', e); await forceLogoutIfNecessary(serverUrl, e as ClientError); } }; export const sendPasswordResetEmail = async (serverUrl: string, email: string) => { let client; try { client = NetworkManager.getClient(serverUrl); } catch (error) { return {error}; } let response; try { response = await client.sendPasswordResetEmail(email); } catch (error) { return {error}; } return { data: response.data, error: undefined, }; }; export const ssoLogin = async (serverUrl: string, serverDisplayName: string, serverIdentifier: string, bearerToken: string, csrfToken: string): Promise => { let deviceToken; let user; const database = DatabaseManager.appDatabase?.database; if (!database) { return {error: 'App database not found', failed: true}; } let client; try { client = NetworkManager.getClient(serverUrl); } catch (error) { return {error: error as Error, failed: true}; } client.setBearerToken(bearerToken); client.setCSRFToken(csrfToken); // Setting up active database for this SSO login flow try { const server = await DatabaseManager.createServerDatabase({ config: { dbName: serverUrl, serverUrl, identifier: serverIdentifier, displayName: serverDisplayName, }, }); deviceToken = await getDeviceToken(); 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 (e) { return {error: e as ClientError, failed: true}; } try { const {error, hasTeams, time} = await loginEntry({serverUrl, user, deviceToken}); completeLogin(serverUrl); return {error: error as ClientError, failed: false, hasTeams, time}; } catch (error) { return {error: error as ClientError, failed: false, time: 0}; } }; 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; }