[NEW] Two Factor authentication via email (#1961)

* First api call working

* [NEW] REST API Post wrapper 2FA

* [NEW] Send 2FA on Email

* [I18n] Add translations

* [NEW] Translations & Cancel totp

* [CHORE] Totp -> TwoFactor

* [NEW] Two Factor by email

* [NEW] Tablet Support

* [FIX] Text colors

* [NEW] Password 2fa

* [FIX] Encrypt password on 2FA

* [NEW] MethodCall2FA

* [FIX] Password fallback

* [FIX] Wrap all post/methodCall with 2fa

* [FIX] Wrap missed function

* few fixes

* [FIX] Use new TOTP on Login

* [improvement] 2fa methodCall

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
This commit is contained in:
Diego Mello 2020-04-01 17:32:24 -03:00 committed by GitHub
parent 18afdd843e
commit 6982d7676a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 393 additions and 128 deletions

View File

@ -0,0 +1,137 @@
import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';
import _ from 'lodash';
import PropTypes from 'prop-types';
import { sha256 } from 'js-sha256';
import Modal from 'react-native-modal';
import useDeepCompareEffect from 'use-deep-compare-effect';
import TextInput from '../TextInput';
import I18n from '../../i18n';
import EventEmitter from '../../utils/events';
import { withTheme } from '../../theme';
import { withSplit } from '../../split';
import { themes } from '../../constants/colors';
import Button from '../Button';
import sharedStyles from '../../views/Styles';
import RocketChat from '../../lib/rocketchat';
import styles from './styles';
export const TWO_FACTOR = 'TWO_FACTOR';
const methods = {
totp: {
text: 'Open_your_authentication_app_and_enter_the_code',
keyboardType: 'numeric'
},
email: {
text: 'Verify_your_email_for_the_code_we_sent',
keyboardType: 'numeric'
},
password: {
title: 'Please_enter_your_password',
text: 'For_your_security_you_must_enter_your_current_password_to_continue',
secureTextEntry: true,
keyboardType: 'default'
}
};
const TwoFactor = React.memo(({ theme, split }) => {
const [visible, setVisible] = useState(false);
const [data, setData] = useState({});
const [code, setCode] = useState('');
const method = methods[data.method];
const isEmail = data.method === 'email';
const sendEmail = () => RocketChat.sendEmailCode();
useDeepCompareEffect(() => {
if (!_.isEmpty(data)) {
setVisible(true);
} else {
setVisible(false);
}
}, [data]);
const showTwoFactor = args => setData(args);
useEffect(() => {
EventEmitter.addEventListener(TWO_FACTOR, showTwoFactor);
return () => EventEmitter.removeListener(TWO_FACTOR);
}, []);
const onCancel = () => {
const { cancel } = data;
if (cancel) {
cancel();
}
setData({});
};
const onSubmit = () => {
const { submit } = data;
if (submit) {
if (data.method === 'password') {
submit(sha256(code));
} else {
submit(code);
}
}
setData({});
};
const color = themes[theme].titleText;
return (
<Modal
transparent
avoidKeyboard
useNativeDriver
isVisible={visible}
hideModalContentWhileAnimating
>
<View style={styles.container}>
<View style={[styles.content, split && [sharedStyles.modal, sharedStyles.modalFormSheet], { backgroundColor: themes[theme].backgroundColor }]}>
<Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text>
<Text style={[styles.subtitle, { color }]}>{I18n.t(method?.text)}</Text>
<TextInput
value={code}
theme={theme}
returnKeyType='send'
autoCapitalize='none'
onChangeText={setCode}
onSubmitEditing={onSubmit}
keyboardType={method?.keyboardType}
secureTextEntry={method?.secureTextEntry}
error={data.invalid && { error: 'totp-invalid', reason: I18n.t('Code_or_password_invalid') }}
/>
{isEmail && <Text style={[styles.sendEmail, { color }]} onPress={sendEmail}>{I18n.t('Send_me_the_code_again')}</Text>}
<View style={styles.buttonContainer}>
<Button
title={I18n.t('Cancel')}
type='secondary'
backgroundColor={themes[theme].chatComponentBackground}
style={styles.button}
onPress={onCancel}
theme={theme}
/>
<Button
title={I18n.t('Send')}
type='primary'
style={styles.button}
onPress={onSubmit}
theme={theme}
/>
</View>
</View>
</View>
</Modal>
);
});
TwoFactor.propTypes = {
theme: PropTypes.string,
split: PropTypes.bool
};
export default withSplit(withTheme(TwoFactor));

View File

@ -0,0 +1,40 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles';
export default StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
content: {
padding: 16,
width: '100%',
borderRadius: 4
},
title: {
fontSize: 14,
...sharedStyles.textBold
},
subtitle: {
fontSize: 14,
paddingVertical: 8,
...sharedStyles.textRegular,
...sharedStyles.textAlignCenter
},
sendEmail: {
fontSize: 14,
paddingBottom: 24,
paddingTop: 8,
alignSelf: 'center',
...sharedStyles.textRegular
},
button: {
marginBottom: 0
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between'
}
});

View File

@ -138,6 +138,7 @@ export default {
Choose_file: 'Choose file',
Choose_where_you_want_links_be_opened: 'Choose where you want links be opened',
Code: 'Code',
Code_or_password_invalid: 'Code or password invalid',
Collaborative: 'Collaborative',
Confirm: 'Confirm',
Connect: 'Connect',
@ -330,6 +331,7 @@ export default {
Only_authorized_users_can_write_new_messages: 'Only authorized users can write new messages',
Open_emoji_selector: 'Open emoji selector',
Open_Source_Communication: 'Open Source Communication',
Open_your_authentication_app_and_enter_the_code: 'Open your authentication app and enter the code.',
OR: 'OR',
Overwrites_the_server_configuration_and_use_room_config: 'Overwrites the server configuration and use room config',
Password: 'Password',
@ -416,6 +418,7 @@ export default {
Send_audio_message: 'Send audio message',
Send_crash_report: 'Send crash report',
Send_message: 'Send message',
Send_me_the_code_again: 'Send me the code again',
Send_to: 'Send to...',
Sent_an_attachment: 'Sent an attachment',
Server: 'Server',
@ -499,8 +502,10 @@ export default {
Uses_server_configuration: 'Uses server configuration',
Usually_a_discussion_starts_with_a_question_like_How_do_I_upload_a_picture: 'Usually, a discussion starts with a question, like "How do I upload a picture?"',
Validating: 'Validating',
Verify: 'Verify',
Verify_email_title: 'Registration Succeeded!',
Verify_email_desc: 'We have sent you an email to confirm your registration. If you do not receive an email shortly, please come back and try again.',
Verify_your_email_for_the_code_we_sent: 'Verify your email for the code we sent',
Video_call: 'Video call',
View_Original: 'View Original',
Voice_call: 'Voice call',

View File

@ -140,6 +140,7 @@ export default {
Choose_file: 'Enviar arquivo',
Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos',
Code: 'Código',
Code_or_password_invalid: 'Código ou senha inválido',
Collaborative: 'Colaborativo',
Confirm: 'Confirmar',
Connect: 'Conectar',
@ -302,6 +303,7 @@ export default {
Only_authorized_users_can_write_new_messages: 'Somente usuários autorizados podem escrever novas mensagens',
Open_emoji_selector: 'Abrir seletor de emoji',
Open_Source_Communication: 'Comunicação Open Source',
Open_your_authentication_app_and_enter_the_code: 'Abra seu aplicativo de autenticação e digite o código.',
OR: 'OU',
Overwrites_the_server_configuration_and_use_room_config: 'Substituir a configuração do servidor e usar a configuração da sala',
Password: 'Senha',
@ -378,6 +380,7 @@ export default {
Send: 'Enviar',
Send_audio_message: 'Enviar mensagem de áudio',
Send_message: 'Enviar mensagem',
Send_me_the_code_again: 'Envie-me o código novamente',
Send_to: 'Enviar para...',
Sent_an_attachment: 'Enviou um anexo',
Server: 'Servidor',
@ -446,8 +449,10 @@ export default {
Username_or_email: 'Usuário ou email',
Uses_server_configuration: 'Usar configuração do servidor',
Usually_a_discussion_starts_with_a_question_like_How_do_I_upload_a_picture: 'Normalmente, uma discussão começa com uma pergunta como: Como faço para enviar uma foto?',
Verify: 'Verificar',
Verify_email_title: 'Registrado com sucesso!',
Verify_email_desc: 'Nós lhe enviamos um e-mail para confirmar o seu registro. Se você não receber um e-mail em breve, por favor retorne e tente novamente.',
Verify_your_email_for_the_code_we_sent: 'Verifique em seu e-mail o código que enviamos',
Video_call: 'Chamada de vídeo',
Voice_call: 'Chamada de voz',
Websocket_disabled: 'Websocket está desativado para esse servidor.\n{{contact}}',

View File

@ -42,6 +42,7 @@ import { KEY_COMMAND } from './commands';
import Tablet, { initTabletNav } from './tablet';
import sharedStyles from './views/Styles';
import { SplitContext } from './split';
import TwoFactor from './containers/TwoFactor';
import RoomsListView from './views/RoomsListView';
import RoomView from './views/RoomView';
@ -721,6 +722,7 @@ export default class Root extends React.Component {
}}
>
{content}
<TwoFactor />
</ThemeContext.Provider>
</Provider>
</AppearanceProvider>

View File

@ -48,6 +48,7 @@ import { getDeviceToken } from '../notifications/push';
import { SERVERS, SERVER_URL } from '../constants/userDefaults';
import { setActiveUsers } from '../actions/activeUsers';
import I18n from '../i18n';
import { twoFactor } from '../utils/twoFactor';
const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
@ -76,7 +77,7 @@ const RocketChat = {
name, users, type, readOnly, broadcast
}) {
// RC 0.51.0
return this.sdk.methodCall(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast });
return this.methodCall(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast });
},
async getUserToken() {
try {
@ -195,6 +196,10 @@ const RocketChat = {
this.sdk = null;
}
if (this.code) {
this.code = null;
}
// Use useSsl: false only if server url starts with http://
const useSsl = !/http:\/\//.test(server);
@ -304,25 +309,47 @@ const RocketChat = {
},
updateJitsiTimeout(rid) {
return this.sdk.methodCall('jitsi:updateTimeout', rid);
return this.methodCall('jitsi:updateTimeout', rid);
},
register(credentials) {
// RC 0.50.0
return this.sdk.post('users.register', credentials, false);
return this.post('users.register', credentials, false);
},
setUsername(username) {
// RC 0.51.0
return this.sdk.methodCall('setUsername', username);
return this.methodCall('setUsername', username);
},
forgotPassword(email) {
// RC 0.64.0
return this.sdk.post('users.forgotPassword', { email }, false);
return this.post('users.forgotPassword', { email }, false);
},
async loginWithPassword({ user, password, code }) {
loginTOTP(params) {
return new Promise(async(resolve, reject) => {
try {
const result = await this.login(params);
return resolve(result);
} catch (e) {
if (e.data?.error && (e.data.error === 'totp-required' || e.data.error === 'totp-invalid')) {
const { details } = e.data;
try {
await twoFactor({ method: details?.method, invalid: e.data.error === 'totp-invalid' });
return resolve(this.loginTOTP(params));
} catch {
// twoFactor was canceled
return reject();
}
} else {
reject(e);
}
}
});
},
loginWithPassword({ user, password }) {
let params = { user, password };
const state = reduxStore.getState();
@ -341,16 +368,8 @@ const RocketChat = {
};
}
if (code) {
params = {
user,
password,
code
};
}
try {
return await this.login(params);
return this.loginTOTP(params);
} catch (error) {
throw error;
}
@ -487,7 +506,7 @@ const RocketChat = {
};
try {
// RC 0.60.0
await this.sdk.post('push.token', data);
await this.post('push.token', data);
} catch (error) {
console.log(error);
}
@ -599,12 +618,12 @@ const RocketChat = {
spotlight(search, usernames, type) {
// RC 0.51.0
return this.sdk.methodCall('spotlight', search, usernames, type);
return this.methodCall('spotlight', search, usernames, type);
},
createDirectMessage(username) {
// RC 0.59.0
return this.sdk.post('im.create', { username });
return this.post('im.create', { username });
},
createGroupChat() {
@ -612,14 +631,14 @@ const RocketChat = {
users = users.map(u => u.name);
// RC 3.1.0
return this.sdk.methodCall('createDirectMessage', ...users);
return this.methodCall('createDirectMessage', ...users);
},
createDiscussion({
prid, pmid, t_name, reply, users
}) {
// RC 1.0.0
return this.sdk.post('rooms.createDiscussion', {
return this.post('rooms.createDiscussion', {
prid, pmid, t_name, reply, users
});
},
@ -628,9 +647,9 @@ const RocketChat = {
// TODO: join code
// RC 0.48.0
if (type === 'p') {
return this.sdk.methodCall('joinRoom', roomId);
return this.methodCall('joinRoom', roomId);
}
return this.sdk.post('channels.join', { roomId });
return this.post('channels.join', { roomId });
},
triggerBlockAction,
triggerSubmitView,
@ -662,34 +681,34 @@ const RocketChat = {
},
deleteMessage(messageId, rid) {
// RC 0.48.0
return this.sdk.post('chat.delete', { msgId: messageId, roomId: rid });
return this.post('chat.delete', { msgId: messageId, roomId: rid });
},
editMessage(message) {
const { id, msg, rid } = message;
// RC 0.49.0
return this.sdk.post('chat.update', { roomId: rid, msgId: id, text: msg });
return this.post('chat.update', { roomId: rid, msgId: id, text: msg });
},
markAsUnread({ messageId }) {
return this.sdk.post('subscriptions.unread', { firstUnreadMessage: { _id: messageId } });
return this.post('subscriptions.unread', { firstUnreadMessage: { _id: messageId } });
},
toggleStarMessage(messageId, starred) {
if (starred) {
// RC 0.59.0
return this.sdk.post('chat.unStarMessage', { messageId });
return this.post('chat.unStarMessage', { messageId });
}
// RC 0.59.0
return this.sdk.post('chat.starMessage', { messageId });
return this.post('chat.starMessage', { messageId });
},
togglePinMessage(messageId, pinned) {
if (pinned) {
// RC 0.59.0
return this.sdk.post('chat.unPinMessage', { messageId });
return this.post('chat.unPinMessage', { messageId });
}
// RC 0.59.0
return this.sdk.post('chat.pinMessage', { messageId });
return this.post('chat.pinMessage', { messageId });
},
reportMessage(messageId) {
return this.sdk.post('chat.reportMessage', { messageId, description: 'Message reported by user' });
return this.post('chat.reportMessage', { messageId, description: 'Message reported by user' });
},
async getRoom(rid) {
try {
@ -739,42 +758,42 @@ const RocketChat = {
},
emitTyping(room, t = true) {
const { login } = reduxStore.getState();
return this.sdk.methodCall('stream-notify-room', `${ room }/typing`, login.user.username, t);
return this.methodCall('stream-notify-room', `${ room }/typing`, login.user.username, t);
},
setUserPresenceAway() {
return this.sdk.methodCall('UserPresence:away');
return this.methodCall('UserPresence:away');
},
setUserPresenceOnline() {
return this.sdk.methodCall('UserPresence:online');
return this.methodCall('UserPresence:online');
},
setUserPresenceDefaultStatus(status) {
return this.sdk.methodCall('UserPresence:setDefaultStatus', status);
return this.methodCall('UserPresence:setDefaultStatus', status);
},
setUserStatus(message) {
// RC 1.2.0
return this.sdk.post('users.setStatus', { message });
return this.post('users.setStatus', { message });
},
setReaction(emoji, messageId) {
// RC 0.62.2
return this.sdk.post('chat.react', { emoji, messageId });
return this.post('chat.react', { emoji, messageId });
},
toggleFavorite(roomId, favorite) {
// RC 0.64.0
return this.sdk.post('rooms.favorite', { roomId, favorite });
return this.post('rooms.favorite', { roomId, favorite });
},
toggleRead(read, roomId) {
if (read) {
return this.sdk.post('subscriptions.unread', { roomId });
return this.post('subscriptions.unread', { roomId });
}
return this.sdk.post('subscriptions.read', { rid: roomId });
return this.post('subscriptions.read', { rid: roomId });
},
getRoomMembers(rid, allUsers, skip = 0, limit = 10) {
// RC 0.42.0
return this.sdk.methodCall('getUsersOfRoom', rid, allUsers, { skip, limit });
return this.methodCall('getUsersOfRoom', rid, allUsers, { skip, limit });
},
getUserRoles() {
// RC 0.27.0
return this.sdk.methodCall('getUserRoles');
return this.methodCall('getUserRoles');
},
getRoomCounters(roomId, t) {
// RC 0.65.0
@ -816,63 +835,110 @@ const RocketChat = {
toggleBlockUser(rid, blocked, block) {
if (block) {
// RC 0.49.0
return this.sdk.methodCall('blockUser', { rid, blocked });
return this.methodCall('blockUser', { rid, blocked });
}
// RC 0.49.0
return this.sdk.methodCall('unblockUser', { rid, blocked });
return this.methodCall('unblockUser', { rid, blocked });
},
leaveRoom(roomId, t) {
// RC 0.48.0
return this.sdk.post(`${ this.roomTypeToApiType(t) }.leave`, { roomId });
return this.post(`${ this.roomTypeToApiType(t) }.leave`, { roomId });
},
deleteRoom(roomId, t) {
// RC 0.49.0
return this.sdk.post(`${ this.roomTypeToApiType(t) }.delete`, { roomId });
return this.post(`${ this.roomTypeToApiType(t) }.delete`, { roomId });
},
toggleMuteUserInRoom(rid, username, mute) {
if (mute) {
// RC 0.51.0
return this.sdk.methodCall('muteUserInRoom', { rid, username });
return this.methodCall('muteUserInRoom', { rid, username });
}
// RC 0.51.0
return this.sdk.methodCall('unmuteUserInRoom', { rid, username });
return this.methodCall('unmuteUserInRoom', { rid, username });
},
toggleArchiveRoom(roomId, t, archive) {
if (archive) {
// RC 0.48.0
return this.sdk.post(`${ this.roomTypeToApiType(t) }.archive`, { roomId });
return this.post(`${ this.roomTypeToApiType(t) }.archive`, { roomId });
}
// RC 0.48.0
return this.sdk.post(`${ this.roomTypeToApiType(t) }.unarchive`, { roomId });
return this.post(`${ this.roomTypeToApiType(t) }.unarchive`, { roomId });
},
hideRoom(roomId, t) {
return this.sdk.post(`${ this.roomTypeToApiType(t) }.close`, { roomId });
return this.post(`${ this.roomTypeToApiType(t) }.close`, { roomId });
},
saveRoomSettings(rid, params) {
// RC 0.55.0
return this.sdk.methodCall('saveRoomSettings', rid, params);
return this.methodCall('saveRoomSettings', rid, params);
},
post(...args) {
return new Promise(async(resolve, reject) => {
try {
const result = await this.sdk.post(...args);
return resolve(result);
} catch (e) {
if (e.data && (e.data.errorType === 'totp-required' || e.data.errorType === 'totp-invalid')) {
const { details } = e.data;
try {
await twoFactor({ method: details?.method, invalid: e.data.errorType === 'totp-invalid' });
return resolve(this.post(...args));
} catch {
// twoFactor was canceled
return resolve({});
}
} else {
reject(e);
}
}
});
},
methodCall(...args) {
return new Promise(async(resolve, reject) => {
try {
const result = await this.sdk.methodCall(...args, this.code);
return resolve(result);
} catch (e) {
if (e.error && (e.error === 'totp-required' || e.error === 'totp-invalid')) {
const { details } = e;
try {
this.code = await twoFactor({ method: details?.method, invalid: e.error === 'totp-invalid' });
return resolve(this.methodCall(...args));
} catch {
// twoFactor was canceled
return resolve({});
}
} else {
reject(e);
}
}
});
},
sendEmailCode() {
const { username } = reduxStore.getState().login.user;
// RC 3.1.0
return this.post('users.2fa.sendEmailCode', { emailOrUsername: username });
},
saveUserProfile(data, customFields) {
// RC 0.62.2
return this.sdk.post('users.updateOwnBasicInfo', { data, customFields });
return this.post('users.updateOwnBasicInfo', { data, customFields });
},
saveUserPreferences(params) {
// RC 0.51.0
return this.sdk.methodCall('saveUserPreferences', params);
return this.methodCall('saveUserPreferences', params);
},
saveNotificationSettings(roomId, notifications) {
// RC 0.63.0
return this.sdk.post('rooms.saveNotification', { roomId, notifications });
return this.post('rooms.saveNotification', { roomId, notifications });
},
addUsersToRoom(rid) {
let { users } = reduxStore.getState().selectedUsers;
users = users.map(u => u.name);
// RC 0.51.0
return this.sdk.methodCall('addUsersToRoom', { rid, users });
return this.methodCall('addUsersToRoom', { rid, users });
},
getSingleMessage(msgId) {
// RC 0.57.0
return this.sdk.methodCall('getSingleMessage', msgId);
return this.methodCall('getSingleMessage', msgId);
},
async hasPermission(permissions, rid) {
const db = database.active;
@ -915,15 +981,15 @@ const RocketChat = {
},
getAvatarSuggestion() {
// RC 0.51.0
return this.sdk.methodCall('getAvatarSuggestion');
return this.methodCall('getAvatarSuggestion');
},
resetAvatar(userId) {
// RC 0.55.0
return this.sdk.post('users.resetAvatar', { userId });
return this.post('users.resetAvatar', { userId });
},
setAvatarFromService({ data, contentType = '', service = null }) {
// RC 0.51.0
return this.sdk.methodCall('setAvatarFromService', data, contentType, service);
return this.methodCall('setAvatarFromService', data, contentType, service);
},
async getAllowCrashReport() {
const allowCrashReport = await AsyncStorage.getItem(CRASH_REPORT_KEY);
@ -1042,9 +1108,9 @@ const RocketChat = {
toggleFollowMessage(mid, follow) {
// RC 1.0
if (follow) {
return this.sdk.post('chat.followMessage', { mid });
return this.post('chat.followMessage', { mid });
}
return this.sdk.post('chat.unfollowMessage', { mid });
return this.post('chat.unfollowMessage', { mid });
},
getThreadsList({ rid, count, offset }) {
// RC 1.0
@ -1060,7 +1126,7 @@ const RocketChat = {
},
runSlashCommand(command, roomId, params, triggerId, tmid) {
// RC 0.60.2
return this.sdk.post('commands.run', {
return this.post('commands.run', {
command, roomId, params, triggerId, tmid
});
},
@ -1072,7 +1138,7 @@ const RocketChat = {
},
executeCommandPreview(command, params, roomId, previewItem, triggerId, tmid) {
// RC 0.65.0
return this.sdk.post('commands.preview', {
return this.post('commands.preview', {
command, params, roomId, previewItem, triggerId, tmid
});
},
@ -1135,13 +1201,13 @@ const RocketChat = {
saveAutoTranslate({
rid, field, value, options
}) {
return this.sdk.methodCall('autoTranslate.saveSettings', rid, field, value, options);
return this.methodCall('autoTranslate.saveSettings', rid, field, value, options);
},
getSupportedLanguagesAutoTranslate() {
return this.sdk.methodCall('autoTranslate.getSupportedLanguages', 'en');
return this.methodCall('autoTranslate.getSupportedLanguages', 'en');
},
translateMessage(message, targetLanguage) {
return this.sdk.methodCall('autoTranslate.translateMessage', message, targetLanguage);
return this.methodCall('autoTranslate.translateMessage', message, targetLanguage);
},
getRoomTitle(room) {
const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings;
@ -1160,15 +1226,15 @@ const RocketChat = {
findOrCreateInvite({ rid, days, maxUses }) {
// RC 2.4.0
return this.sdk.post('findOrCreateInvite', { rid, days, maxUses });
return this.post('findOrCreateInvite', { rid, days, maxUses });
},
validateInviteToken(token) {
// RC 2.4.0
return this.sdk.post('validateInviteToken', { token });
return this.post('validateInviteToken', { token });
},
useInviteToken(token) {
// RC 2.4.0
return this.sdk.post('useInviteToken', { token });
return this.post('useInviteToken', { token });
}
};

View File

@ -11,7 +11,7 @@ let _basicAuth;
export const setBasicAuth = (basicAuth) => {
_basicAuth = basicAuth;
if (basicAuth) {
RocketChatSettings.customHeaders = { ...RocketChatSettings.customHeaders, Authorization: `Basic ${ _basicAuth }` };
RocketChatSettings.customHeaders = { ...headers, Authorization: `Basic ${ _basicAuth }` };
} else {
RocketChatSettings.customHeaders = headers;
}

20
app/utils/twoFactor.js Normal file
View File

@ -0,0 +1,20 @@
import { settings } from '@rocket.chat/sdk';
import EventEmitter from './events';
import { TWO_FACTOR } from '../containers/TwoFactor';
export const twoFactor = ({ method, invalid }) => new Promise((resolve, reject) => {
EventEmitter.emit(TWO_FACTOR, {
method,
invalid,
cancel: () => reject(),
submit: (code) => {
settings.customHeaders = {
...settings.customHeaders,
'x-2fa-code': code,
'x-2fa-method': method
};
resolve({ twoFactorCode: code, twoFactorMethod: method });
}
});
});

View File

@ -16,7 +16,6 @@ import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation';
import FormContainer, { FormContainerInner } from '../containers/FormContainer';
import TextInput from '../containers/TextInput';
import { animateNextTransition } from '../utils/layoutAnimation';
import { loginRequest as loginRequestAction } from '../actions/login';
import LoginServices from '../containers/LoginServices';
@ -81,20 +80,13 @@ class LoginView extends React.Component {
super(props);
this.state = {
user: '',
password: '',
code: '',
showTOTP: false
password: ''
};
}
componentWillReceiveProps(nextProps) {
const { error } = this.props;
if (nextProps.failure && !equal(error, nextProps.error)) {
if (nextProps.error && nextProps.error.error === 'totp-required') {
animateNextTransition();
this.setState({ showTOTP: true });
return;
}
Alert.alert(I18n.t('Oops'), I18n.t('Login_error'));
}
}
@ -115,12 +107,7 @@ class LoginView extends React.Component {
}
valid = () => {
const {
user, password, code, showTOTP
} = this.state;
if (showTOTP) {
return code.trim();
}
const { user, password } = this.state;
return user.trim() && password.trim();
}
@ -129,10 +116,10 @@ class LoginView extends React.Component {
return;
}
const { user, password, code } = this.state;
const { user, password } = this.state;
const { loginRequest } = this.props;
Keyboard.dismiss();
loginRequest({ user, password, code });
loginRequest({ user, password });
analytics().logEvent('login');
}
@ -211,50 +198,13 @@ class LoginView extends React.Component {
);
}
renderTOTP = () => {
const { isFetching, theme } = this.props;
return (
<>
<Text style={[styles.title, sharedStyles.textBold, { color: themes[theme].titleText }]}>{I18n.t('Two_Factor_Authentication')}</Text>
<Text
style={[sharedStyles.loginSubtitle, sharedStyles.textRegular, { color: themes[theme].titleText }]}
>
{I18n.t('Whats_your_2fa')}
</Text>
<TextInput
inputRef={ref => this.codeInput = ref}
autoFocus
onChangeText={value => this.setState({ code: value })}
keyboardType='numeric'
returnKeyType='send'
autoCapitalize='none'
onSubmitEditing={this.submit}
testID='login-view-totp'
containerStyle={sharedStyles.inputLastChild}
theme={theme}
/>
<Button
title={I18n.t('Confirm')}
type='primary'
onPress={this.submit}
testID='login-view-submit'
loading={isFetching}
disabled={!this.valid()}
theme={theme}
/>
</>
);
}
render() {
const { showTOTP } = this.state;
const { Accounts_ShowFormLogin, theme } = this.props;
return (
<FormContainer theme={theme}>
<FormContainerInner>
{!showTOTP ? <LoginServices separator={Accounts_ShowFormLogin} /> : null}
{!showTOTP ? this.renderUserForm() : null}
{showTOTP ? this.renderTOTP() : null}
<LoginServices separator={Accounts_ShowFormLogin} />
{this.renderUserForm()}
</FormContainerInner>
</FormContainer>
);

View File

@ -243,10 +243,10 @@ class ProfileView extends React.Component {
} else {
setUser({ ...params });
}
this.setState({ saving: false });
EventEmitter.emit(LISTENER, { message: I18n.t('Profile_saved_successfully') });
this.init();
}
this.setState({ saving: false });
} catch (e) {
this.setState({ saving: false, currentPassword: null });
this.handleError(e, 'saveUserProfile', 'saving_profile');

View File

@ -109,7 +109,8 @@
"semver": "6.3.0",
"snyk": "1.210.0",
"strip-ansi": "5.2.0",
"url-parse": "^1.4.7"
"url-parse": "^1.4.7",
"use-deep-compare-effect": "^1.3.1"
},
"devDependencies": {
"@babel/core": "^7.6.2",

View File

@ -2265,11 +2265,31 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
"@types/prop-types@*":
version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
"@types/react@*":
version "16.9.31"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.31.tgz#6a543529766c8934ec8a89667376c2e73e9e2636"
integrity sha512-NpYJpNMWScFXtx3A2BJMeew2G3+9SEslVWMdxNJ6DLvxIuxWjY1bizK9q5Y1ujhln31vtjmhjOAYDr9Xx3k9FQ==
dependencies:
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/use-deep-compare-effect@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@types/use-deep-compare-effect/-/use-deep-compare-effect-1.2.0.tgz#d55d9bda6fea5ff7c93038c53052db1b3145f5d9"
integrity sha512-2uNqaSobMvUTGR7G72tUHDX+Kx341q25OuM0m2B6VID7eljIvYuDaFTKfmDnbvej67yEhCc35zA6dmIYriwOXA==
dependencies:
"@types/react" "*"
"@types/yargs-parser@*":
version "13.1.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228"
@ -3910,6 +3930,11 @@ cssstyle@^1.0.0:
dependencies:
cssom "0.3.x"
csstype@^2.2.0:
version "2.6.10"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==
csstype@^2.5.7:
version "2.6.6"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41"
@ -4107,6 +4132,11 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
dequal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-1.0.0.tgz#41c6065e70de738541c82cdbedea5292277a017e"
integrity sha512-/Nd1EQbQbI9UbSHrMiKZjFLrXSnU328iQdZKPQf78XQI6C+gutkFUeoHpG5J08Ioa6HeRbRNFpSIclh1xyG0mw==
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
@ -12435,6 +12465,15 @@ urlgrey@^0.4.4:
resolved "https://registry.yarnpkg.com/urlgrey/-/urlgrey-0.4.4.tgz#892fe95960805e85519f1cd4389f2cb4cbb7652f"
integrity sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=
use-deep-compare-effect@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/use-deep-compare-effect/-/use-deep-compare-effect-1.3.1.tgz#90bdbed97e1acb8423f7bb0bf24de58590d021af"
integrity sha512-ejL+Al+aeDyC9Sywx56ti4PtSwkf6BH27tEptMWF2cfO41/auG0nRRsArh6Vv5bUyBe3z7IyxmgQCK5nas70hg==
dependencies:
"@babel/runtime" "^7.7.2"
"@types/use-deep-compare-effect" "^1.2.0"
dequal "^1.0.0"
use-subscription@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.3.0.tgz#3df13a798e826c8d462899423293289a3362e4e6"