Skip to content

Commit

Permalink
Desktop: Change Joplin Cloud login process to allow MFA via browser (#…
Browse files Browse the repository at this point in the history
…9445)

Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
  • Loading branch information
pedr and laurent22 authored Mar 9, 2024
1 parent 75cb639 commit 4d8fcff
Show file tree
Hide file tree
Showing 15 changed files with 347 additions and 124 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ packages/app-desktop/gui/IconButton.js
packages/app-desktop/gui/ImportScreen.js
packages/app-desktop/gui/ItemList.js
packages/app-desktop/gui/JoplinCloudConfigScreen.js
packages/app-desktop/gui/JoplinCloudLoginScreen.js
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
packages/app-desktop/gui/KeymapConfig/styles/index.js
Expand Down Expand Up @@ -884,6 +885,7 @@ packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/Module.test.js
packages/lib/services/interop/Module.js
packages/lib/services/interop/types.js
packages/lib/services/joplinCloudUtils.js
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
packages/lib/services/keychain/KeychainService.js
packages/lib/services/keychain/KeychainServiceDriver.dummy.js
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ packages/app-desktop/gui/IconButton.js
packages/app-desktop/gui/ImportScreen.js
packages/app-desktop/gui/ItemList.js
packages/app-desktop/gui/JoplinCloudConfigScreen.js
packages/app-desktop/gui/JoplinCloudLoginScreen.js
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
packages/app-desktop/gui/KeymapConfig/styles/index.js
Expand Down Expand Up @@ -864,6 +865,7 @@ packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/Module.test.js
packages/lib/services/interop/Module.js
packages/lib/services/interop/types.js
packages/lib/services/joplinCloudUtils.js
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
packages/lib/services/keychain/KeychainService.js
packages/lib/services/keychain/KeychainServiceDriver.dummy.js
Expand Down
9 changes: 9 additions & 0 deletions packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ class ConfigScreenComponent extends React.Component<any, any> {
}

private async checkSyncConfig_() {
if (this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud')) {
const isAuthenticated = await reg.syncTarget().isAuthenticated();
if (!isAuthenticated) {
return this.props.dispatch({
type: 'NAV_GO',
routeName: 'JoplinCloudLogin',
});
}
}
await shared.checkSyncConfig(this, this.state.settings);
}

Expand Down
32 changes: 32 additions & 0 deletions packages/app-desktop/gui/JoplinCloudLoginScreen.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.login-page {

display: flex;
flex-direction: column;
height: 100%;
background-color: var(--joplin-background-color);
color: var(--joplin-color);

> .page-container {
height: calc(100% - (var(--joplin-margin) * 2));
flex: 1;
padding: var(--joplin-config-screen-padding);

> .text {
font-size: var(--joplin-font-size);
}

> .buttons-container {
margin-bottom: calc(var(--joplin-font-size) * 2);
display: flex;

> button:first-child {
margin-right: calc(var(--joplin-font-size) * 2);
}
}

> .bold {
font-weight: bold;
font-size: calc(var(--joplin-font-size) * 1.3);
}
}
}
115 changes: 115 additions & 0 deletions packages/app-desktop/gui/JoplinCloudLoginScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useEffect, useMemo, useReducer, useState } from 'react';
import ButtonBar from './ConfigScreen/ButtonBar';
import { _ } from '@joplin/lib/locale';
import { clipboard } from 'electron';
import Button, { ButtonLevel } from './Button/Button';
const bridge = require('@electron/remote').require('./bridge').default;
import { uuidgen } from '@joplin/lib/uuid';
import { Dispatch } from 'redux';
import { reducer, defaultState, generateApplicationConfirmUrl, checkIfLoginWasSuccessful } from '@joplin/lib/services/joplinCloudUtils';
import { AppState } from '../app.reducer';
import Logger from '@joplin/utils/Logger';

const logger = Logger.create('JoplinCloudLoginScreen');
const { connect } = require('react-redux');

interface Props {
dispatch: Dispatch;
joplinCloudWebsite: string;
joplinCloudApi: string;
}

const JoplinCloudScreenComponent = (props: Props) => {

const confirmUrl = (applicationAuthId: string) => `${props.joplinCloudWebsite}/applications/${applicationAuthId}/confirm`;
const applicationAuthUrl = (applicationAuthId: string) => `${props.joplinCloudApi}/api/application_auth/${applicationAuthId}`;

const [intervalIdentifier, setIntervalIdentifier] = useState(undefined);
const [state, dispatch] = useReducer(reducer, defaultState);

const applicatioAuthId = useMemo(() => uuidgen(), []);

const periodicallyCheckForCredentials = () => {
if (intervalIdentifier) return;

const interval = setInterval(async () => {
try {
const response = await checkIfLoginWasSuccessful(applicationAuthUrl(applicatioAuthId));
if (response && response.success) {
dispatch({ type: 'COMPLETED' });
clearInterval(interval);
}
} catch (error) {
logger.error(error);
dispatch({ type: 'ERROR', payload: error.message });
clearInterval(interval);
}
}, 2 * 1000);

setIntervalIdentifier(interval);
};

const onButtonUsed = () => {
if (state.next === 'LINK_USED') {
dispatch({ type: 'LINK_USED' });
}
periodicallyCheckForCredentials();
};

const onAuthorizeClicked = async () => {
const url = await generateApplicationConfirmUrl(confirmUrl(applicatioAuthId));
bridge().openExternal(url);
onButtonUsed();
};

const onCopyToClipboardClicked = async () => {
const url = await generateApplicationConfirmUrl(confirmUrl(applicatioAuthId));
clipboard.writeText(url);
onButtonUsed();
};

useEffect(() => {
return () => {
clearInterval(intervalIdentifier);
};
}, [intervalIdentifier]);

return (
<div className="login-page">
<div className="page-container">
<p className="text">{_('To allow Joplin to synchronise with Joplin Cloud, open this URL in your browser to authorise the application:')}</p>
<div className="buttons-container">
<Button
onClick={onAuthorizeClicked}
title={_('Authorise')}
iconName='fa fa-external-link-alt'
level={ButtonLevel.Primary}
/>
<Button
onClick={onCopyToClipboardClicked}
title={_('Copy link to website')}
iconName='fa fa-clone'
level={ButtonLevel.Secondary}
/>

</div>
<p className={state.className}>{state.message()}
{state.active === 'ERROR' ? (
<span className={state.className}>{state.errorMessage}</span>
) : null}
</p>
{state.active === 'LINK_USED' ? <div id="loading-animation" /> : null}
</div>
<ButtonBar onCancelClick={() => props.dispatch({ type: 'NAV_BACK' })} />
</div>
);
};

const mapStateToProps = (state: AppState) => {
return {
joplinCloudWebsite: state.settings['sync.10.website'],
joplinCloudApi: state.settings['sync.10.path'],
};
};

export default connect(mapStateToProps)(JoplinCloudScreenComponent);
2 changes: 2 additions & 0 deletions packages/app-desktop/gui/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import ImportScreen from './ImportScreen';
const { ResourceScreen } = require('./ResourceScreen.js');
import Navigator from './Navigator';
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components');
const bridge = require('@electron/remote').require('./bridge').default;

Expand Down Expand Up @@ -224,6 +225,7 @@ class RootComponent extends React.Component<Props, any> {
Main: { screen: MainScreen },
OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') },
DropboxLogin: { screen: DropboxLoginScreen, title: () => _('Dropbox Login') },
JoplinCloudLogin: { screen: JoplinCloudLoginScreen, title: () => _('Joplin Cloud Login') },
Import: { screen: ImportScreen, title: () => _('Import') },
Config: { screen: ConfigScreen, title: () => _('Options') },
Resources: { screen: ResourceScreen, title: () => _('Note attachments') },
Expand Down
Loading

0 comments on commit 4d8fcff

Please sign in to comment.