diff --git a/app/definitions/ITotp.ts b/app/definitions/ITotp.ts new file mode 100644 index 000000000..02bd3f54b --- /dev/null +++ b/app/definitions/ITotp.ts @@ -0,0 +1,5 @@ +export enum TwoFactorMethods { + TOTP = 'totp', + EMAIL = 'email', + PASSWORD = 'password' +} diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index d49eee0e0..07ed4f7c5 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -71,6 +71,7 @@ "error-remove-last-owner": "This is the last owner. Please set a new owner before removing this one.", "error-role-in-use": "Cannot delete role because it's in use", "error-role-name-required": "Role name is required", + "error-password-same-as-current": "Entered password same as current password", "error-the-field-is-required": "The field {{field}} is required.", "error-too-many-requests": "Error, too many requests. Please slow down. You must wait {{seconds}} seconds before trying again.", "error-user-is-not-activated": "User is not activated", @@ -836,5 +837,6 @@ "Mark_as_unread": "Mark as unread", "Mark_as_unread_Info": "Display room as unread when there are unread messages", "Show_badge_for_mentions": "Show badge for mentions", - "Show_badge_for_mentions_Info": "Display badge for direct mentions only" + "Show_badge_for_mentions_Info": "Display badge for direct mentions only", + "totp-invalid": "Code or password invalid" } \ No newline at end of file diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 4a092f021..6d4462c55 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -69,6 +69,7 @@ "error-remove-last-owner": "Este é o último proprietário. Por favor, defina um novo proprietário antes de remover este.", "error-role-in-use": "Não é possível remover o papel pois ele está em uso", "error-role-name-required": "Nome do papel é obrigatório", + "error-password-same-as-current": "Senha digitada coincide com a senha atual", "error-the-field-is-required": "O campo {{field}} é obrigatório.", "error-too-many-requests": "Erro, muitas solicitações. Por favor, diminua a velocidade. Você deve esperar {{seconds}} segundos antes de tentar novamente.", "error-user-is-not-activated": "O usuário não está ativo", @@ -743,5 +744,6 @@ "sending_email_confirmation": "enviando email de confirmação", "Unsupported_format": "Formato não suportado", "Downloaded_file": "Arquivo baixado", - "Error_Download_file": "Erro ao baixar o arquivo" + "Error_Download_file": "Erro ao baixar o arquivo", + "totp-invalid": "Código ou senha inválida" } \ No newline at end of file diff --git a/app/lib/services/restApi.ts b/app/lib/services/restApi.ts index 867653b97..5d7007c53 100644 --- a/app/lib/services/restApi.ts +++ b/app/lib/services/restApi.ts @@ -918,6 +918,15 @@ export function getUserInfo(userId: string) { export const toggleFavorite = (roomId: string, favorite: boolean) => sdk.post('rooms.favorite', { roomId, favorite }); +export const saveUserProfileMethod = ( + params: IProfileParams, + customFields = {}, + twoFactorOptions: { + twoFactorCode: string; + twoFactorMethod: string; + } | null +) => sdk.current.methodCall('saveUserProfile', params, customFields, twoFactorOptions); + export const deleteOwnAccount = (password: string, confirmRelinquish = false): any => // RC 0.67.0 sdk.post('users.deleteOwnAccount', { password, confirmRelinquish }); diff --git a/app/views/ProfileView/index.tsx b/app/views/ProfileView/index.tsx index 71cdc3891..1c7320878 100644 --- a/app/views/ProfileView/index.tsx +++ b/app/views/ProfileView/index.tsx @@ -41,6 +41,8 @@ import { IProfileParams, IUser } from '../../definitions'; +import { twoFactor } from '../../lib/services/twoFactor'; +import { TwoFactorMethods } from '../../definitions/ITotp'; import { withActionSheet, IActionSheetProvider } from '../../containers/ActionSheet'; import { DeleteAccountActionSheetContent } from './components/DeleteAccountActionSheetContent'; @@ -71,6 +73,10 @@ interface IProfileViewState { customFields: { [key: string | number]: string; }; + twoFactorCode: null | { + twoFactorCode: string; + twoFactorMethod: string; + }; } class ProfileView extends React.Component { @@ -113,7 +119,8 @@ class ProfileView extends React.Component url: '' }, avatarSuggestions: {}, - customFields: {} + customFields: {}, + twoFactorCode: null }; async componentDidMount() { @@ -194,14 +201,17 @@ class ProfileView extends React.Component ); }; - handleError = (e: any, func: string, action: string) => { + handleError = (e: any, _func: string, action: string) => { if (e.data && e.data.error.includes('[error-too-many-requests]')) { return showErrorAlert(e.data.error); } + if (I18n.isTranslated(e.error)) { + return showErrorAlert(I18n.t(e.error)); + } showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) })); }; - submit = async () => { + submit = async (): Promise => { Keyboard.dismiss(); if (!this.formIsChanged()) { @@ -210,7 +220,7 @@ class ProfileView extends React.Component this.setState({ saving: true }); - const { name, username, email, newPassword, currentPassword, avatar, customFields } = this.state; + const { name, username, email, newPassword, currentPassword, avatar, customFields, twoFactorCode } = this.state; const { user, dispatch } = this.props; const params = {} as IProfileParams; @@ -275,9 +285,16 @@ class ProfileView extends React.Component } } - const result = await Services.saveUserProfile(params, customFields); + const twoFactorOptions = params.currentPassword + ? { + twoFactorCode: params.currentPassword, + twoFactorMethod: TwoFactorMethods.PASSWORD + } + : null; - if (result.success) { + const result = await Services.saveUserProfileMethod(params, customFields, twoFactorCode || twoFactorOptions); + + if (result) { logEvent(events.PROFILE_SAVE_CHANGES); if (customFields) { dispatch(setUser({ customFields, ...params })); @@ -287,10 +304,18 @@ class ProfileView extends React.Component EventEmitter.emit(LISTENER, { message: I18n.t('Profile_saved_successfully') }); this.init(); } - this.setState({ saving: false }); - } catch (e) { + this.setState({ saving: false, currentPassword: null, twoFactorCode: null }); + } catch (e: any) { + if (e?.error === 'totp-invalid' && e?.details.method !== TwoFactorMethods.PASSWORD) { + try { + const code = await twoFactor({ method: e?.details.method, invalid: e?.error === 'totp-invalid' && !!twoFactorCode }); + return this.setState({ twoFactorCode: code }, () => this.submit()); + } catch { + // cancelled twoFactor modal + } + } logEvent(events.PROFILE_SAVE_CHANGES_F); - this.setState({ saving: false, currentPassword: null }); + this.setState({ saving: false, currentPassword: null, twoFactorCode: null }); this.handleError(e, 'saveUserProfile', 'saving_profile'); } };