diff --git a/app/containers/TwoFactor/index.tsx b/app/containers/TwoFactor/index.tsx index 1f8817150e..2b28a286f8 100644 --- a/app/containers/TwoFactor/index.tsx +++ b/app/containers/TwoFactor/index.tsx @@ -91,6 +91,7 @@ const TwoFactor = React.memo(() => { const sendEmail = async () => { try { + console.log('params', params); if (params?.user) { clearErrors(); const response = await sendEmailCode(params?.user); diff --git a/app/definitions/IUser.ts b/app/definitions/IUser.ts index ac89e1d585..0c25d675d3 100644 --- a/app/definitions/IUser.ts +++ b/app/definitions/IUser.ts @@ -177,3 +177,16 @@ export type IUserDataEvent = { ); export type TUserModel = IUser & Model; + +export interface IUserTwoFactorDisable { + success: boolean; + error: string; + errorType: 'totp-required' | 'totp-invalid'; + details: { + method: string; + emailOrUsername: string; + codeGenerated: boolean; + codeExpires: string; + availableMethods: string[]; + }; +} diff --git a/app/definitions/rest/v1/index.ts b/app/definitions/rest/v1/index.ts index d4dcbba0fa..fe5f9917d1 100644 --- a/app/definitions/rest/v1/index.ts +++ b/app/definitions/rest/v1/index.ts @@ -21,6 +21,7 @@ import { PushEndpoints } from './push'; import { DirectoryEndpoint } from './directory'; import { AutoTranslateEndpoints } from './autotranslate'; import { ModerationEndpoints } from './moderation'; +import { MeEndpoints } from './me'; export type Endpoints = ChannelsEndpoints & ChatEndpoints & @@ -44,4 +45,5 @@ export type Endpoints = ChannelsEndpoints & PushEndpoints & DirectoryEndpoint & AutoTranslateEndpoints & - ModerationEndpoints; + ModerationEndpoints & + MeEndpoints; diff --git a/app/definitions/rest/v1/me.ts b/app/definitions/rest/v1/me.ts new file mode 100644 index 0000000000..81618e9808 --- /dev/null +++ b/app/definitions/rest/v1/me.ts @@ -0,0 +1,7 @@ +import type { IUser } from '../../IUser'; + +export type MeEndpoints = { + me: { + GET: () => IUser; + }; +}; diff --git a/app/definitions/rest/v1/users.ts b/app/definitions/rest/v1/users.ts index ab12be4dbc..67c6a9da91 100644 --- a/app/definitions/rest/v1/users.ts +++ b/app/definitions/rest/v1/users.ts @@ -1,7 +1,7 @@ import { IProfileParams } from '../../IProfile'; import type { ITeam } from '../../ITeam'; import type { IUser } from '../../IUser'; -import { INotificationPreferences, IUserPreferences, IUserRegistered } from '../../IUser'; +import { INotificationPreferences, IUserPreferences, IUserRegistered, IUserTwoFactorDisable } from '../../IUser'; export type UsersEndpoints = { 'users.2fa.sendEmailCode': { @@ -64,4 +64,7 @@ export type UsersEndpoints = { 'users.deleteOwnAccount': { POST: (params: { password: string; confirmRelinquish: boolean }) => { success: boolean }; }; + 'users.2fa.disableEmail': { + POST: (params: { emailOrUsername: string }) => IUserTwoFactorDisable; + }; }; diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 1fdcd02235..818985eb79 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -137,6 +137,7 @@ "Chat_started": "Chat started", "Chats": "Chats", "Check_again": "Check again", + "Check_auth_app_for_code": "Check your authenticator app and type in the 6-digit code", "Check_canned_responses": "Check on canned responses.", "Checked": "Checked", "Choose": "Choose", @@ -240,6 +241,8 @@ "Disable": "Disable", "Disable_encryption_description": "Disabling E2EE compromises privacy. You can re-enable it later if needed. Proceed with caution.", "Disable_encryption_title": "Disable encryption", + "Disable_totp_authenticator_app": "Disable TOTP Authenticator App", + "Disable_two_factor_auth_via_email": "Disable email TOTP authentication", "Disable_writing_in_room": "Disable writing in room", "Disabled": "Disabled", "Disabled_E2E_Encryption_for_this_room": "disabled E2E encryption for this room", @@ -297,6 +300,8 @@ "Enable_encryption_description": "Ensure conversations are kept private", "Enable_encryption_title": "Enable encryption", "Enable_Message_Parser": "Enable message parser", + "Enable_totp_authenticator_app": "Enable TOTP Authenticator App", + "Enable_two_factor_auth_via_email": "Enable email TOTP authentication", "Enable_writing_in_room": "Enable writing in room", "Enabled": "Enabled", "Enabled_E2E_Encryption_for_this_room": "enabled E2E encryption for this room", @@ -312,6 +317,7 @@ "Encryption_keys_reset": "Encryption keys reset", "Encryption_keys_reset_failed": "Encryption keys reset failed", "End_to_end_encrypted_room": "End to end encrypted room", + "Enter_6_digit_code": "Enter 6 digit code", "Enter_E2EE_Password": "Enter E2EE password", "Enter_E2EE_Password_description": "To access your encrypted channels and direct messages, enter your encryption password. This is not stored on the server, so you’ll need to use it on every device.", "Enter_the_code": "Enter the code we just emailed you.", @@ -744,6 +750,7 @@ "saving_preferences": "saving preferences", "saving_profile": "saving profile", "saving_settings": "saving settings", + "Scan_TOTP_QR_code": "Scan this QR code in Authy, Google Authenticator, or any TOTP app.", "Screen_lock": "Screen lock", "Search": "Search", "Search_by": "Search by", @@ -863,6 +870,10 @@ "Token_expired": "Your session has expired. Please log in again.", "Topic": "Topic", "topic": "topic", + "TOTP_email_not_verified": "You need to verify your emails before setting up 2FA", + "TOTP_enter_Code": "Enter code", + "TOTP_Open_Authentication_App": "Open authentication app", + "TOTP_Setup_Title": "Two-factor authentication", "totp-invalid": "Code or password invalid", "Translate": "Translate", "Travel_and_places": "Travel and places", diff --git a/app/lib/services/restApi.ts b/app/lib/services/restApi.ts index c8ec7512a1..7673b0df6b 100644 --- a/app/lib/services/restApi.ts +++ b/app/lib/services/restApi.ts @@ -1105,3 +1105,14 @@ export const getUsersRoles = async (): Promise => { export const getSupportedVersionsCloud = (uniqueId?: string, domain?: string) => fetch(`https://releases.rocket.chat/v2/server/supportedVersions?uniqueId=${uniqueId}&domain=${domain}&source=mobile`); + +export const getMe = () => sdk.get('me'); + +export const requestUserTotp = (userid: string) => + sdk.methodCall('2fa:enable', { msg: 'method', id: userid, method: '2fa:enable', params: [] }); + +export const verifyUserTotp = (code: string) => sdk.methodCall('2fa:validateTempToken', [code]); + +export const enableEmail2fa = () => sdk.post('users.2fa.enableEmail'); + +export const disableEmail2fa = () => sdk.post('users.2fa.disableEmail'); diff --git a/app/lib/services/sdk.ts b/app/lib/services/sdk.ts index 0cdda07b23..c49f392194 100644 --- a/app/lib/services/sdk.ts +++ b/app/lib/services/sdk.ts @@ -83,6 +83,7 @@ class Sdk { } return resolve(result); } catch (e: any) { + console.log('e', e); const errorType = isMethodCall ? e?.error : e?.data?.errorType; const totpInvalid = 'totp-invalid'; const totpRequired = 'totp-required'; diff --git a/app/stacks/InsideStack.tsx b/app/stacks/InsideStack.tsx index 1e4c52fbe9..69b8855a8b 100644 --- a/app/stacks/InsideStack.tsx +++ b/app/stacks/InsideStack.tsx @@ -90,6 +90,8 @@ import { import { isIOS } from '../lib/methods/helpers'; import { TNavigation } from './stackType'; import AccessibilityAndAppearanceView from '../views/AccessibilityAndAppearanceView'; +import TotpView from '../views/TwoFactorAuthenticationView'; +import TotpVerifyView from '../views/TwoFactorAuthenticationView/VerifyView'; // ChatsStackNavigator const ChatsStack = createNativeStackNavigator(); @@ -196,6 +198,8 @@ const SettingsStackNavigator = () => { component={ScreenLockConfigView} options={ScreenLockConfigView.navigationOptions} /> + + ); }; diff --git a/app/stacks/MasterDetailStack/types.ts b/app/stacks/MasterDetailStack/types.ts index c2a2a80ea5..d995f41b42 100644 --- a/app/stacks/MasterDetailStack/types.ts +++ b/app/stacks/MasterDetailStack/types.ts @@ -207,6 +207,8 @@ export type ModalStackParamList = { name: string; }; AccessibilityAndAppearanceView: undefined; + TotpView: undefined; + TotpVerifyView: undefined; }; export type MasterDetailInsideStackParamList = { diff --git a/app/stacks/types.ts b/app/stacks/types.ts index 4634b2457b..432fdc0a07 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -215,6 +215,8 @@ export type SettingsStackParamList = { PushTroubleshootView: undefined; GetHelpView: undefined; AccessibilityAndAppearanceView: undefined; + TotpView: undefined; + TotpVerifyView: undefined; }; export type AdminPanelStackParamList = { diff --git a/app/views/SecurityPrivacyView.tsx b/app/views/SecurityPrivacyView.tsx index 0b08fdd842..636b434e14 100644 --- a/app/views/SecurityPrivacyView.tsx +++ b/app/views/SecurityPrivacyView.tsx @@ -1,6 +1,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import React, { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; import * as List from '../containers/List'; import SafeAreaView from '../containers/SafeAreaView'; @@ -19,6 +20,10 @@ import { toggleCrashErrorsReport } from '../lib/methods/helpers/log'; import Switch from '../containers/Switch'; +import { getUserSelector } from '../selectors/login'; +import { disableEmail2fa, enableEmail2fa, getMe } from '../lib/services/restApi'; +import { showToast } from '../lib/methods/helpers/showToast'; +import { setUser } from '../actions/login'; interface ISecurityPrivacyViewProps { navigation: NativeStackNavigationProp; @@ -28,8 +33,10 @@ const SecurityPrivacyView = ({ navigation }: ISecurityPrivacyViewProps): JSX.Ele const [crashReportState, setCrashReportState] = useState(getReportCrashErrorsValue()); const [analyticsEventsState, setAnalyticsEventsState] = useState(getReportAnalyticsEventsValue()); const [server] = useServer(); + const dispatch = useDispatch(); const e2eEnabled = useAppSelector(state => state.settings.E2E_Enable); + const user = useAppSelector(state => getUserSelector(state)); useEffect(() => { navigation.setOptions({ @@ -51,7 +58,35 @@ const SecurityPrivacyView = ({ navigation }: ISecurityPrivacyViewProps): JSX.Ele toggleAnalyticsEventsReport(value); }; - const navigateToScreen = (screen: 'E2EEncryptionSecurityView' | 'ScreenLockConfigView') => { + const toggleEmail2fa = async (value: boolean) => { + if (!value) { + try { + const res = await disableEmail2fa(); + if (res.success) { + showToast('Email 2FA disabled successfully'); + + const updatedMe = await getMe(); + dispatch(setUser(updatedMe)); + } + } catch (error) { + console.log('error', error); + } + } else { + try { + const res = await enableEmail2fa(); + if (res.success) { + showToast('Email 2FA enabled successfully'); + + const updatedMe = await getMe(); + dispatch(setUser(updatedMe)); + } + } catch (error) { + console.log('error', error); + } + } + }; + + const navigateToScreen = (screen: 'E2EEncryptionSecurityView' | 'ScreenLockConfigView' | 'TotpView') => { // @ts-ignore logEvent(events[`SP_GO_${screen.replace('View', '').toUpperCase()}`]); navigation.navigate(screen); @@ -64,6 +99,10 @@ const SecurityPrivacyView = ({ navigation }: ISecurityPrivacyViewProps): JSX.Ele navigateToScreen('ScreenLockConfigView'); }; + const navigateToTotpView = async () => { + navigateToScreen('TotpView'); + }; + return ( @@ -89,6 +128,34 @@ const SecurityPrivacyView = ({ navigation }: ISecurityPrivacyViewProps): JSX.Ele + + + + + {user.services?.totp?.enabled ? ( + <> + + + + ) : null} + } + /> + + + { + navigation.setOptions({ + title: I18n.t('Two_Factor_Authentication') + }); + }, []); + + useFocusEffect( + useCallback(() => { + Clipboard.getString().then(content => { + if (/^\d{6}$/.test(content)) { + setCode(content); + } + }); + }, []) + ); + + const handleVerify = async () => { + if (code.length !== 6) { + alert(I18n.t('Invalid_Code')); + return; + } + + try { + const result = await verifyUserTotp(code); + const codes = result.codes; + alert('Done'); + } catch (error) { + // @ts-ignore + if (error.error === '[invalid-totp]') { + alert(I18n.t('Invalid_Code')); + return; + } + console.log(error); + } + }; + + return ( + + + {I18n.t('Enter_6_digit_code')} + + {I18n.t('Check_auth_app_for_code')} + + + + +