mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +02:00
Desktop: Change Joplin Cloud login process to allow MFA via browser (#9445)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
This commit is contained in:
parent
75cb639ed2
commit
4d8fcff6d5
@ -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
|
||||
@ -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
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
32
packages/app-desktop/gui/JoplinCloudLoginScreen.scss
Normal file
32
packages/app-desktop/gui/JoplinCloudLoginScreen.scss
Normal file
@ -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
packages/app-desktop/gui/JoplinCloudLoginScreen.tsx
Normal file
115
packages/app-desktop/gui/JoplinCloudLoginScreen.tsx
Normal file
@ -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);
|
@ -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;
|
||||
|
||||
@ -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') },
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import DialogButtonRow from '../DialogButtonRow';
|
||||
import Dialog from '../Dialog';
|
||||
@ -9,10 +9,7 @@ import SyncTargetRegistry, { SyncTargetInfo } from '@joplin/lib/SyncTargetRegist
|
||||
import useElementSize from '@joplin/lib/hooks/useElementSize';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import bridge from '../../services/bridge';
|
||||
import StyledInput from '../style/StyledInput';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
|
||||
import StyledLink from '../style/StyledLink';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@ -32,10 +29,6 @@ const SyncTargetDescription = styled.div<{ height: number }>`
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
const CreateAccountLink = styled(StyledLink)`
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
const ContentRoot = styled.div`
|
||||
background-color: ${props => props.theme.backgroundColor3};
|
||||
padding: 1em;
|
||||
@ -70,7 +63,7 @@ const SyncTargetLogo = styled.img`
|
||||
margin-right: 0.4em;
|
||||
`;
|
||||
|
||||
const SyncTargetBox = styled.div<{ faded: boolean }>`
|
||||
const SyncTargetBox = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
@ -82,7 +75,7 @@ const SyncTargetBox = styled.div<{ faded: boolean }>`
|
||||
padding: 2em 2.2em 2em 2.2em;
|
||||
margin-right: 1em;
|
||||
max-width: 400px;
|
||||
opacity: ${props => props.faded ? 0.5 : 1};
|
||||
opacity: 1;
|
||||
`;
|
||||
|
||||
const FeatureList = styled.div`
|
||||
@ -117,16 +110,6 @@ const SelectButton = styled(Button)`
|
||||
font-size: 1em;
|
||||
`;
|
||||
|
||||
const JoplinCloudLoginForm = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const FormLabel = styled.label`
|
||||
font-weight: bold;
|
||||
margin: 1em 0 0.6em 0;
|
||||
`;
|
||||
|
||||
const SlowSyncWarning = styled.div`
|
||||
margin-top: 1em;
|
||||
opacity: 0.8;
|
||||
@ -152,12 +135,10 @@ const logosImageNames: Record<string, string> = {
|
||||
'onedrive': 'SyncTarget_OneDrive.svg',
|
||||
};
|
||||
|
||||
type SyncTargetInfoName = 'dropbox' | 'onedrive' | 'joplinCloud';
|
||||
|
||||
export default function(props: Props) {
|
||||
const [showJoplinCloudForm, setShowJoplinCloudForm] = useState(false);
|
||||
const joplinCloudDescriptionRef = useRef(null);
|
||||
const [joplinCloudEmail, setJoplinCloudEmail] = useState('');
|
||||
const [joplinCloudPassword, setJoplinCloudPassword] = useState('');
|
||||
const [joplinCloudLoginInProgress, setJoplinCloudLoginInProgress] = useState(false);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
function closeDialog(dispatch: Function) {
|
||||
@ -192,93 +173,33 @@ export default function(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const onJoplinCloudEmailChange = useCallback((event: any) => {
|
||||
setJoplinCloudEmail(event.target.value);
|
||||
}, []);
|
||||
const onSelectButtonClick = useCallback(async (name: SyncTargetInfoName) => {
|
||||
const routes = {
|
||||
'dropbox': { name: 'DropboxLogin', target: 7 },
|
||||
'onedrive': { name: 'OneDriveLogin', target: 3 },
|
||||
'joplinCloud': { name: 'JoplinCloudLogin', target: 10 },
|
||||
};
|
||||
const route = routes[name];
|
||||
if (!route) return; // throw error??
|
||||
|
||||
const onJoplinCloudPasswordChange = useCallback((event: any) => {
|
||||
setJoplinCloudPassword(event.target.value);
|
||||
}, []);
|
||||
|
||||
const onJoplinCloudLoginClick = useCallback(async () => {
|
||||
setJoplinCloudLoginInProgress(true);
|
||||
|
||||
let result = null;
|
||||
|
||||
try {
|
||||
result = await SyncTargetJoplinCloud.checkConfig({
|
||||
password: () => joplinCloudPassword,
|
||||
path: () => Setting.value('sync.10.path'),
|
||||
userContentPath: () => Setting.value('sync.10.userContentPath'),
|
||||
username: () => joplinCloudEmail,
|
||||
});
|
||||
} finally {
|
||||
setJoplinCloudLoginInProgress(false);
|
||||
}
|
||||
|
||||
if (result.ok) {
|
||||
Setting.setValue('sync.target', 10);
|
||||
Setting.setValue('sync.10.username', joplinCloudEmail);
|
||||
Setting.setValue('sync.10.password', joplinCloudPassword);
|
||||
await Setting.saveAll();
|
||||
|
||||
alert(_('Thank you! Your Joplin Cloud account is now setup and ready to use.'));
|
||||
|
||||
closeDialog(props.dispatch);
|
||||
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Main',
|
||||
});
|
||||
} else {
|
||||
alert(_('There was an error setting up your Joplin Cloud account. Please verify your email and password and try again. Error was:\n\n%s', result.errorMessage));
|
||||
}
|
||||
}, [joplinCloudEmail, joplinCloudPassword, props.dispatch]);
|
||||
|
||||
const onJoplinCloudCreateAccountClick = useCallback(() => {
|
||||
void bridge().openExternal('https://joplinapp.org/plans/');
|
||||
}, []);
|
||||
|
||||
function renderJoplinCloudLoginForm() {
|
||||
return (
|
||||
<JoplinCloudLoginForm>
|
||||
<div style={{ fontSize: '16px' }}>{_('Login below.')} <CreateAccountLink href="#" onClick={onJoplinCloudCreateAccountClick}>{_('Or create an account.')}</CreateAccountLink></div>
|
||||
<FormLabel>{_('Email')}</FormLabel>
|
||||
<StyledInput type="email" onChange={onJoplinCloudEmailChange}/>
|
||||
<FormLabel>{_('Password')}</FormLabel>
|
||||
<StyledInput type="password" onChange={onJoplinCloudPasswordChange}/>
|
||||
<SelectButton mt="1.3em" disabled={joplinCloudLoginInProgress} level={ButtonLevel.Primary} title={_('Login')} onClick={onJoplinCloudLoginClick}/>
|
||||
</JoplinCloudLoginForm>
|
||||
);
|
||||
}
|
||||
|
||||
const onSelectButtonClick = useCallback(async (name: string) => {
|
||||
if (name === 'joplinCloud') {
|
||||
setShowJoplinCloudForm(true);
|
||||
} else {
|
||||
Setting.setValue('sync.target', name === 'dropbox' ? 7 : 3);
|
||||
await Setting.saveAll();
|
||||
closeDialog(props.dispatch);
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: name === 'dropbox' ? 'DropboxLogin' : 'OneDriveLogin',
|
||||
});
|
||||
}
|
||||
Setting.setValue('sync.target', route.target);
|
||||
await Setting.saveAll();
|
||||
closeDialog(props.dispatch);
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: route.name,
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
function renderSelectArea(info: SyncTargetInfo) {
|
||||
if (info.name === 'joplinCloud' && showJoplinCloudForm) {
|
||||
return renderJoplinCloudLoginForm();
|
||||
} else {
|
||||
return (
|
||||
<SelectButton
|
||||
level={ButtonLevel.Primary}
|
||||
title={_('Select')}
|
||||
onClick={() => onSelectButtonClick(info.name)}
|
||||
disabled={joplinCloudLoginInProgress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SelectButton
|
||||
level={ButtonLevel.Primary}
|
||||
title={_('Select')}
|
||||
onClick={() => onSelectButtonClick(info.name as SyncTargetInfoName)}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSyncTarget(info: SyncTargetInfo) {
|
||||
@ -289,7 +210,7 @@ export default function(props: Props) {
|
||||
const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/${logoImageName}` : '';
|
||||
const logo = logoImageSrc ? <SyncTargetLogo src={logoImageSrc}/> : null;
|
||||
const descriptionComp = <SyncTargetDescription height={height} ref={info.name === 'joplinCloud' ? joplinCloudDescriptionRef : null}>{info.description}</SyncTargetDescription>;
|
||||
const featuresComp = showJoplinCloudForm && info.name === 'joplinCloud' ? null : renderFeatures(info.name);
|
||||
const featuresComp = renderFeatures(info.name);
|
||||
|
||||
const renderSlowSyncWarning = () => {
|
||||
if (info.name === 'joplinCloud') return null;
|
||||
@ -297,7 +218,7 @@ export default function(props: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<SyncTargetBox id={key} key={key} faded={showJoplinCloudForm && info.name !== 'joplinCloud'}>
|
||||
<SyncTargetBox id={key} key={key}>
|
||||
<SyncTargetTitle>{logo}{info.label}</SyncTargetTitle>
|
||||
{descriptionComp}
|
||||
{featuresComp}
|
||||
@ -328,7 +249,7 @@ export default function(props: Props) {
|
||||
boxes.push(renderSyncTarget(info));
|
||||
}
|
||||
|
||||
const selfHostingMessage = showJoplinCloudForm ? null : <SelfHostingMessage>Self-hosting? Joplin also supports various self-hosting options such as Nextcloud, WebDAV, AWS S3 and Joplin Server. <a href="#" onClick={onSelfHostingClick}>Click here to select one</a>.</SelfHostingMessage>;
|
||||
const selfHostingMessage = <SelfHostingMessage>Self-hosting? Joplin also supports various self-hosting options such as Nextcloud, WebDAV, AWS S3 and Joplin Server. <a href="#" onClick={onSelfHostingClick}>Click here to select one</a>.</SelfHostingMessage>;
|
||||
|
||||
return (
|
||||
<ContentRoot>
|
||||
|
@ -122,7 +122,7 @@ a {
|
||||
margin: 40px 20px;
|
||||
}
|
||||
|
||||
.modal-message #loading-animation {
|
||||
#loading-animation {
|
||||
margin-right: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
@ -6,6 +6,7 @@
|
||||
@use 'gui/Dropdown/style.scss' as dropdown-control;
|
||||
@use 'gui/ShareFolderDialog/style.scss' as share-folder-dialog;
|
||||
@use 'gui/NoteList/style.scss' as note-list;
|
||||
@use 'gui/JoplinCloudLoginScreen.scss' as joplin-cloud-login-screen;
|
||||
@use 'gui/NoteListHeader/style.scss' as note-list-header;
|
||||
@use 'gui/TrashNotification/style.scss' as trash-notification;
|
||||
@use 'main.scss' as main;
|
@ -777,6 +777,7 @@ export default class BaseApplication {
|
||||
// Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com');
|
||||
Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
|
||||
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
|
||||
Setting.setValue('sync.10.website', 'http://joplincloud.local:22300');
|
||||
}
|
||||
|
||||
// For now always disable fuzzy search due to performance issues:
|
||||
|
@ -6,6 +6,7 @@ import { Env } from './models/Setting';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import personalizedUserContentBaseUrl from './services/joplinServer/personalizedUserContentBaseUrl';
|
||||
import { getHttpStatusMessage } from './net-utils';
|
||||
import { getApplicationInformation } from './services/joplinCloudUtils';
|
||||
const { stringify } = require('query-string');
|
||||
|
||||
const logger = Logger.create('JoplinServerApi');
|
||||
@ -63,13 +64,27 @@ export default class JoplinServerApi {
|
||||
return personalizedUserContentBaseUrl(userId, this.baseUrl(), this.options_.userContentBaseUrl());
|
||||
}
|
||||
|
||||
private async getClientInfo() {
|
||||
const { platform, type } = await getApplicationInformation();
|
||||
const clientInfo = {
|
||||
platform,
|
||||
type,
|
||||
version: shim.appVersion(),
|
||||
};
|
||||
|
||||
return clientInfo;
|
||||
}
|
||||
|
||||
private async session() {
|
||||
if (this.session_) return this.session_;
|
||||
|
||||
const clientInfo = await this.getClientInfo();
|
||||
|
||||
try {
|
||||
this.session_ = await this.exec_('POST', 'api/sessions', null, {
|
||||
email: this.options_.username(),
|
||||
password: this.options_.password(),
|
||||
...clientInfo,
|
||||
});
|
||||
|
||||
return this.session_;
|
||||
|
@ -46,7 +46,19 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
|
||||
}
|
||||
|
||||
public async isAuthenticated() {
|
||||
return true;
|
||||
try {
|
||||
const fileApi = await this.fileApi();
|
||||
const api = fileApi.driver().api();
|
||||
const sessionId = await api.sessionId();
|
||||
return !!sessionId;
|
||||
} catch (error) {
|
||||
if (error.code === 403) return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public authRouteName() {
|
||||
return 'JoplinCloudLogin';
|
||||
}
|
||||
|
||||
public static requiresPassword() {
|
||||
|
@ -705,26 +705,22 @@ class Setting extends BaseModel {
|
||||
public: false,
|
||||
storage: SettingStorage.Database,
|
||||
},
|
||||
'sync.10.website': {
|
||||
value: 'https://joplincloud.com',
|
||||
type: SettingItemType.String,
|
||||
public: false,
|
||||
storage: SettingStorage.Database,
|
||||
},
|
||||
'sync.10.username': {
|
||||
value: '',
|
||||
type: SettingItemType.String,
|
||||
section: 'sync',
|
||||
show: (settings: any) => {
|
||||
return settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud');
|
||||
},
|
||||
public: true,
|
||||
label: () => _('Joplin Cloud email'),
|
||||
public: false,
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'sync.10.password': {
|
||||
value: '',
|
||||
type: SettingItemType.String,
|
||||
section: 'sync',
|
||||
show: (settings: any) => {
|
||||
return settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud');
|
||||
},
|
||||
public: true,
|
||||
label: () => _('Joplin Cloud password'),
|
||||
public: false,
|
||||
secure: true,
|
||||
},
|
||||
|
||||
|
114
packages/lib/services/joplinCloudUtils.ts
Normal file
114
packages/lib/services/joplinCloudUtils.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { Reducer } from 'react';
|
||||
import Setting from '../models/Setting';
|
||||
import { ApplicationPlatform, ApplicationType } from '../types';
|
||||
import shim from '../shim';
|
||||
import { _ } from '../locale';
|
||||
|
||||
type ActionType = 'LINK_USED' | 'COMPLETED' | 'ERROR';
|
||||
type Action = {
|
||||
type: ActionType;
|
||||
payload?: any;
|
||||
};
|
||||
|
||||
type DefaultState = {
|
||||
className: 'text' | 'bold';
|
||||
message: ()=> string;
|
||||
next: ActionType;
|
||||
active: ActionType | 'INITIAL';
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
export const defaultState: DefaultState = {
|
||||
className: 'text',
|
||||
message: ()=> _('Waiting for authorisation...'),
|
||||
next: 'LINK_USED',
|
||||
active: 'INITIAL',
|
||||
};
|
||||
|
||||
export const reducer: Reducer<DefaultState, Action> = (state: DefaultState, action: Action) => {
|
||||
switch (action.type) {
|
||||
case 'LINK_USED': {
|
||||
return {
|
||||
className: 'text',
|
||||
message: () => _('If you have already authorised, please wait for the application to sync to Joplin Cloud.'),
|
||||
next: 'COMPLETED',
|
||||
active: 'LINK_USED',
|
||||
};
|
||||
}
|
||||
case 'COMPLETED': {
|
||||
return {
|
||||
className: 'bold',
|
||||
message: () => _('You are logged in into Joplin Cloud, you can leave this screen now.'),
|
||||
active: 'COMPLETED',
|
||||
next: 'COMPLETED',
|
||||
};
|
||||
}
|
||||
case 'ERROR': {
|
||||
return {
|
||||
className: 'text',
|
||||
message: () => _('You were unable to connect to Joplin Cloud, verify your connection. Error: '),
|
||||
active: 'ERROR',
|
||||
next: 'COMPLETED',
|
||||
errorMessage: action.payload,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getApplicationInformation = async () => {
|
||||
const platformName = await shim.platformName();
|
||||
switch (platformName) {
|
||||
case 'ios':
|
||||
return { type: ApplicationType.Mobile, platform: ApplicationPlatform.Ios };
|
||||
case 'android':
|
||||
return { type: ApplicationType.Mobile, platform: ApplicationPlatform.Android };
|
||||
case 'darwin':
|
||||
return { type: ApplicationType.Desktop, platform: ApplicationPlatform.MacOs };
|
||||
case 'win32':
|
||||
return { type: ApplicationType.Desktop, platform: ApplicationPlatform.Windows };
|
||||
case 'linux':
|
||||
return { type: ApplicationType.Desktop, platform: ApplicationPlatform.Linux };
|
||||
default:
|
||||
return { type: ApplicationType.Unknown, platform: ApplicationPlatform.Unknown };
|
||||
}
|
||||
};
|
||||
|
||||
export const generateApplicationConfirmUrl = async (confirmUrl: string) => {
|
||||
const applicationInfo = await getApplicationInformation();
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('platform', applicationInfo.platform.toString());
|
||||
searchParams.append('type', applicationInfo.type.toString());
|
||||
searchParams.append('version', shim.appVersion());
|
||||
|
||||
return `${confirmUrl}?${searchParams.toString()}`;
|
||||
};
|
||||
|
||||
|
||||
// We have isWaitingResponse inside the function to avoid any state from lingering
|
||||
// after an error occurs. E.g.: if the function would throw an error while isWaitingResponse
|
||||
// was set to true the next time we call the function the value would still be true.
|
||||
// The closure function prevents that.
|
||||
export const checkIfLoginWasSuccessful = async (applicationsUrl: string) => {
|
||||
let isWaitingResponse = false;
|
||||
const performLoginRequest = async () => {
|
||||
if (isWaitingResponse) return undefined;
|
||||
isWaitingResponse = true;
|
||||
|
||||
const response = await fetch(applicationsUrl);
|
||||
const jsonBody = await response.json();
|
||||
|
||||
if (!response.ok || jsonBody.status !== 'finished') {
|
||||
isWaitingResponse = false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Setting.setValue('sync.10.username', jsonBody.id);
|
||||
Setting.setValue('sync.10.password', jsonBody.password);
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
return performLoginRequest();
|
||||
};
|
@ -4,6 +4,7 @@ export enum ApplicationPlatform {
|
||||
Linux = 2,
|
||||
MacOs = 3,
|
||||
Android = 4,
|
||||
Ios = 5,
|
||||
}
|
||||
|
||||
export enum ApplicationType {
|
||||
|
Loading…
Reference in New Issue
Block a user