[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_file: 'Choose file',
Choose_where_you_want_links_be_opened: 'Choose where you want links be opened', Choose_where_you_want_links_be_opened: 'Choose where you want links be opened',
Code: 'Code', Code: 'Code',
Code_or_password_invalid: 'Code or password invalid',
Collaborative: 'Collaborative', Collaborative: 'Collaborative',
Confirm: 'Confirm', Confirm: 'Confirm',
Connect: 'Connect', Connect: 'Connect',
@ -330,6 +331,7 @@ export default {
Only_authorized_users_can_write_new_messages: 'Only authorized users can write new messages', Only_authorized_users_can_write_new_messages: 'Only authorized users can write new messages',
Open_emoji_selector: 'Open emoji selector', Open_emoji_selector: 'Open emoji selector',
Open_Source_Communication: 'Open Source Communication', Open_Source_Communication: 'Open Source Communication',
Open_your_authentication_app_and_enter_the_code: 'Open your authentication app and enter the code.',
OR: 'OR', OR: 'OR',
Overwrites_the_server_configuration_and_use_room_config: 'Overwrites the server configuration and use room config', Overwrites_the_server_configuration_and_use_room_config: 'Overwrites the server configuration and use room config',
Password: 'Password', Password: 'Password',
@ -416,6 +418,7 @@ export default {
Send_audio_message: 'Send audio message', Send_audio_message: 'Send audio message',
Send_crash_report: 'Send crash report', Send_crash_report: 'Send crash report',
Send_message: 'Send message', Send_message: 'Send message',
Send_me_the_code_again: 'Send me the code again',
Send_to: 'Send to...', Send_to: 'Send to...',
Sent_an_attachment: 'Sent an attachment', Sent_an_attachment: 'Sent an attachment',
Server: 'Server', Server: 'Server',
@ -499,8 +502,10 @@ export default {
Uses_server_configuration: 'Uses server configuration', 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?"', 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', Validating: 'Validating',
Verify: 'Verify',
Verify_email_title: 'Registration Succeeded!', 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_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', Video_call: 'Video call',
View_Original: 'View Original', View_Original: 'View Original',
Voice_call: 'Voice call', Voice_call: 'Voice call',

View File

@ -140,6 +140,7 @@ export default {
Choose_file: 'Enviar arquivo', Choose_file: 'Enviar arquivo',
Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos', Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos',
Code: 'Código', Code: 'Código',
Code_or_password_invalid: 'Código ou senha inválido',
Collaborative: 'Colaborativo', Collaborative: 'Colaborativo',
Confirm: 'Confirmar', Confirm: 'Confirmar',
Connect: 'Conectar', Connect: 'Conectar',
@ -302,6 +303,7 @@ export default {
Only_authorized_users_can_write_new_messages: 'Somente usuários autorizados podem escrever novas mensagens', Only_authorized_users_can_write_new_messages: 'Somente usuários autorizados podem escrever novas mensagens',
Open_emoji_selector: 'Abrir seletor de emoji', Open_emoji_selector: 'Abrir seletor de emoji',
Open_Source_Communication: 'Comunicação Open Source', 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', OR: 'OU',
Overwrites_the_server_configuration_and_use_room_config: 'Substituir a configuração do servidor e usar a configuração da sala', Overwrites_the_server_configuration_and_use_room_config: 'Substituir a configuração do servidor e usar a configuração da sala',
Password: 'Senha', Password: 'Senha',
@ -378,6 +380,7 @@ export default {
Send: 'Enviar', Send: 'Enviar',
Send_audio_message: 'Enviar mensagem de áudio', Send_audio_message: 'Enviar mensagem de áudio',
Send_message: 'Enviar mensagem', Send_message: 'Enviar mensagem',
Send_me_the_code_again: 'Envie-me o código novamente',
Send_to: 'Enviar para...', Send_to: 'Enviar para...',
Sent_an_attachment: 'Enviou um anexo', Sent_an_attachment: 'Enviou um anexo',
Server: 'Servidor', Server: 'Servidor',
@ -446,8 +449,10 @@ export default {
Username_or_email: 'Usuário ou email', Username_or_email: 'Usuário ou email',
Uses_server_configuration: 'Usar configuração do servidor', 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?', 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_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_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', Video_call: 'Chamada de vídeo',
Voice_call: 'Chamada de voz', Voice_call: 'Chamada de voz',
Websocket_disabled: 'Websocket está desativado para esse servidor.\n{{contact}}', 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 Tablet, { initTabletNav } from './tablet';
import sharedStyles from './views/Styles'; import sharedStyles from './views/Styles';
import { SplitContext } from './split'; import { SplitContext } from './split';
import TwoFactor from './containers/TwoFactor';
import RoomsListView from './views/RoomsListView'; import RoomsListView from './views/RoomsListView';
import RoomView from './views/RoomView'; import RoomView from './views/RoomView';
@ -721,6 +722,7 @@ export default class Root extends React.Component {
}} }}
> >
{content} {content}
<TwoFactor />
</ThemeContext.Provider> </ThemeContext.Provider>
</Provider> </Provider>
</AppearanceProvider> </AppearanceProvider>

View File

@ -48,6 +48,7 @@ import { getDeviceToken } from '../notifications/push';
import { SERVERS, SERVER_URL } from '../constants/userDefaults'; import { SERVERS, SERVER_URL } from '../constants/userDefaults';
import { setActiveUsers } from '../actions/activeUsers'; import { setActiveUsers } from '../actions/activeUsers';
import I18n from '../i18n'; import I18n from '../i18n';
import { twoFactor } from '../utils/twoFactor';
const TOKEN_KEY = 'reactnativemeteor_usertoken'; const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY'; const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
@ -76,7 +77,7 @@ const RocketChat = {
name, users, type, readOnly, broadcast name, users, type, readOnly, broadcast
}) { }) {
// RC 0.51.0 // 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() { async getUserToken() {
try { try {
@ -195,6 +196,10 @@ const RocketChat = {
this.sdk = null; this.sdk = null;
} }
if (this.code) {
this.code = null;
}
// Use useSsl: false only if server url starts with http:// // Use useSsl: false only if server url starts with http://
const useSsl = !/http:\/\//.test(server); const useSsl = !/http:\/\//.test(server);
@ -304,25 +309,47 @@ const RocketChat = {
}, },
updateJitsiTimeout(rid) { updateJitsiTimeout(rid) {
return this.sdk.methodCall('jitsi:updateTimeout', rid); return this.methodCall('jitsi:updateTimeout', rid);
}, },
register(credentials) { register(credentials) {
// RC 0.50.0 // RC 0.50.0
return this.sdk.post('users.register', credentials, false); return this.post('users.register', credentials, false);
}, },
setUsername(username) { setUsername(username) {
// RC 0.51.0 // RC 0.51.0
return this.sdk.methodCall('setUsername', username); return this.methodCall('setUsername', username);
}, },
forgotPassword(email) { forgotPassword(email) {
// RC 0.64.0 // 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 }; let params = { user, password };
const state = reduxStore.getState(); const state = reduxStore.getState();
@ -341,16 +368,8 @@ const RocketChat = {
}; };
} }
if (code) {
params = {
user,
password,
code
};
}
try { try {
return await this.login(params); return this.loginTOTP(params);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -487,7 +506,7 @@ const RocketChat = {
}; };
try { try {
// RC 0.60.0 // RC 0.60.0
await this.sdk.post('push.token', data); await this.post('push.token', data);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
@ -599,12 +618,12 @@ const RocketChat = {
spotlight(search, usernames, type) { spotlight(search, usernames, type) {
// RC 0.51.0 // RC 0.51.0
return this.sdk.methodCall('spotlight', search, usernames, type); return this.methodCall('spotlight', search, usernames, type);
}, },
createDirectMessage(username) { createDirectMessage(username) {
// RC 0.59.0 // RC 0.59.0
return this.sdk.post('im.create', { username }); return this.post('im.create', { username });
}, },
createGroupChat() { createGroupChat() {
@ -612,14 +631,14 @@ const RocketChat = {
users = users.map(u => u.name); users = users.map(u => u.name);
// RC 3.1.0 // RC 3.1.0
return this.sdk.methodCall('createDirectMessage', ...users); return this.methodCall('createDirectMessage', ...users);
}, },
createDiscussion({ createDiscussion({
prid, pmid, t_name, reply, users prid, pmid, t_name, reply, users
}) { }) {
// RC 1.0.0 // RC 1.0.0
return this.sdk.post('rooms.createDiscussion', { return this.post('rooms.createDiscussion', {
prid, pmid, t_name, reply, users prid, pmid, t_name, reply, users
}); });
}, },
@ -628,9 +647,9 @@ const RocketChat = {
// TODO: join code // TODO: join code
// RC 0.48.0 // RC 0.48.0
if (type === 'p') { 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, triggerBlockAction,
triggerSubmitView, triggerSubmitView,
@ -662,34 +681,34 @@ const RocketChat = {
}, },
deleteMessage(messageId, rid) { deleteMessage(messageId, rid) {
// RC 0.48.0 // 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) { editMessage(message) {
const { id, msg, rid } = message; const { id, msg, rid } = message;
// RC 0.49.0 // 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 }) { markAsUnread({ messageId }) {
return this.sdk.post('subscriptions.unread', { firstUnreadMessage: { _id: messageId } }); return this.post('subscriptions.unread', { firstUnreadMessage: { _id: messageId } });
}, },
toggleStarMessage(messageId, starred) { toggleStarMessage(messageId, starred) {
if (starred) { if (starred) {
// RC 0.59.0 // RC 0.59.0
return this.sdk.post('chat.unStarMessage', { messageId }); return this.post('chat.unStarMessage', { messageId });
} }
// RC 0.59.0 // RC 0.59.0
return this.sdk.post('chat.starMessage', { messageId }); return this.post('chat.starMessage', { messageId });
}, },
togglePinMessage(messageId, pinned) { togglePinMessage(messageId, pinned) {
if (pinned) { if (pinned) {
// RC 0.59.0 // RC 0.59.0
return this.sdk.post('chat.unPinMessage', { messageId }); return this.post('chat.unPinMessage', { messageId });
} }
// RC 0.59.0 // RC 0.59.0
return this.sdk.post('chat.pinMessage', { messageId }); return this.post('chat.pinMessage', { messageId });
}, },
reportMessage(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) { async getRoom(rid) {
try { try {
@ -739,42 +758,42 @@ const RocketChat = {
}, },
emitTyping(room, t = true) { emitTyping(room, t = true) {
const { login } = reduxStore.getState(); 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() { setUserPresenceAway() {
return this.sdk.methodCall('UserPresence:away'); return this.methodCall('UserPresence:away');
}, },
setUserPresenceOnline() { setUserPresenceOnline() {
return this.sdk.methodCall('UserPresence:online'); return this.methodCall('UserPresence:online');
}, },
setUserPresenceDefaultStatus(status) { setUserPresenceDefaultStatus(status) {
return this.sdk.methodCall('UserPresence:setDefaultStatus', status); return this.methodCall('UserPresence:setDefaultStatus', status);
}, },
setUserStatus(message) { setUserStatus(message) {
// RC 1.2.0 // RC 1.2.0
return this.sdk.post('users.setStatus', { message }); return this.post('users.setStatus', { message });
}, },
setReaction(emoji, messageId) { setReaction(emoji, messageId) {
// RC 0.62.2 // RC 0.62.2
return this.sdk.post('chat.react', { emoji, messageId }); return this.post('chat.react', { emoji, messageId });
}, },
toggleFavorite(roomId, favorite) { toggleFavorite(roomId, favorite) {
// RC 0.64.0 // RC 0.64.0
return this.sdk.post('rooms.favorite', { roomId, favorite }); return this.post('rooms.favorite', { roomId, favorite });
}, },
toggleRead(read, roomId) { toggleRead(read, roomId) {
if (read) { 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) { getRoomMembers(rid, allUsers, skip = 0, limit = 10) {
// RC 0.42.0 // RC 0.42.0
return this.sdk.methodCall('getUsersOfRoom', rid, allUsers, { skip, limit }); return this.methodCall('getUsersOfRoom', rid, allUsers, { skip, limit });
}, },
getUserRoles() { getUserRoles() {
// RC 0.27.0 // RC 0.27.0
return this.sdk.methodCall('getUserRoles'); return this.methodCall('getUserRoles');
}, },
getRoomCounters(roomId, t) { getRoomCounters(roomId, t) {
// RC 0.65.0 // RC 0.65.0
@ -816,63 +835,110 @@ const RocketChat = {
toggleBlockUser(rid, blocked, block) { toggleBlockUser(rid, blocked, block) {
if (block) { if (block) {
// RC 0.49.0 // RC 0.49.0
return this.sdk.methodCall('blockUser', { rid, blocked }); return this.methodCall('blockUser', { rid, blocked });
} }
// RC 0.49.0 // RC 0.49.0
return this.sdk.methodCall('unblockUser', { rid, blocked }); return this.methodCall('unblockUser', { rid, blocked });
}, },
leaveRoom(roomId, t) { leaveRoom(roomId, t) {
// RC 0.48.0 // RC 0.48.0
return this.sdk.post(`${ this.roomTypeToApiType(t) }.leave`, { roomId }); return this.post(`${ this.roomTypeToApiType(t) }.leave`, { roomId });
}, },
deleteRoom(roomId, t) { deleteRoom(roomId, t) {
// RC 0.49.0 // 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) { toggleMuteUserInRoom(rid, username, mute) {
if (mute) { if (mute) {
// RC 0.51.0 // RC 0.51.0
return this.sdk.methodCall('muteUserInRoom', { rid, username }); return this.methodCall('muteUserInRoom', { rid, username });
} }
// RC 0.51.0 // RC 0.51.0
return this.sdk.methodCall('unmuteUserInRoom', { rid, username }); return this.methodCall('unmuteUserInRoom', { rid, username });
}, },
toggleArchiveRoom(roomId, t, archive) { toggleArchiveRoom(roomId, t, archive) {
if (archive) { if (archive) {
// RC 0.48.0 // 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 // RC 0.48.0
return this.sdk.post(`${ this.roomTypeToApiType(t) }.unarchive`, { roomId }); return this.post(`${ this.roomTypeToApiType(t) }.unarchive`, { roomId });
}, },
hideRoom(roomId, t) { hideRoom(roomId, t) {
return this.sdk.post(`${ this.roomTypeToApiType(t) }.close`, { roomId }); return this.post(`${ this.roomTypeToApiType(t) }.close`, { roomId });
}, },
saveRoomSettings(rid, params) { saveRoomSettings(rid, params) {
// RC 0.55.0 // 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) { saveUserProfile(data, customFields) {
// RC 0.62.2 // RC 0.62.2
return this.sdk.post('users.updateOwnBasicInfo', { data, customFields }); return this.post('users.updateOwnBasicInfo', { data, customFields });
}, },
saveUserPreferences(params) { saveUserPreferences(params) {
// RC 0.51.0 // RC 0.51.0
return this.sdk.methodCall('saveUserPreferences', params); return this.methodCall('saveUserPreferences', params);
}, },
saveNotificationSettings(roomId, notifications) { saveNotificationSettings(roomId, notifications) {
// RC 0.63.0 // RC 0.63.0
return this.sdk.post('rooms.saveNotification', { roomId, notifications }); return this.post('rooms.saveNotification', { roomId, notifications });
}, },
addUsersToRoom(rid) { addUsersToRoom(rid) {
let { users } = reduxStore.getState().selectedUsers; let { users } = reduxStore.getState().selectedUsers;
users = users.map(u => u.name); users = users.map(u => u.name);
// RC 0.51.0 // RC 0.51.0
return this.sdk.methodCall('addUsersToRoom', { rid, users }); return this.methodCall('addUsersToRoom', { rid, users });
}, },
getSingleMessage(msgId) { getSingleMessage(msgId) {
// RC 0.57.0 // RC 0.57.0
return this.sdk.methodCall('getSingleMessage', msgId); return this.methodCall('getSingleMessage', msgId);
}, },
async hasPermission(permissions, rid) { async hasPermission(permissions, rid) {
const db = database.active; const db = database.active;
@ -915,15 +981,15 @@ const RocketChat = {
}, },
getAvatarSuggestion() { getAvatarSuggestion() {
// RC 0.51.0 // RC 0.51.0
return this.sdk.methodCall('getAvatarSuggestion'); return this.methodCall('getAvatarSuggestion');
}, },
resetAvatar(userId) { resetAvatar(userId) {
// RC 0.55.0 // RC 0.55.0
return this.sdk.post('users.resetAvatar', { userId }); return this.post('users.resetAvatar', { userId });
}, },
setAvatarFromService({ data, contentType = '', service = null }) { setAvatarFromService({ data, contentType = '', service = null }) {
// RC 0.51.0 // RC 0.51.0
return this.sdk.methodCall('setAvatarFromService', data, contentType, service); return this.methodCall('setAvatarFromService', data, contentType, service);
}, },
async getAllowCrashReport() { async getAllowCrashReport() {
const allowCrashReport = await AsyncStorage.getItem(CRASH_REPORT_KEY); const allowCrashReport = await AsyncStorage.getItem(CRASH_REPORT_KEY);
@ -1042,9 +1108,9 @@ const RocketChat = {
toggleFollowMessage(mid, follow) { toggleFollowMessage(mid, follow) {
// RC 1.0 // RC 1.0
if (follow) { 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 }) { getThreadsList({ rid, count, offset }) {
// RC 1.0 // RC 1.0
@ -1060,7 +1126,7 @@ const RocketChat = {
}, },
runSlashCommand(command, roomId, params, triggerId, tmid) { runSlashCommand(command, roomId, params, triggerId, tmid) {
// RC 0.60.2 // RC 0.60.2
return this.sdk.post('commands.run', { return this.post('commands.run', {
command, roomId, params, triggerId, tmid command, roomId, params, triggerId, tmid
}); });
}, },
@ -1072,7 +1138,7 @@ const RocketChat = {
}, },
executeCommandPreview(command, params, roomId, previewItem, triggerId, tmid) { executeCommandPreview(command, params, roomId, previewItem, triggerId, tmid) {
// RC 0.65.0 // RC 0.65.0
return this.sdk.post('commands.preview', { return this.post('commands.preview', {
command, params, roomId, previewItem, triggerId, tmid command, params, roomId, previewItem, triggerId, tmid
}); });
}, },
@ -1135,13 +1201,13 @@ const RocketChat = {
saveAutoTranslate({ saveAutoTranslate({
rid, field, value, options rid, field, value, options
}) { }) {
return this.sdk.methodCall('autoTranslate.saveSettings', rid, field, value, options); return this.methodCall('autoTranslate.saveSettings', rid, field, value, options);
}, },
getSupportedLanguagesAutoTranslate() { getSupportedLanguagesAutoTranslate() {
return this.sdk.methodCall('autoTranslate.getSupportedLanguages', 'en'); return this.methodCall('autoTranslate.getSupportedLanguages', 'en');
}, },
translateMessage(message, targetLanguage) { translateMessage(message, targetLanguage) {
return this.sdk.methodCall('autoTranslate.translateMessage', message, targetLanguage); return this.methodCall('autoTranslate.translateMessage', message, targetLanguage);
}, },
getRoomTitle(room) { getRoomTitle(room) {
const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings; const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings;
@ -1160,15 +1226,15 @@ const RocketChat = {
findOrCreateInvite({ rid, days, maxUses }) { findOrCreateInvite({ rid, days, maxUses }) {
// RC 2.4.0 // RC 2.4.0
return this.sdk.post('findOrCreateInvite', { rid, days, maxUses }); return this.post('findOrCreateInvite', { rid, days, maxUses });
}, },
validateInviteToken(token) { validateInviteToken(token) {
// RC 2.4.0 // RC 2.4.0
return this.sdk.post('validateInviteToken', { token }); return this.post('validateInviteToken', { token });
}, },
useInviteToken(token) { useInviteToken(token) {
// RC 2.4.0 // 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) => { export const setBasicAuth = (basicAuth) => {
_basicAuth = basicAuth; _basicAuth = basicAuth;
if (basicAuth) { if (basicAuth) {
RocketChatSettings.customHeaders = { ...RocketChatSettings.customHeaders, Authorization: `Basic ${ _basicAuth }` }; RocketChatSettings.customHeaders = { ...headers, Authorization: `Basic ${ _basicAuth }` };
} else { } else {
RocketChatSettings.customHeaders = headers; 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 { themedHeader } from '../utils/navigation';
import FormContainer, { FormContainerInner } from '../containers/FormContainer'; import FormContainer, { FormContainerInner } from '../containers/FormContainer';
import TextInput from '../containers/TextInput'; import TextInput from '../containers/TextInput';
import { animateNextTransition } from '../utils/layoutAnimation';
import { loginRequest as loginRequestAction } from '../actions/login'; import { loginRequest as loginRequestAction } from '../actions/login';
import LoginServices from '../containers/LoginServices'; import LoginServices from '../containers/LoginServices';
@ -81,20 +80,13 @@ class LoginView extends React.Component {
super(props); super(props);
this.state = { this.state = {
user: '', user: '',
password: '', password: ''
code: '',
showTOTP: false
}; };
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const { error } = this.props; const { error } = this.props;
if (nextProps.failure && !equal(error, nextProps.error)) { 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')); Alert.alert(I18n.t('Oops'), I18n.t('Login_error'));
} }
} }
@ -115,12 +107,7 @@ class LoginView extends React.Component {
} }
valid = () => { valid = () => {
const { const { user, password } = this.state;
user, password, code, showTOTP
} = this.state;
if (showTOTP) {
return code.trim();
}
return user.trim() && password.trim(); return user.trim() && password.trim();
} }
@ -129,10 +116,10 @@ class LoginView extends React.Component {
return; return;
} }
const { user, password, code } = this.state; const { user, password } = this.state;
const { loginRequest } = this.props; const { loginRequest } = this.props;
Keyboard.dismiss(); Keyboard.dismiss();
loginRequest({ user, password, code }); loginRequest({ user, password });
analytics().logEvent('login'); 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() { render() {
const { showTOTP } = this.state;
const { Accounts_ShowFormLogin, theme } = this.props; const { Accounts_ShowFormLogin, theme } = this.props;
return ( return (
<FormContainer theme={theme}> <FormContainer theme={theme}>
<FormContainerInner> <FormContainerInner>
{!showTOTP ? <LoginServices separator={Accounts_ShowFormLogin} /> : null} <LoginServices separator={Accounts_ShowFormLogin} />
{!showTOTP ? this.renderUserForm() : null} {this.renderUserForm()}
{showTOTP ? this.renderTOTP() : null}
</FormContainerInner> </FormContainerInner>
</FormContainer> </FormContainer>
); );

View File

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

View File

@ -109,7 +109,8 @@
"semver": "6.3.0", "semver": "6.3.0",
"snyk": "1.210.0", "snyk": "1.210.0",
"strip-ansi": "5.2.0", "strip-ansi": "5.2.0",
"url-parse": "^1.4.7" "url-parse": "^1.4.7",
"use-deep-compare-effect": "^1.3.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.6.2", "@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" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== 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": "@types/stack-utils@^1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== 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@*": "@types/yargs-parser@*":
version "13.1.0" version "13.1.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228"
@ -3910,6 +3930,11 @@ cssstyle@^1.0.0:
dependencies: dependencies:
cssom "0.3.x" 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: csstype@^2.5.7:
version "2.6.6" version "2.6.6"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41" 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" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= 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: destroy@~1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 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" resolved "https://registry.yarnpkg.com/urlgrey/-/urlgrey-0.4.4.tgz#892fe95960805e85519f1cd4389f2cb4cbb7652f"
integrity sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8= 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: use-subscription@^1.0.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.3.0.tgz#3df13a798e826c8d462899423293289a3362e4e6" resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.3.0.tgz#3df13a798e826c8d462899423293289a3362e4e6"