diff --git a/app/containers/LoginServices.tsx b/app/containers/LoginServices.tsx index aab5c889e..4495c7e88 100644 --- a/app/containers/LoginServices.tsx +++ b/app/containers/LoginServices.tsx @@ -234,7 +234,7 @@ class LoginServices extends React.PureComponent { AppleAuthentication.AppleAuthenticationScope.EMAIL ] }); - + // @ts-ignore await RocketChat.loginOAuthOrSso({ fullName, email, identityToken }); } catch { logEvent(events.ENTER_WITH_APPLE_F); diff --git a/app/containers/MessageBox/index.tsx b/app/containers/MessageBox/index.tsx index 3c924df6f..de1666223 100644 --- a/app/containers/MessageBox/index.tsx +++ b/app/containers/MessageBox/index.tsx @@ -49,6 +49,7 @@ import { sanitizeLikeString } from '../../lib/database/utils'; import { CustomIcon } from '../../lib/Icons'; import { IMessage } from '../../definitions/IMessage'; import { forceJpgExtension } from './forceJpgExtension'; +import { IUser } from '../../definitions'; if (isAndroid) { require('./EmojiKeyboard'); @@ -80,12 +81,7 @@ interface IMessageBoxProps { editing: boolean; threadsEnabled: boolean; isFocused(): boolean; - user: { - id: string; - _id: string; - username: string; - token: string; - }; + user: IUser; roomType: string; tmid: string; replyWithMention: boolean; diff --git a/app/definitions/ILoggedUser.ts b/app/definitions/ILoggedUser.ts index 0b083cd9a..d83f87fa5 100644 --- a/app/definitions/ILoggedUser.ts +++ b/app/definitions/ILoggedUser.ts @@ -1,28 +1,29 @@ import Model from '@nozbe/watermelondb/Model'; +import { IUserEmail } from './IUser'; +import { UserStatus } from './UserStatus'; + export interface ILoggedUser { id: string; token: string; username: string; name: string; language?: string; - status: string; + status: UserStatus; statusText?: string; - customFields: object; - statusLivechat: string; - emails: string[]; - roles: string[]; + customFields?: { + [key: string]: any; + }; + statusLivechat?: string; + emails?: IUserEmail[]; + roles?: string[]; avatarETag?: string; isFromWebView: boolean; - settings?: { - preferences: { - showMessageInMainThread: boolean; - enableMessageParserEarlyAdoption: boolean; - }; - }; + showMessageInMainThread: boolean; + enableMessageParserEarlyAdoption: boolean; } -export interface ILoginResult { +export interface ILoginResultFromServer { status: string; authToken: string; userId: string; diff --git a/app/definitions/IRocketChat.ts b/app/definitions/IRocketChat.ts index 654e2c92c..91d701a5d 100644 --- a/app/definitions/IRocketChat.ts +++ b/app/definitions/IRocketChat.ts @@ -4,6 +4,7 @@ type TRocketChat = typeof rocketchat; export interface IRocketChat extends TRocketChat { sdk: any; + shareSDK: any; activeUsersSubTimeout: any; roomsSub: any; } diff --git a/app/lib/rocketchat/rocketchat.js b/app/lib/rocketchat/rocketchat.js index 6a38c6d08..b0199ea85 100644 --- a/app/lib/rocketchat/rocketchat.js +++ b/app/lib/rocketchat/rocketchat.js @@ -65,6 +65,7 @@ import getUserInfo from './services/getUserInfo'; // Services import sdk from './services/sdk'; import toggleFavorite from './services/toggleFavorite'; +import { login, loginTOTP, loginWithPassword, loginOAuthOrSso, getLoginServices, _determineAuthType } from './services/connect'; import * as restAPis from './services/restApi'; const TOKEN_KEY = 'reactnativemeteor_usertoken'; @@ -487,103 +488,10 @@ const RocketChat = { return this.methodCallWrapper('e2e.resetOwnE2EKey'); }, - loginTOTP(params, loginEmailPassword, isFromWebView = false) { - return new Promise(async (resolve, reject) => { - try { - const result = await this.login(params, isFromWebView); - return resolve(result); - } catch (e) { - 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) { - reduxStore.dispatch(setUser({ username: params.user || params.username })); - - // Force normalized params for 2FA starting RC 3.9.0. - const serverVersion = reduxStore.getState().server.version; - if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '3.9.0')) { - const user = params.user ?? params.username; - const password = params.password ?? params.ldapPass ?? params.crowdPassword; - params = { user, password }; - } - - return resolve(this.loginTOTP({ ...params, code: code?.twoFactorCode }, loginEmailPassword)); - } - - return resolve( - this.loginTOTP({ - totp: { - login: { - ...params - }, - code: code?.twoFactorCode - } - }) - ); - } catch { - // twoFactor was canceled - return reject(); - } - } else { - reject(e); - } - } - }); - }, - - loginWithPassword({ user, password }) { - let params = { user, password }; - const state = reduxStore.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 this.loginTOTP(params, true); - }, - - async loginOAuthOrSso(params, isFromWebView = true) { - const result = await this.loginTOTP(params, false, isFromWebView); - reduxStore.dispatch(loginRequest({ resume: result.token }, false, isFromWebView)); - }, - - async login(credentials, isFromWebView = false) { - const sdk = this.shareSDK || this.sdk; - // RC 0.64.0 - await sdk.login(credentials); - const { result } = sdk.currentLogin; - 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; - }, + loginTOTP, + loginWithPassword, + loginOAuthOrSso, + login, logout, logoutOtherLocations() { const { id: userId } = reduxStore.getState().login.user; @@ -928,59 +836,8 @@ const RocketChat = { prefs = { ...prefs, ...param }; return UserPreferences.setMapAsync(SORT_PREFS_KEY, prefs); }, - async getLoginServices(server) { - try { - let loginServices = []; - const loginServicesResult = await fetch(`${server}/api/v1/settings.oauth`).then(response => response.json()); - - if (loginServicesResult.success && loginServicesResult.services) { - const { services } = loginServicesResult; - loginServices = services; - - const loginServicesReducer = loginServices.reduce((ret, item) => { - const name = item.name || item.buttonLabelText || item.service; - const authType = this._determineAuthType(item); - - if (authType !== 'not_supported') { - ret[name] = { ...item, name, authType }; - } - - return ret; - }, {}); - reduxStore.dispatch(setLoginServices(loginServicesReducer)); - } else { - reduxStore.dispatch(setLoginServices({})); - } - } catch (error) { - console.log(error); - reduxStore.dispatch(setLoginServices({})); - } - }, - _determineAuthType(services) { - const { name, custom, showButton = true, service } = services; - - const authName = name || service; - - if (custom && showButton) { - return 'oauth_custom'; - } - - if (service === 'saml') { - return 'saml'; - } - - if (service === 'cas') { - return 'cas'; - } - - if (authName === 'apple' && isIOS) { - return 'apple'; - } - - // TODO: remove this after other oauth providers are implemented. e.g. Drupal, github_enterprise - const availableOAuth = ['facebook', 'github', 'gitlab', 'google', 'linkedin', 'meteor-developer', 'twitter', 'wordpress']; - return availableOAuth.includes(authName) ? 'oauth' : 'not_supported'; - }, + getLoginServices, + _determineAuthType, roomTypeToApiType, readThreads(tmid) { const serverVersion = reduxStore.getState().server.version; diff --git a/app/lib/rocketchat/services/connect.ts b/app/lib/rocketchat/services/connect.ts index 1a2af2f87..b7058c2fd 100644 --- a/app/lib/rocketchat/services/connect.ts +++ b/app/lib/rocketchat/services/connect.ts @@ -6,18 +6,29 @@ 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 { loginRequest, setLoginServices, setUser } from '../../../actions/login'; import sdk from './sdk'; import I18n from '../../../i18n'; import { MIN_ROCKETCHAT_VERSION } from '../rocketchat'; -import { ICredentials, ILoggedUser } from '../../../definitions'; +import { ICredentials, ILoggedUser, IRocketChat } from '../../../definitions'; +import { isIOS } from '../../../utils/deviceInfo'; -async function login(credentials: ICredentials, isFromWebView = false) { +interface IServices { + [index: string]: string | boolean; + name: string; + custom: boolean; + showButton: boolean; + buttonLabelText: string; + service: string; +} + +async function login(this: IRocketChat, credentials: ICredentials, isFromWebView = false): Promise { + const sdk = this.shareSDK || this.sdk; // RC 0.64.0 await sdk.login(credentials); const result = sdk.currentLogin?.result; if (result) { - const user = { + const user: ILoggedUser = { id: result.userId, token: result.authToken, username: result.me.username, @@ -38,10 +49,15 @@ async function login(credentials: ICredentials, isFromWebView = false) { } } -function loginTOTP(params: ICredentials, loginEmailPassword?: boolean, isFromWebView = false): Promise { +function loginTOTP( + this: IRocketChat, + params: ICredentials, + loginEmailPassword?: boolean, + isFromWebView = false +): Promise { return new Promise(async (resolve, reject) => { try { - const result = await login(params, isFromWebView); + const result = await this.login(params, isFromWebView); if (result) { return resolve(result); } @@ -62,11 +78,11 @@ function loginTOTP(params: ICredentials, loginEmailPassword?: boolean, isFromWeb params = { user, password }; } - return resolve(loginTOTP({ ...params, code: code?.twoFactorCode }, loginEmailPassword)); + return resolve(this.loginTOTP({ ...params, code: code?.twoFactorCode }, loginEmailPassword)); } return resolve( - loginTOTP({ + this.loginTOTP({ totp: { login: { ...params @@ -86,7 +102,7 @@ function loginTOTP(params: ICredentials, loginEmailPassword?: boolean, isFromWeb }); } -function loginWithPassword({ user, password }: { user: string; password: string }) { +function loginWithPassword(this: IRocketChat, { user, password }: { user: string; password: string }) { let params: ICredentials = { user, password }; const state = store.getState(); @@ -105,11 +121,11 @@ function loginWithPassword({ user, password }: { user: string; password: string }; } - return loginTOTP(params, true); + return this.loginTOTP(params, true); } -async function loginOAuthOrSso(params: ICredentials, isFromWebView = true) { - const result = await loginTOTP(params, false, isFromWebView); +async function loginOAuthOrSso(this: IRocketChat, params: ICredentials, isFromWebView = true) { + const result = await this.loginTOTP(params, false, isFromWebView); store.dispatch(loginRequest({ resume: result.token }, false, isFromWebView)); } @@ -193,6 +209,61 @@ async function getWebsocketInfo({ server }: { server: string }) { }; } +async function getLoginServices(this: IRocketChat, server: string) { + try { + let loginServices = []; + const loginServicesResult = await fetch(`${server}/api/v1/settings.oauth`).then(response => response.json()); + + if (loginServicesResult.success && loginServicesResult.services) { + const { services } = loginServicesResult; + loginServices = services; + + const loginServicesReducer = loginServices.reduce((ret: IServices[], item: IServices) => { + const name = item.name || item.buttonLabelText || item.service; + const authType = this._determineAuthType(item); + + if (authType !== 'not_supported') { + ret[name as unknown as number] = { ...item, name, authType }; + } + + return ret; + }, {}); + store.dispatch(setLoginServices(loginServicesReducer)); + } else { + store.dispatch(setLoginServices({})); + } + } catch (error) { + console.log(error); + store.dispatch(setLoginServices({})); + } +} + +function _determineAuthType(services: IServices) { + const { name, custom, showButton = true, service } = services; + + const authName = name || service; + + if (custom && showButton) { + return 'oauth_custom'; + } + + if (service === 'saml') { + return 'saml'; + } + + if (service === 'cas') { + return 'cas'; + } + + if (authName === 'apple' && isIOS) { + return 'apple'; + } + + // TODO: remove this after other oauth providers are implemented. e.g. Drupal, github_enterprise + const availableOAuth = ['facebook', 'github', 'gitlab', 'google', 'linkedin', 'meteor-developer', 'twitter', 'wordpress']; + return availableOAuth.includes(authName) ? 'oauth' : 'not_supported'; +} + export { login, loginTOTP, @@ -202,5 +273,7 @@ export { abort, disconnect, getServerInfo, - getWebsocketInfo + getWebsocketInfo, + getLoginServices, + _determineAuthType }; diff --git a/app/lib/rocketchat/services/sdk.ts b/app/lib/rocketchat/services/sdk.ts index 2c6123947..13af977e2 100644 --- a/app/lib/rocketchat/services/sdk.ts +++ b/app/lib/rocketchat/services/sdk.ts @@ -6,13 +6,13 @@ 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'; +import { ICredentials, ILoginResultFromServer } from '../../../definitions'; class Sdk { private sdk: typeof Rocketchat; private code: any; currentLogin: { - result: ILoginResult; + result: ILoginResultFromServer; } | null = null; // TODO: We need to stop returning the SDK after all methods are dehydrated @@ -163,8 +163,8 @@ class Sdk { return this.sdk.onStreamData(...args); } - login(...args: any[]) { - return this.sdk.login(...args); + login(credentials: ICredentials) { + return this.sdk.login(credentials); } checkAndReopen() { diff --git a/app/views/AuthenticationWebView.tsx b/app/views/AuthenticationWebView.tsx index ac304fbfb..d9eb5a111 100644 --- a/app/views/AuthenticationWebView.tsx +++ b/app/views/AuthenticationWebView.tsx @@ -102,6 +102,7 @@ class AuthenticationWebView extends React.PureComponent