diff --git a/web/account/log-in-form.react.js b/web/account/log-in-form.react.js index 27c4d76c6..7fccadb6f 100644 --- a/web/account/log-in-form.react.js +++ b/web/account/log-in-form.react.js @@ -1,100 +1,129 @@ // @flow import olm from '@matrix-org/olm'; import { useConnectModal } from '@rainbow-me/rainbowkit'; import * as React from 'react'; import { useDispatch } from 'react-redux'; +import uuid from 'uuid'; import { useSigner } from 'wagmi'; import css from './log-in-form.css'; import SIWEButton from './siwe-button.react.js'; import SIWELoginForm from './siwe-login-form.react.js'; import TraditionalLoginForm from './traditional-login-form.react.js'; import OrBreak from '../components/or-break.react.js'; import { setPrimaryIdentityKeys, setNotificationIdentityKeys, + setPickledPrimaryAccount, + setPickledNotificationAccount, } from '../redux/crypto-store-reducer.js'; import { useSelector } from '../redux/redux-utils.js'; function LoginForm(): React.Node { const { openConnectModal } = useConnectModal(); const { data: signer } = useSigner(); const dispatch = useDispatch(); const primaryIdentityPublicKeys = useSelector( state => state.cryptoStore.primaryIdentityKeys, ); const notificationIdentityPublicKeys = useSelector( state => state.cryptoStore.notificationIdentityKeys, ); React.useEffect(() => { (async () => { if ( primaryIdentityPublicKeys !== null && primaryIdentityPublicKeys !== undefined && notificationIdentityPublicKeys !== null && notificationIdentityPublicKeys !== undefined ) { return; } await olm.init(); const identityAccount = new olm.Account(); identityAccount.create(); const { ed25519: identityED25519, curve25519: identityCurve25519 } = JSON.parse(identityAccount.identity_keys()); dispatch({ type: setPrimaryIdentityKeys, payload: { ed25519: identityED25519, curve25519: identityCurve25519 }, }); + const identityAccountPicklingKey = uuid.v4(); + const pickledIdentityAccount = identityAccount.pickle( + identityAccountPicklingKey, + ); + + dispatch({ + type: setPickledPrimaryAccount, + payload: { + picklingKey: identityAccountPicklingKey, + pickledAccount: pickledIdentityAccount, + }, + }); + const notificationAccount = new olm.Account(); notificationAccount.create(); const { ed25519: notificationED25519, curve25519: notificationCurve25519, } = JSON.parse(notificationAccount.identity_keys()); dispatch({ type: setNotificationIdentityKeys, payload: { ed25519: notificationED25519, curve25519: notificationCurve25519, }, }); + + const notificationAccountPicklingKey = uuid.v4(); + const pickledNotificationAccount = notificationAccount.pickle( + notificationAccountPicklingKey, + ); + + dispatch({ + type: setPickledNotificationAccount, + payload: { + picklingKey: notificationAccountPicklingKey, + pickledAccount: pickledNotificationAccount, + }, + }); })(); }, [dispatch, notificationIdentityPublicKeys, primaryIdentityPublicKeys]); const [siweAuthFlowSelected, setSIWEAuthFlowSelected] = React.useState(false); const onSIWEButtonClick = React.useCallback(() => { setSIWEAuthFlowSelected(true); openConnectModal && openConnectModal(); }, [openConnectModal]); const cancelSIWEAuthFlow = React.useCallback(() => { setSIWEAuthFlowSelected(false); }, []); if (siweAuthFlowSelected && signer) { return (
); } return (
); } export default LoginForm; diff --git a/web/package.json b/web/package.json index 037ab09b0..49a93c5b1 100644 --- a/web/package.json +++ b/web/package.json @@ -1,99 +1,100 @@ { "name": "web", "version": "0.0.1", "type": "module", "private": true, "license": "BSD-3-Clause", "scripts": { "clean": "rm -rf dist/ && rm -rf node_modules/", "dev": "yarn concurrently --names=\"NODESSR,BROWSER\" -c \"bgBlue.bold,bgMagenta.bold\" \"yarn webpack --config webpack.config.cjs --config-name=server --watch\" \"yarn webpack-dev-server --config webpack.config.cjs --config-name=browser\"", "prod": "yarn webpack --config webpack.config.cjs --env prod --progress", "test": "jest" }, "devDependencies": { "@babel/core": "^7.13.14", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-object-rest-spread": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-transform-react-constant-elements": "^7.13.13", "@babel/plugin-transform-runtime": "^7.13.10", "@babel/preset-env": "^7.13.12", "@babel/preset-flow": "^7.13.13", "@babel/preset-react": "^7.13.13", "assets-webpack-plugin": "^7.1.1", "babel-jest": "^26.6.3", "babel-plugin-transform-remove-console": "^6.9.4", "concurrently": "^5.3.0", "copy-webpack-plugin": "^11.0.0", "flow-bin": "^0.182.0", "flow-typed": "^3.2.1", "identity-obj-proxy": "^3.0.0", "jest": "^26.6.3", "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.11.1" }, "dependencies": { "@babel/runtime": "^7.13.10", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", "@fortawesome/fontawesome-svg-core": "1.2.25", "@fortawesome/free-regular-svg-icons": "5.11.2", "@fortawesome/free-solid-svg-icons": "5.11.2", "@fortawesome/react-fontawesome": "0.1.5", "@rainbow-me/rainbowkit": "^0.8.1", "basscss": "8.0.2", "classnames": "^2.2.5", "core-js": "^3.6.5", "dateformat": "^3.0.3", "detect-browser": "^4.0.4", "emoji-mart": "^5.5.2", "exif-js": "^2.3.0", "history": "^4.6.3", "ethers": "^5.7.2", "invariant": "^2.2.4", "is-svg": "^4.3.0", "isomorphic-fetch": "^3.0.0", "lib": "0.0.1", "lodash": "^4.17.21", "@matrix-org/olm": "3.2.4", "react": "18.1.0", "react-circular-progressbar": "^2.0.2", "react-color": "^2.13.0", "react-dnd": "^11.1.3", "react-dnd-html5-backend": "^11.1.3", "react-dom": "18.1.0", "react-feather": "^2.0.3", "react-icomoon": "^2.5.7", "react-icons": "^4.4.0", "react-redux": "^7.1.1", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-switch": "^7.0.0", "react-timeago": "^5.2.0", "redux": "^4.0.4", "redux-devtools-extension": "^2.13.2", "redux-persist": "^6.0.0", "redux-thunk": "^2.2.0", "reselect": "^4.0.0", "simple-markdown": "^0.7.2", "siwe": "^1.1.6", "tinycolor2": "^1.4.1", + "uuid": "^3.3.3", "visibilityjs": "^2.0.2", "wagmi": "^0.8.10" }, "jest": { "roots": [ "/utils" ], "transform": { "\\.js$": "babel-jest" }, "transformIgnorePatterns": [ "/node_modules/(?!@babel/runtime)" ], "moduleNameMapper": { "\\.(css)$": "identity-obj-proxy" } } } diff --git a/web/redux/crypto-store-reducer.js b/web/redux/crypto-store-reducer.js index 4d0430dba..8bd53dd40 100644 --- a/web/redux/crypto-store-reducer.js +++ b/web/redux/crypto-store-reducer.js @@ -1,46 +1,60 @@ // @flow import { logOutActionTypes, deleteAccountActionTypes, } from 'lib/actions/user-actions.js'; import type { CryptoStore } from 'lib/types/crypto-types.js'; import { setNewSessionActionType } from 'lib/utils/action-utils.js'; import type { Action } from './redux-setup.js'; const setPrimaryIdentityKeys = 'SET_PRIMARY_IDENTITY_KEYS'; const setNotificationIdentityKeys = 'SET_NOTIFICATION_IDENTITY_KEYS'; +const setPickledPrimaryAccount = 'SET_PICKLED_PRIMARY_ACCOUNT'; +const setPickledNotificationAccount = 'SET_PICKLED_NOTIFICATION_ACCOUNT'; function reduceCryptoStore(state: CryptoStore, action: Action): CryptoStore { if (action.type === setPrimaryIdentityKeys) { return { ...state, primaryIdentityKeys: action.payload, }; } else if (action.type === setNotificationIdentityKeys) { return { ...state, notificationIdentityKeys: action.payload, }; + } else if (action.type === setPickledPrimaryAccount) { + return { + ...state, + primaryAccount: action.payload, + }; + } else if (action.type === setPickledNotificationAccount) { + return { + ...state, + notificationAccount: action.payload, + }; } else if ( action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success || (action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated) ) { return { primaryAccount: null, primaryIdentityKeys: null, notificationAccount: null, notificationIdentityKeys: null, }; } return state; } export { setPrimaryIdentityKeys, setNotificationIdentityKeys, + setPickledPrimaryAccount, + setPickledNotificationAccount, reduceCryptoStore, }; diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js index d56380638..aa7b7be12 100644 --- a/web/redux/redux-setup.js +++ b/web/redux/redux-setup.js @@ -1,305 +1,315 @@ // @flow import invariant from 'invariant'; import type { PersistState } from 'redux-persist/es/types.js'; import { logOutActionTypes, deleteAccountActionTypes, } from 'lib/actions/user-actions.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { nonThreadCalendarFilters } from 'lib/selectors/calendar-filter-selectors.js'; import { mostRecentlyReadThreadSelector } from 'lib/selectors/thread-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { invalidSessionDowngrade } from 'lib/shared/account-utils.js'; import type { Shape } from 'lib/types/core.js'; -import type { CryptoStore, OLMIdentityKeys } from 'lib/types/crypto-types.js'; +import type { + CryptoStore, + OLMIdentityKeys, + PickledOLMAccount, +} from 'lib/types/crypto-types.js'; import type { DraftStore } from 'lib/types/draft-types.js'; import type { EnabledApps } from 'lib/types/enabled-apps.js'; import type { EntryStore } from 'lib/types/entry-types.js'; import { type CalendarFilter, calendarThreadFilterTypes, } from 'lib/types/filter-types.js'; import type { LifecycleState } from 'lib/types/lifecycle-state-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { MessageStore } from 'lib/types/message-types.js'; import type { UserPolicies } from 'lib/types/policy-types.js'; import type { BaseAction } from 'lib/types/redux-types.js'; import type { ReportStore } from 'lib/types/report-types.js'; import type { ConnectionInfo } from 'lib/types/socket-types.js'; import type { ThreadStore } from 'lib/types/thread-types.js'; import type { CurrentUserInfo, UserStore } from 'lib/types/user-types.js'; import { setNewSessionActionType } from 'lib/utils/action-utils.js'; import { updateWindowActiveActionType, setDeviceIDActionType, updateNavInfoActionType, updateWindowDimensionsActionType, updateCalendarCommunityFilter, clearCalendarCommunityFilter, } from './action-types.js'; import { reduceCryptoStore, setPrimaryIdentityKeys, setNotificationIdentityKeys, + setPickledNotificationAccount, + setPickledPrimaryAccount, } from './crypto-store-reducer.js'; import { reduceDeviceID } from './device-id-reducer.js'; import reduceNavInfo from './nav-reducer.js'; import { getVisibility } from './visibility.js'; import { filterThreadIDsBelongingToCommunity } from '../selectors/calendar-selectors.js'; import { activeThreadSelector } from '../selectors/nav-selectors.js'; import { type NavInfo } from '../types/nav-types.js'; export type WindowDimensions = { width: number, height: number }; export type AppState = { navInfo: NavInfo, deviceID: ?string, currentUserInfo: ?CurrentUserInfo, draftStore: DraftStore, sessionID: ?string, entryStore: EntryStore, threadStore: ThreadStore, userStore: UserStore, messageStore: MessageStore, updatesCurrentAsOf: number, loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, calendarFilters: $ReadOnlyArray, calendarPickedCommunityID: ?string, urlPrefix: string, windowDimensions: WindowDimensions, cookie?: void, deviceToken?: void, baseHref: string, connection: ConnectionInfo, watchedThreadIDs: $ReadOnlyArray, lifecycleState: LifecycleState, enabledApps: EnabledApps, reportStore: ReportStore, nextLocalID: number, dataLoaded: boolean, windowActive: boolean, userPolicies: UserPolicies, cryptoStore: CryptoStore, _persist: ?PersistState, }; export type Action = | BaseAction | { type: 'UPDATE_NAV_INFO', payload: Shape } | { type: 'UPDATE_WINDOW_DIMENSIONS', payload: WindowDimensions, } | { type: 'UPDATE_WINDOW_ACTIVE', payload: boolean, } | { type: 'SET_DEVICE_ID', payload: string, } | { +type: 'SET_PRIMARY_IDENTITY_KEYS', payload: ?OLMIdentityKeys } | { +type: 'SET_NOTIFICATION_IDENTITY_KEYS', payload: ?OLMIdentityKeys } + | { +type: 'SET_PICKLED_PRIMARY_ACCOUNT', payload: ?PickledOLMAccount } + | { +type: 'SET_PICKLED_NOTIFICATION_ACCOUNT', payload: ?PickledOLMAccount } | { +type: 'UPDATE_CALENDAR_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CALENDAR_COMMUNITY_FILTER', +payload: void, }; export function reducer(oldState: AppState | void, action: Action): AppState { invariant(oldState, 'should be set'); let state = oldState; if (action.type === updateWindowDimensionsActionType) { return validateState(oldState, { ...state, windowDimensions: action.payload, }); } else if (action.type === updateWindowActiveActionType) { return validateState(oldState, { ...state, windowActive: action.payload, }); } else if (action.type === updateCalendarCommunityFilter) { const nonThreadFilters = nonThreadCalendarFilters(state.calendarFilters); const threadIDs = Array.from( filterThreadIDsBelongingToCommunity( action.payload, state.threadStore.threadInfos, ), ); return { ...state, calendarFilters: [ ...nonThreadFilters, { type: calendarThreadFilterTypes.THREAD_LIST, threadIDs, }, ], calendarPickedCommunityID: action.payload, }; } else if (action.type === clearCalendarCommunityFilter) { const nonThreadFilters = nonThreadCalendarFilters(state.calendarFilters); return { ...state, calendarFilters: nonThreadFilters, calendarPickedCommunityID: null, }; } else if (action.type === setNewSessionActionType) { if ( invalidSessionDowngrade( oldState, action.payload.sessionChange.currentUserInfo, action.payload.preRequestUserState, ) ) { return oldState; } state = { ...state, sessionID: action.payload.sessionChange.sessionID, }; } else if ( (action.type === logOutActionTypes.success && invalidSessionDowngrade( oldState, action.payload.currentUserInfo, action.payload.preRequestUserState, )) || (action.type === deleteAccountActionTypes.success && invalidSessionDowngrade( oldState, action.payload.currentUserInfo, action.payload.preRequestUserState, )) ) { return oldState; } if ( action.type !== updateNavInfoActionType && action.type !== setDeviceIDActionType && action.type !== setPrimaryIdentityKeys && - action.type !== setNotificationIdentityKeys + action.type !== setNotificationIdentityKeys && + action.type !== setPickledPrimaryAccount && + action.type !== setPickledNotificationAccount ) { state = baseReducer(state, action).state; } state = { ...state, navInfo: reduceNavInfo( state.navInfo, action, state.threadStore.threadInfos, ), deviceID: reduceDeviceID(state.deviceID, action), cryptoStore: reduceCryptoStore(state.cryptoStore, action), }; return validateState(oldState, state); } function validateState(oldState: AppState, state: AppState): AppState { if ( (state.navInfo.activeChatThreadID && !state.navInfo.pendingThread && !state.threadStore.threadInfos[state.navInfo.activeChatThreadID]) || (!state.navInfo.activeChatThreadID && isLoggedIn(state)) ) { // Makes sure the active thread always exists state = { ...state, navInfo: { ...state.navInfo, activeChatThreadID: mostRecentlyReadThreadSelector(state), }, }; } const activeThread = activeThreadSelector(state); if ( activeThread && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread && getVisibility().hidden() ) { console.warn( `thread ${activeThread} is active and unread, ` + 'but visibilityjs reports the window is not visible', ); } if ( activeThread && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread && typeof document !== 'undefined' && document && 'hasFocus' in document && !document.hasFocus() ) { console.warn( `thread ${activeThread} is active and unread, ` + 'but document.hasFocus() is false', ); } if ( activeThread && !getVisibility().hidden() && typeof document !== 'undefined' && document && 'hasFocus' in document && document.hasFocus() && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread ) { // Makes sure a currently focused thread is never unread state = { ...state, threadStore: { ...state.threadStore, threadInfos: { ...state.threadStore.threadInfos, [activeThread]: { ...state.threadStore.threadInfos[activeThread], currentUser: { ...state.threadStore.threadInfos[activeThread].currentUser, unread: false, }, }, }, }, }; } const oldActiveThread = activeThreadSelector(oldState); if ( activeThread && oldActiveThread !== activeThread && state.messageStore.threads[activeThread] ) { // Update messageStore.threads[activeThread].lastNavigatedTo state = { ...state, messageStore: { ...state.messageStore, threads: { ...state.messageStore.threads, [activeThread]: { ...state.messageStore.threads[activeThread], lastNavigatedTo: Date.now(), }, }, }, }; } return state; }