diff --git a/app/definitions/ILoggedUser.ts b/app/definitions/ILoggedUser.ts index 380bcd86c..0b083cd9a 100644 --- a/app/definitions/ILoggedUser.ts +++ b/app/definitions/ILoggedUser.ts @@ -8,11 +8,25 @@ export interface ILoggedUser { language?: string; status: string; statusText?: string; + customFields: object; + statusLivechat: string; + emails: string[]; roles: string[]; avatarETag?: string; - showMessageInMainThread: boolean; isFromWebView: boolean; - enableMessageParserEarlyAdoption?: boolean; + settings?: { + preferences: { + showMessageInMainThread: boolean; + enableMessageParserEarlyAdoption: boolean; + }; + }; +} + +export interface ILoginResult { + status: string; + authToken: string; + userId: string; + me: ILoggedUser; } export type TLoggedUserModel = ILoggedUser & Model; diff --git a/app/lib/rocketchat/rocketchat.js b/app/lib/rocketchat/rocketchat.js index b2171eacd..6a38c6d08 100644 --- a/app/lib/rocketchat/rocketchat.js +++ b/app/lib/rocketchat/rocketchat.js @@ -74,7 +74,7 @@ const CERTIFICATE_KEY = 'RC_CERTIFICATE_KEY'; export const THEME_PREFERENCES_KEY = 'RC_THEME_PREFERENCES_KEY'; export const CRASH_REPORT_KEY = 'RC_CRASH_REPORT_KEY'; export const ANALYTICS_EVENTS_KEY = 'RC_ANALYTICS_EVENTS_KEY'; -const MIN_ROCKETCHAT_VERSION = '0.70.0'; +export const MIN_ROCKETCHAT_VERSION = '0.70.0'; const STATUSES = ['offline', 'online', 'away', 'busy']; diff --git a/app/lib/rocketchat/services/connect.ts b/app/lib/rocketchat/services/connect.ts new file mode 100644 index 000000000..bf9aeca1b --- /dev/null +++ b/app/lib/rocketchat/services/connect.ts @@ -0,0 +1,222 @@ +import RNFetchBlob from 'rn-fetch-blob'; +import { Rocketchat as RocketchatClient, settings as RocketChatSettings } from '@rocket.chat/sdk'; + +import { useSsl } from '../../../utils/url'; +import { selectServerFailure } from '../../../actions/server'; +import { twoFactor } from '../../../utils/twoFactor'; +import { compareServerVersion } from '../../utils'; +import { store } from '../../auxStore'; +import { loginRequest, setUser } from '../../../actions/login'; +import sdk from './sdk'; +import I18n from '../../../i18n'; +import { MIN_ROCKETCHAT_VERSION } from '../rocketchat'; +import { ILoggedUser } from '../../../definitions'; + +interface ICredentials { + user?: string; + password?: string; + username?: string; + ldapPass?: string; + ldap?: boolean; + ldapOptions?: object; + crowdPassword?: string; + crowd?: boolean; + code?: string; + totp?: { + login: ICredentials; + code: string; + }; +} + +async function login(credentials: ICredentials, isFromWebView = false) { + // RC 0.64.0 + await sdk.login(credentials); + const result = sdk.currentLogin?.result; + if (result) { + const user = { + id: result.userId, + token: result.authToken, + username: result.me.username, + name: result.me.name, + language: result.me.language, + status: result.me.status, + statusText: result.me.statusText, + customFields: result.me.customFields, + statusLivechat: result.me.statusLivechat, + emails: result.me.emails, + roles: result.me.roles, + avatarETag: result.me.avatarETag, + isFromWebView, + showMessageInMainThread: result.me.settings?.preferences?.showMessageInMainThread ?? true, + enableMessageParserEarlyAdoption: result.me.settings?.preferences?.enableMessageParserEarlyAdoption ?? true + }; + return user; + } +} + +function loginTOTP(params: ICredentials, loginEmailPassword?: boolean, isFromWebView = false): Promise { + return new Promise(async (resolve, reject) => { + try { + const result = await login(params, isFromWebView); + if (result) { + return resolve(result); + } + } catch (e: any) { + if (e.data?.error && (e.data.error === 'totp-required' || e.data.error === 'totp-invalid')) { + const { details } = e.data; + try { + const code = await twoFactor({ method: details?.method || 'totp', invalid: details?.error === 'totp-invalid' }); + + if (loginEmailPassword) { + store.dispatch(setUser({ username: params.user || params.username })); + + // Force normalized params for 2FA starting RC 3.9.0. + const serverVersion = store.getState().server.version; + if (compareServerVersion(serverVersion as string, 'greaterThanOrEqualTo', '3.9.0')) { + const user = params.user ?? params.username; + const password = params.password ?? params.ldapPass ?? params.crowdPassword; + params = { user, password }; + } + + return resolve(loginTOTP({ ...params, code: code?.twoFactorCode }, loginEmailPassword)); + } + + return resolve( + loginTOTP({ + totp: { + login: { + ...params + }, + code: code?.twoFactorCode + } + }) + ); + } catch { + // twoFactor was canceled + return reject(); + } + } else { + reject(e); + } + } + }); +} + +function loginWithPassword({ user, password }: { user: string; password: string }) { + let params: ICredentials = { user, password }; + const state = store.getState(); + + if (state.settings.LDAP_Enable) { + params = { + username: user, + ldapPass: password, + ldap: true, + ldapOptions: {} + }; + } else if (state.settings.CROWD_Enable) { + params = { + username: user, + crowdPassword: password, + crowd: true + }; + } + + return loginTOTP(params, true); +} + +async function loginOAuthOrSso(params: ICredentials, isFromWebView = true) { + const result = await loginTOTP(params, false, isFromWebView); + store.dispatch(loginRequest({ resume: result.token }, false, isFromWebView)); +} + +function abort() { + if (sdk) { + return sdk.abort(); + } + return new AbortController(); +} + +function checkAndReopen() { + return sdk.checkAndReopen(); +} + +function disconnect() { + return sdk.disconnect(); +} + +async function getServerInfo(server: string) { + try { + const response = await RNFetchBlob.fetch('GET', `${server}/api/info`, { ...RocketChatSettings.customHeaders }); + try { + // Try to resolve as json + const jsonRes = response.json(); + if (!jsonRes?.success) { + return { + success: false, + message: I18n.t('Not_RC_Server', { contact: I18n.t('Contact_your_server_admin') }) + }; + } + if (compareServerVersion(jsonRes.version, 'lowerThan', MIN_ROCKETCHAT_VERSION)) { + return { + success: false, + message: I18n.t('Invalid_server_version', { + currentVersion: jsonRes.version, + minVersion: MIN_ROCKETCHAT_VERSION + }) + }; + } + return jsonRes; + } catch (error) { + // Request is successful, but response isn't a json + } + } catch (e: any) { + if (e?.message) { + if (e.message === 'Aborted') { + store.dispatch(selectServerFailure()); + throw e; + } + return { + success: false, + message: e.message + }; + } + } + + return { + success: false, + message: I18n.t('Not_RC_Server', { contact: I18n.t('Contact_your_server_admin') }) + }; +} + +async function getWebsocketInfo({ server }: { server: string }) { + const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) }); + + try { + await sdk.connect(); + } catch (err: any) { + if (err.message && err.message.includes('400')) { + return { + success: false, + message: I18n.t('Websocket_disabled', { contact: I18n.t('Contact_your_server_admin') }) + }; + } + } + + sdk.disconnect(); + + return { + success: true + }; +} + +export { + login, + loginTOTP, + loginWithPassword, + loginOAuthOrSso, + checkAndReopen, + abort, + disconnect, + getServerInfo, + getWebsocketInfo +}; diff --git a/app/lib/rocketchat/services/sdk.ts b/app/lib/rocketchat/services/sdk.ts index 41c124b11..2c6123947 100644 --- a/app/lib/rocketchat/services/sdk.ts +++ b/app/lib/rocketchat/services/sdk.ts @@ -6,10 +6,14 @@ import { twoFactor } from '../../../utils/twoFactor'; import { useSsl } from '../../../utils/url'; import reduxStore from '../../createStore'; import { Serialized, MatchPathPattern, OperationParams, PathFor, ResultFor } from '../../../definitions/rest/helpers'; +import { ILoginResult } from '../../../definitions'; class Sdk { private sdk: typeof Rocketchat; private code: any; + currentLogin: { + result: ILoginResult; + } | null = null; // TODO: We need to stop returning the SDK after all methods are dehydrated initialize(server: string) { @@ -158,6 +162,18 @@ class Sdk { onStreamData(...args: any[]) { return this.sdk.onStreamData(...args); } + + login(...args: any[]) { + return this.sdk.login(...args); + } + + checkAndReopen() { + return this.sdk.checkAndReopen(); + } + + abort() { + return this.sdk.abort(); + } } const sdk = new Sdk();