diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index e89b9df7b..b9f723706 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -560,6 +560,7 @@ export default { You_will_be_logged_out_of_this_application: 'You will be logged out of this application.', Clear: 'Clear', This_will_clear_all_your_offline_data: 'This will clear all your offline data.', + This_will_remove_all_data_from_this_server: 'This will remove all data from this server.', Mark_unread: 'Mark Unread', Wait_activation_warning: 'Before you can login, your account must be manually activated by an administrator.' }; diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index 5cd3fb3e1..c104637d7 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -499,6 +499,7 @@ export default { You_will_be_logged_out_of_this_application: 'Você sairá deste aplicativo.', Clear: 'Limpar', This_will_clear_all_your_offline_data: 'Isto limpará todos os seus dados offline.', + This_will_remove_all_data_from_this_server: 'Isto removerá todos os dados desse servidor.', Mark_unread: 'Marcar como não Lida', Wait_activation_warning: 'Antes que você possa fazer o login, sua conta deve ser manualmente ativada por um administrador.' }; diff --git a/app/lib/database/index.js b/app/lib/database/index.js index 67f1188ae..31bf5317b 100644 --- a/app/lib/database/index.js +++ b/app/lib/database/index.js @@ -33,6 +33,36 @@ if (__DEV__ && isIOS) { console.log(appGroupPath); } +export const getDatabase = (database = '') => { + const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.'); + const dbName = `${ appGroupPath }${ path }.db`; + + const adapter = new SQLiteAdapter({ + dbName, + schema: appSchema, + migrations + }); + + return new Database({ + adapter, + modelClasses: [ + Subscription, + Room, + Message, + Thread, + ThreadMessage, + CustomEmoji, + FrequentlyUsedEmoji, + Upload, + Setting, + Role, + Permission, + SlashCommand + ], + actionsEnabled: true + }); +}; + class DB { databases = { serversDB: new Database({ @@ -86,34 +116,8 @@ class DB { }); } - setActiveDB(database = '') { - const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.'); - const dbName = `${ appGroupPath }${ path }.db`; - - const adapter = new SQLiteAdapter({ - dbName, - schema: appSchema, - migrations - }); - - this.databases.activeDB = new Database({ - adapter, - modelClasses: [ - Subscription, - Room, - Message, - Thread, - ThreadMessage, - CustomEmoji, - FrequentlyUsedEmoji, - Upload, - Setting, - Role, - Permission, - SlashCommand - ], - actionsEnabled: true - }); + setActiveDB(database) { + this.databases.activeDB = getDatabase(database); } } diff --git a/app/lib/methods/logout.js b/app/lib/methods/logout.js new file mode 100644 index 000000000..4064362de --- /dev/null +++ b/app/lib/methods/logout.js @@ -0,0 +1,127 @@ +import RNUserDefaults from 'rn-user-defaults'; +import * as FileSystem from 'expo-file-system'; +import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; + +import { SERVERS, SERVER_URL } from '../../constants/userDefaults'; +import { getDeviceToken } from '../../notifications/push'; +import { extractHostname } from '../../utils/server'; +import { BASIC_AUTH_KEY } from '../../utils/fetch'; +import database, { getDatabase } from '../database'; +import RocketChat from '../rocketchat'; +import { useSsl } from '../../utils/url'; + +async function removeServerKeys({ server, userId }) { + await RNUserDefaults.clear(`${ RocketChat.TOKEN_KEY }-${ server }`); + await RNUserDefaults.clear(`${ RocketChat.TOKEN_KEY }-${ userId }`); + await RNUserDefaults.clear(`${ BASIC_AUTH_KEY }-${ server }`); +} + +async function removeSharedCredentials({ server }) { + try { + const servers = await RNUserDefaults.objectForKey(SERVERS); + await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server)); + + // clear certificate for server - SSL Pinning + const certificate = await RNUserDefaults.objectForKey(extractHostname(server)); + if (certificate && certificate.path) { + await RNUserDefaults.clear(extractHostname(server)); + await FileSystem.deleteAsync(certificate.path); + } + } catch (e) { + console.log('removeSharedCredentials', e); + } +} + +async function removeServerData({ server }) { + try { + const batch = []; + const serversDB = database.servers; + const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`); + + const usersCollection = serversDB.collections.get('users'); + if (userId) { + const userRecord = await usersCollection.find(userId); + batch.push(userRecord.prepareDestroyPermanently()); + } + const serverCollection = serversDB.collections.get('servers'); + const serverRecord = await serverCollection.find(server); + batch.push(serverRecord.prepareDestroyPermanently()); + + await serversDB.action(() => serversDB.batch(...batch)); + await removeSharedCredentials({ server }); + await removeServerKeys({ server }); + } catch (e) { + console.log('removeServerData', e); + } +} + +async function removeCurrentServer() { + await RNUserDefaults.clear('currentServer'); + await RNUserDefaults.clear(RocketChat.TOKEN_KEY); +} + +async function removeServerDatabase({ server }) { + try { + const db = getDatabase(server); + await db.action(() => db.unsafeResetDatabase()); + } catch (e) { + console.log(e); + } +} + +export async function removeServer({ server }) { + try { + const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`); + if (userId) { + const resume = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ userId }`); + + const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) }); + await sdk.login({ resume }); + + const token = getDeviceToken(); + if (token) { + await sdk.del('push.token', { token }); + } + + await sdk.logout(); + } + + await removeServerData({ server }); + await removeServerDatabase({ server }); + } catch (e) { + console.log('removePush', e); + } +} + +export default async function logout({ server }) { + if (this.roomsSub) { + this.roomsSub.stop(); + this.roomsSub = null; + } + + if (this.activeUsersSubTimeout) { + clearTimeout(this.activeUsersSubTimeout); + this.activeUsersSubTimeout = false; + } + + try { + await this.removePushToken(); + } catch (e) { + console.log('removePushToken', e); + } + + try { + // RC 0.60.0 + await this.sdk.logout(); + } catch (e) { + console.log('logout', e); + } + + if (this.sdk) { + this.sdk = null; + } + + await removeServerData({ server }); + await removeCurrentServer(); + await removeServerDatabase({ server }); +} diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 098717d26..5bef3ad8a 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -3,7 +3,6 @@ import semver from 'semver'; import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; import RNUserDefaults from 'rn-user-defaults'; import { Q } from '@nozbe/watermelondb'; -import * as FileSystem from 'expo-file-system'; import reduxStore from './createStore'; import defaultSettings from '../constants/settings'; @@ -11,8 +10,7 @@ import messagesStatus from '../constants/messagesStatus'; import database from './database'; import log from '../utils/log'; import { isIOS, getBundleId } from '../utils/deviceInfo'; -import { extractHostname } from '../utils/server'; -import fetch, { BASIC_AUTH_KEY } from '../utils/fetch'; +import fetch from '../utils/fetch'; import { setUser, setLoginServices, loginRequest } from '../actions/login'; import { disconnect, connectSuccess, connectRequest } from '../actions/connect'; @@ -43,12 +41,13 @@ import sendMessage, { sendMessageCall } from './methods/sendMessage'; import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage'; import callJitsi from './methods/callJitsi'; +import logout, { removeServer } from './methods/logout'; 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'; +import { useSsl } from '../utils/url'; const TOKEN_KEY = 'reactnativemeteor_usertoken'; const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY'; @@ -86,10 +85,7 @@ const RocketChat = { } }, async getWebsocketInfo({ server }) { - // Use useSsl: false only if server url starts with http:// - const useSsl = !/http:\/\//.test(server); - - const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl }); + const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) }); try { await sdk.connect(); @@ -200,10 +196,7 @@ const RocketChat = { this.code = null; } - // Use useSsl: false only if server url starts with http:// - const useSsl = !/http:\/\//.test(server); - - this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl }); + this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) }); this.getSettings(); const sdkConnect = () => this.sdk.connect() @@ -270,10 +263,7 @@ const RocketChat = { this.shareSDK = null; } - // Use useSsl: false only if server url starts with http:// - const useSsl = !/http:\/\//.test(server); - - this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl }); + this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) }); // set Server const serversDB = database.servers; @@ -408,73 +398,8 @@ const RocketChat = { throw e; } }, - async logout({ server }) { - if (this.roomsSub) { - this.roomsSub.stop(); - this.roomsSub = null; - } - - if (this.activeUsersSubTimeout) { - clearTimeout(this.activeUsersSubTimeout); - this.activeUsersSubTimeout = false; - } - - try { - await this.removePushToken(); - } catch (error) { - console.log('logout -> removePushToken -> catch -> error', error); - } - try { - // RC 0.60.0 - await this.sdk.logout(); - } catch (error) { - console.log('​logout -> api logout -> catch -> error', error); - } - this.sdk = null; - - try { - const servers = await RNUserDefaults.objectForKey(SERVERS); - await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server)); - // clear certificate for server - SSL Pinning - const certificate = await RNUserDefaults.objectForKey(extractHostname(server)); - if (certificate && certificate.path) { - await RNUserDefaults.clear(extractHostname(server)); - await FileSystem.deleteAsync(certificate.path); - } - } catch (error) { - console.log('logout_rn_user_defaults', error); - } - - const userId = await RNUserDefaults.get(`${ TOKEN_KEY }-${ server }`); - - try { - const serversDB = database.servers; - await serversDB.action(async() => { - const usersCollection = serversDB.collections.get('users'); - const userRecord = await usersCollection.find(userId); - const serverCollection = serversDB.collections.get('servers'); - const serverRecord = await serverCollection.find(server); - await serversDB.batch( - userRecord.prepareDestroyPermanently(), - serverRecord.prepareDestroyPermanently() - ); - }); - } catch (error) { - // Do nothing - } - - await RNUserDefaults.clear('currentServer'); - await RNUserDefaults.clear(TOKEN_KEY); - await RNUserDefaults.clear(`${ TOKEN_KEY }-${ server }`); - await RNUserDefaults.clear(`${ BASIC_AUTH_KEY }-${ server }`); - - try { - const db = database.active; - await db.action(() => db.unsafeResetDatabase()); - } catch (error) { - console.log(error); - } - }, + logout, + removeServer, async clearCache({ server }) { try { const serversDB = database.servers; diff --git a/app/presentation/UserItem.js b/app/presentation/UserItem.js index de8e0ef73..bb37108bf 100644 --- a/app/presentation/UserItem.js +++ b/app/presentation/UserItem.js @@ -1,13 +1,13 @@ import React from 'react'; import { Text, View, StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; -import { LongPressGestureHandler, State } from 'react-native-gesture-handler'; import Avatar from '../containers/Avatar'; import { CustomIcon } from '../lib/Icons'; import sharedStyles from '../views/Styles'; import { themes } from '../constants/colors'; import Touch from '../utils/touch'; +import LongPress from '../utils/longPress'; const styles = StyleSheet.create({ button: { @@ -41,38 +41,25 @@ const styles = StyleSheet.create({ const UserItem = ({ name, username, onPress, testID, onLongPress, style, icon, baseUrl, user, theme -}) => { - const longPress = ({ nativeEvent }) => { - if (nativeEvent.state === State.ACTIVE) { - if (onLongPress) { - onLongPress(); - } - } - }; - - return ( - ( + + - - - - - {name} - @{username} - - {icon ? : null} + + + + {name} + @{username} - - - ); -}; + {icon ? : null} + + + +); UserItem.propTypes = { name: PropTypes.string.isRequired, diff --git a/app/utils/longPress.js b/app/utils/longPress.js new file mode 100644 index 000000000..e491efcb9 --- /dev/null +++ b/app/utils/longPress.js @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { State, LongPressGestureHandler } from 'react-native-gesture-handler'; + +class LongPress extends React.Component { + setNativeProps(props) { + this.ref.setNativeProps(props); + } + + getRef = (ref) => { + this.ref = ref; + }; + + longPress = ({ nativeEvent }) => { + const { onLongPress } = this.props; + if (nativeEvent.state === State.ACTIVE) { + if (onLongPress) { + onLongPress(); + } + } + }; + + render() { + const { children, ...props } = this.props; + + return ( + + {children} + + ); + } +} + +LongPress.propTypes = { + children: PropTypes.node, + onLongPress: PropTypes.func +}; + +export default LongPress; diff --git a/app/utils/url.js b/app/utils/url.js index 856eac771..16506ed13 100644 --- a/app/utils/url.js +++ b/app/utils/url.js @@ -7,3 +7,6 @@ export const isValidURL = (url) => { + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator return !!pattern.test(url); }; + +// Use useSsl: false only if server url starts with http:// +export const useSsl = url => !/http:\/\//.test(url); diff --git a/app/views/RoomsListView/ServerDropdown.js b/app/views/RoomsListView/ServerDropdown.js index f91c20bd8..3e69b956a 100644 --- a/app/views/RoomsListView/ServerDropdown.js +++ b/app/views/RoomsListView/ServerDropdown.js @@ -22,6 +22,8 @@ import { withTheme } from '../../theme'; import { KEY_COMMAND, handleCommandSelectServer } from '../../commands'; import { isTablet } from '../../utils/deviceInfo'; import { withSplit } from '../../split'; +import LongPress from '../../utils/longPress'; +import { showConfirmationAlert } from '../../utils/info'; const ROW_HEIGHT = 68; const ANIMATION_DURATION = 200; @@ -132,7 +134,6 @@ class ServerDropdown extends Component { const { server: currentServer, selectServerRequest, navigation, split } = this.props; - this.close(); if (currentServer !== server) { const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`); @@ -152,6 +153,19 @@ class ServerDropdown extends Component { } } + remove = server => showConfirmationAlert({ + message: I18n.t('This_will_remove_all_data_from_this_server'), + callToAction: I18n.t('Delete'), + onPress: async() => { + this.close(); + try { + await RocketChat.removeServer({ server }); + } catch { + // do nothing + } + } + }); + handleCommands = ({ event }) => { const { servers } = this.state; const { navigation } = this.props; @@ -173,35 +187,37 @@ class ServerDropdown extends Component { const { server, theme } = this.props; return ( - this.select(item.id)} - testID={`rooms-list-header-server-${ item.id }`} - theme={theme} - > - - {item.iconURL - ? ( - console.warn('error loading serverIcon')} - /> - ) - : ( - - ) - } - - {item.name || item.id} - {item.id} + (item.id === server || this.remove(item.id))}> + this.select(item.id)} + testID={`rooms-list-header-server-${ item.id }`} + theme={theme} + > + + {item.iconURL + ? ( + console.warn('error loading serverIcon')} + /> + ) + : ( + + ) + } + + {item.name || item.id} + {item.id} + + {item.id === server ? : null} - {item.id === server ? : null} - - + + ); }