Skip to content

Commit

Permalink
Refactor: Extract authentication and login screen from main app (#2066)
Browse files Browse the repository at this point in the history
Explored in 5f1f65d
Explored in e3c660e

This work exists in preparation for further and deeper work to decouple Simperium in the app data flows and to finish the internal state refactors.

The goal here is to initialize Simperium only after having proper authentication in order to allow us to move Simperium into Redux middleware. The further goal is to remove the race condition that exists in many places between making an edit or clicking a button, making a network call to Simperium (or not), updating indexedDB, and rerendering the app.

App boot is now handled on its own and centralizes the token-loading process. Upon logout it force-reloads the browser window to clear out app state. This is necessary due to the ways that things like client and app-state initialize their variables in the module global scope. We can't reload the app state once the module has been imported the first time. Reloading the page completely resets this. There is a flash of a white screen when logging out.

The auth component of app state has been correspondingly removed because the app will not load without an authorization. A new action LOGOUT has been created in order to trigger a logout in the app, driven from the Simperium middleware.
  • Loading branch information
dmsnell authored May 12, 2020
1 parent 526025d commit 7e65566
Show file tree
Hide file tree
Showing 26 changed files with 561 additions and 497 deletions.
80 changes: 14 additions & 66 deletions lib/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ import { connect } from 'react-redux';
import 'focus-visible/dist/focus-visible.js';
import appState from './flux/app-state';
import { loadTags } from './state/domain/tags';
import reduxActions from './state/actions';
import selectors from './state/selectors';
import browserShell from './browser-shell';
import NoteInfo from './note-info';
import NavigationBar from './navigation-bar';
import AppLayout from './app-layout';
import Auth from './auth';
import DevBadge from './components/dev-badge';
import DialogRenderer from './dialog-renderer';
import { getIpcRenderer } from './utils/electron';
Expand Down Expand Up @@ -42,11 +39,6 @@ export type OwnProps = {
noteBucket: object;
};

export type StateProps = S.State & {
authIsPending: boolean;
isAuthorized: boolean;
};

export type DispatchProps = {
createNote: () => any;
closeNote: () => any;
Expand All @@ -56,13 +48,9 @@ export type DispatchProps = {
trashNote: (previousIndex: number) => any;
};

export type Props = OwnProps & StateProps & DispatchProps;
export type Props = OwnProps & DispatchProps;

const mapStateToProps: S.MapState<StateProps> = (state) => ({
...state,
authIsPending: selectors.auth.authIsPending(state),
isAuthorized: selectors.auth.isAuthorized(state),
});
const mapStateToProps: S.MapState<S.State> = (state) => state;

const mapDispatchToProps: S.MapDispatch<
DispatchProps,
Expand Down Expand Up @@ -106,9 +94,7 @@ const mapDispatchToProps: S.MapDispatch<
toggleSortTagsAlpha: thenReloadTags(settingsActions.toggleSortTagsAlpha),
createNote: () => dispatch(createNote()),
openTagList: () => dispatch(toggleNavigation()),
resetAuth: () => dispatch(reduxActions.auth.reset()),
selectNote: (note: T.NoteEntity) => dispatch(actions.ui.selectNote(note)),
setAuthorized: () => dispatch(reduxActions.auth.setAuthorized()),
focusSearchField: () => dispatch(actions.ui.focusSearchField()),
setSimperiumConnectionStatus: (connected) =>
dispatch(toggleSimperiumConnectionStatus(connected)),
Expand All @@ -129,31 +115,17 @@ export const App = connect(
static propTypes = {
actions: PropTypes.object.isRequired,
appState: PropTypes.object.isRequired,
authIsPending: PropTypes.bool.isRequired,
authorizeUserWithToken: PropTypes.func.isRequired,
client: PropTypes.object.isRequired,
isAuthorized: PropTypes.bool.isRequired,
isDevConfig: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
loadTags: PropTypes.func.isRequired,
onAuthenticate: PropTypes.func.isRequired,
onCreateUser: PropTypes.func.isRequired,
openTagList: PropTypes.func.isRequired,
onSignOut: PropTypes.func.isRequired,
settings: PropTypes.object.isRequired,
preferencesBucket: PropTypes.object.isRequired,
resetAuth: PropTypes.func.isRequired,
setAuthorized: PropTypes.func.isRequired,
systemTheme: PropTypes.string.isRequired,
tagBucket: PropTypes.object.isRequired,
};

static defaultProps = {
onAuthenticate: () => {},
onCreateUser: () => {},
onSignOut: () => {},
};

UNSAFE_componentWillMount() {
if (isElectron) {
this.initializeElectron();
Expand Down Expand Up @@ -341,21 +313,9 @@ export const App = connect(

onAuthChanged = () => {
const {
actions,
appState: { accountName },
client,
resetAuth,
setAuthorized,
} = this.props;

actions.authChanged();

if (!client.isAuthorized()) {
this.props.closeNote();
return resetAuth();
}

setAuthorized();
analytics.initialize(accountName);
this.onLoadPreferences();

Expand Down Expand Up @@ -487,8 +447,6 @@ export const App = connect(
render() {
const {
appState: state,
authIsPending,
isAuthorized,
isDevConfig,
noteBucket,
preferencesBucket,
Expand All @@ -515,29 +473,19 @@ export const App = connect(
return (
<div className={appClasses}>
{isDevConfig && <DevBadge />}
{isAuthorized ? (
<div className={mainClasses}>
{showNavigation && <NavigationBar />}
<AppLayout
isFocusMode={settings.focusModeEnabled}
isNavigationOpen={showNavigation}
isNoteInfoOpen={showNoteInfo}
isSmallScreen={isSmallScreen}
noteBucket={noteBucket}
onUpdateContent={this.onUpdateContent}
syncNote={this.syncNote}
/>
{showNoteInfo && <NoteInfo noteBucket={noteBucket} />}
</div>
) : (
<Auth
authPending={authIsPending}
isAuthenticated={isAuthorized}
onAuthenticate={this.props.onAuthenticate}
onCreateUser={this.props.onCreateUser}
authorizeUserWithToken={this.props.authorizeUserWithToken}
<div className={mainClasses}>
{showNavigation && <NavigationBar />}
<AppLayout
isFocusMode={settings.focusModeEnabled}
isNavigationOpen={showNavigation}
isNoteInfoOpen={showNoteInfo}
isSmallScreen={isSmallScreen}
noteBucket={noteBucket}
onUpdateContent={this.onUpdateContent}
syncNote={this.syncNote}
/>
)}
{showNoteInfo && <NoteInfo noteBucket={noteBucket} />}
</div>
<DialogRenderer
appProps={this.props}
buckets={{ noteBucket, preferencesBucket, tagBucket }}
Expand Down
67 changes: 30 additions & 37 deletions lib/auth/index.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import classNames from 'classnames';
import cryptoRandomString from '../utils/crypto-random-string';
import { get } from 'lodash';
import getConfig from '../../get-config';
import SimplenoteLogo from '../icons/simplenote';
import Spinner from '../components/spinner';

import { hasInvalidCredentials, hasLoginError } from '../state/auth/selectors';
import { isElectron, isMac } from '../utils/platform';
import { reset } from '../state/auth/actions';
import { setWPToken } from '../state/settings/actions';
import { viewExternalUrl } from '../utils/url-utils';

export class Auth extends Component {
static propTypes = {
authorizeUserWithToken: PropTypes.func.isRequired,
authPending: PropTypes.bool,
hasInvalidCredentials: PropTypes.bool,
hasLoginError: PropTypes.bool,
isAuthenticated: PropTypes.bool,
onAuthenticate: PropTypes.func.isRequired,
onCreateUser: PropTypes.func.isRequired,
resetErrors: PropTypes.func.isRequired,
saveWPToken: PropTypes.func.isRequired,
};
type OwnProps = {
authPending: boolean;
hasInvalidCredentials: boolean;
hasLoginError: boolean;
login: (username: string, password: string) => any;
signup: (username: string, password: string) => any;
tokenLogin: (username: string, token: string) => any;
resetErrors: () => any;
};

type Props = OwnProps;

export class Auth extends Component<Props> {
state = {
isCreatingAccount: false,
passwordErrorMessage: null,
onLine: window.navigator.onLine,
};

componentDidMount() {
if (this.usernameInput) {
this.usernameInput.focus();
}

window.addEventListener('online', this.setConnectivity, false);
window.addEventListener('offline', this.setConnectivity, false);
}

componentWillUnmount() {
window.removeEventListener('online', this.setConnectivity, false);
window.removeEventListener('offline', this.setConnectivity, false);
}

setConnectivity = () => this.setState({ onLine: window.navigator.onLine });

render() {
// Don't render this component when running on the web
const config = getConfig();
Expand Down Expand Up @@ -67,7 +73,9 @@ export class Auth extends Component {
<SimplenoteLogo />
<form className="login__form" onSubmit={this.onLogin}>
<h1>{buttonLabel}</h1>

{!this.state.onLine && (
<p className="login__auth-message is-error">Offline</p>
)}
{this.props.hasInvalidCredentials && (
<p
className="login__auth-message is-error"
Expand Down Expand Up @@ -121,6 +129,7 @@ export class Auth extends Component {
<button
id="login__login-button"
className={submitClasses}
disabled={!this.state.onLine}
onClick={isCreatingAccount ? this.onSignUp : this.onLogin}
type="submit"
>
Expand Down Expand Up @@ -207,7 +216,7 @@ export class Auth extends Component {
return;
}

this.props.onAuthenticate(username, password);
this.props.login(username, password);
};

onWPLogin = () => {
Expand Down Expand Up @@ -283,11 +292,7 @@ export class Auth extends Component {
return;
}

const { authorizeUserWithToken, saveWPToken } = this.props;
authorizeUserWithToken(userEmail, simpToken);
if (wpccToken) {
saveWPToken(wpccToken);
}
this.props.tokenLogin(userEmail, simpToken);
}
);
};
Expand Down Expand Up @@ -331,7 +336,7 @@ export class Auth extends Component {
return;
}

this.props.onCreateUser(username, password);
this.props.signup(username, password, true);
this.setState({ passwordErrorMessage: null });
};

Expand All @@ -344,15 +349,3 @@ export class Auth extends Component {
this.setState({ isCreatingAccount: !this.state.isCreatingAccount });
};
}

const mapDispatchToProps = (dispatch) => ({
resetErrors: () => dispatch(reset()),
saveWPToken: (token) => dispatch(setWPToken(token)),
});

const mapStateToProps = (state) => ({
hasInvalidCredentials: hasInvalidCredentials(state),
hasLoginError: hasLoginError(state),
});

export default connect(mapStateToProps, mapDispatchToProps)(Auth);
97 changes: 97 additions & 0 deletions lib/boot-with-auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
if (__TEST__) {
window.testEvents = [];
}

import 'core-js/stable';
import 'regenerator-runtime/runtime';
import 'unorm';

import React from 'react';
import App from './app';
import Modal from 'react-modal';
import Debug from 'debug';
import { initClient } from './client';
import getConfig from '../get-config';
import { makeStore } from './state';
import actions from './state/actions';
import { initSimperium } from './state/simperium/middleware';
import { render } from 'react-dom';
import { Provider } from 'react-redux';

import '../scss/style.scss';

import isDevConfig from './utils/is-dev-config';
import { normalizeForSorting } from './utils/note-utils';

import * as T from './types';

const config = getConfig();
const appID = config.app_id;

export const bootWithToken = (
logout: () => any,
token: string,
username: string | null,
createWelcomeNote: boolean
) => {
const client = initClient({
appID,
token,
bucketConfig: {
note: {
beforeIndex: function (note: T.NoteEntity) {
var content = (note.data && note.data.content) || '';

return {
...note,
contentKey: normalizeForSorting(content),
};
},
configure: function (objectStore) {
objectStore.createIndex('modificationDate', 'data.modificationDate');
objectStore.createIndex('creationDate', 'data.creationDate');
objectStore.createIndex('alphabetical', 'contentKey');
},
},
preferences: function (objectStore) {
console.log('Configure preferences', objectStore); // eslint-disable-line no-console
},
tag: function (objectStore) {
console.log('Configure tag', objectStore); // eslint-disable-line no-console
},
},
database: 'simplenote',
version: 42,
});

const debug = Debug('client');
const l = (msg: string) => (...args: unknown[]) => debug(msg, ...args);

client
.on('connect', l('Connected'))
.on('disconnect', l('Not connected'))
.on('message', l('<='))
.on('send', l('=>'))
.on('unauthorized', l('Not authorized'));

Modal.setAppElement('#root');

const store = makeStore(
initSimperium(logout, token, username, createWelcomeNote, client)
);

store.dispatch(actions.settings.setAccountName(username));

render(
<Provider store={store}>
<App
client={client}
noteBucket={client.bucket('note')}
preferencesBucket={client.bucket('preferences')}
tagBucket={client.bucket('tag')}
isDevConfig={isDevConfig(config?.development)}
/>
</Provider>,
document.getElementById('root')
);
};
Loading

0 comments on commit 7e65566

Please sign in to comment.