mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-23 18:53:36 +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/ImportScreen.js
|
||||||
packages/app-desktop/gui/ItemList.js
|
packages/app-desktop/gui/ItemList.js
|
||||||
packages/app-desktop/gui/JoplinCloudConfigScreen.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/KeymapConfigScreen.js
|
||||||
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
|
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
|
||||||
packages/app-desktop/gui/KeymapConfig/styles/index.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.test.js
|
||||||
packages/lib/services/interop/Module.js
|
packages/lib/services/interop/Module.js
|
||||||
packages/lib/services/interop/types.js
|
packages/lib/services/interop/types.js
|
||||||
|
packages/lib/services/joplinCloudUtils.js
|
||||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
||||||
packages/lib/services/keychain/KeychainService.js
|
packages/lib/services/keychain/KeychainService.js
|
||||||
packages/lib/services/keychain/KeychainServiceDriver.dummy.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/ImportScreen.js
|
||||||
packages/app-desktop/gui/ItemList.js
|
packages/app-desktop/gui/ItemList.js
|
||||||
packages/app-desktop/gui/JoplinCloudConfigScreen.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/KeymapConfigScreen.js
|
||||||
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
|
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
|
||||||
packages/app-desktop/gui/KeymapConfig/styles/index.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.test.js
|
||||||
packages/lib/services/interop/Module.js
|
packages/lib/services/interop/Module.js
|
||||||
packages/lib/services/interop/types.js
|
packages/lib/services/interop/types.js
|
||||||
|
packages/lib/services/joplinCloudUtils.js
|
||||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
||||||
packages/lib/services/keychain/KeychainService.js
|
packages/lib/services/keychain/KeychainService.js
|
||||||
packages/lib/services/keychain/KeychainServiceDriver.dummy.js
|
packages/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||||
|
@ -58,6 +58,15 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async checkSyncConfig_() {
|
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);
|
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');
|
const { ResourceScreen } = require('./ResourceScreen.js');
|
||||||
import Navigator from './Navigator';
|
import Navigator from './Navigator';
|
||||||
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
|
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
|
||||||
|
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
|
||||||
const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components');
|
const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components');
|
||||||
const bridge = require('@electron/remote').require('./bridge').default;
|
const bridge = require('@electron/remote').require('./bridge').default;
|
||||||
|
|
||||||
@ -224,6 +225,7 @@ class RootComponent extends React.Component<Props, any> {
|
|||||||
Main: { screen: MainScreen },
|
Main: { screen: MainScreen },
|
||||||
OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') },
|
OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') },
|
||||||
DropboxLogin: { screen: DropboxLoginScreen, title: () => _('Dropbox Login') },
|
DropboxLogin: { screen: DropboxLoginScreen, title: () => _('Dropbox Login') },
|
||||||
|
JoplinCloudLogin: { screen: JoplinCloudLoginScreen, title: () => _('Joplin Cloud Login') },
|
||||||
Import: { screen: ImportScreen, title: () => _('Import') },
|
Import: { screen: ImportScreen, title: () => _('Import') },
|
||||||
Config: { screen: ConfigScreen, title: () => _('Options') },
|
Config: { screen: ConfigScreen, title: () => _('Options') },
|
||||||
Resources: { screen: ResourceScreen, title: () => _('Note attachments') },
|
Resources: { screen: ResourceScreen, title: () => _('Note attachments') },
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState, useRef, useCallback } from 'react';
|
import { useRef, useCallback } from 'react';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import DialogButtonRow from '../DialogButtonRow';
|
import DialogButtonRow from '../DialogButtonRow';
|
||||||
import Dialog from '../Dialog';
|
import Dialog from '../Dialog';
|
||||||
@ -9,10 +9,7 @@ import SyncTargetRegistry, { SyncTargetInfo } from '@joplin/lib/SyncTargetRegist
|
|||||||
import useElementSize from '@joplin/lib/hooks/useElementSize';
|
import useElementSize from '@joplin/lib/hooks/useElementSize';
|
||||||
import Button, { ButtonLevel } from '../Button/Button';
|
import Button, { ButtonLevel } from '../Button/Button';
|
||||||
import bridge from '../../services/bridge';
|
import bridge from '../../services/bridge';
|
||||||
import StyledInput from '../style/StyledInput';
|
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
|
|
||||||
import StyledLink from '../style/StyledLink';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
themeId: number;
|
themeId: number;
|
||||||
@ -32,10 +29,6 @@ const SyncTargetDescription = styled.div<{ height: number }>`
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CreateAccountLink = styled(StyledLink)`
|
|
||||||
font-size: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ContentRoot = styled.div`
|
const ContentRoot = styled.div`
|
||||||
background-color: ${props => props.theme.backgroundColor3};
|
background-color: ${props => props.theme.backgroundColor3};
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
@ -70,7 +63,7 @@ const SyncTargetLogo = styled.img`
|
|||||||
margin-right: 0.4em;
|
margin-right: 0.4em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SyncTargetBox = styled.div<{ faded: boolean }>`
|
const SyncTargetBox = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -82,7 +75,7 @@ const SyncTargetBox = styled.div<{ faded: boolean }>`
|
|||||||
padding: 2em 2.2em 2em 2.2em;
|
padding: 2em 2.2em 2em 2.2em;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
opacity: ${props => props.faded ? 0.5 : 1};
|
opacity: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FeatureList = styled.div`
|
const FeatureList = styled.div`
|
||||||
@ -117,16 +110,6 @@ const SelectButton = styled(Button)`
|
|||||||
font-size: 1em;
|
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`
|
const SlowSyncWarning = styled.div`
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
@ -152,12 +135,10 @@ const logosImageNames: Record<string, string> = {
|
|||||||
'onedrive': 'SyncTarget_OneDrive.svg',
|
'onedrive': 'SyncTarget_OneDrive.svg',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SyncTargetInfoName = 'dropbox' | 'onedrive' | 'joplinCloud';
|
||||||
|
|
||||||
export default function(props: Props) {
|
export default function(props: Props) {
|
||||||
const [showJoplinCloudForm, setShowJoplinCloudForm] = useState(false);
|
|
||||||
const joplinCloudDescriptionRef = useRef(null);
|
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
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
function closeDialog(dispatch: Function) {
|
function closeDialog(dispatch: Function) {
|
||||||
@ -192,93 +173,33 @@ export default function(props: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onJoplinCloudEmailChange = useCallback((event: any) => {
|
const onSelectButtonClick = useCallback(async (name: SyncTargetInfoName) => {
|
||||||
setJoplinCloudEmail(event.target.value);
|
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) => {
|
Setting.setValue('sync.target', route.target);
|
||||||
setJoplinCloudPassword(event.target.value);
|
await Setting.saveAll();
|
||||||
}, []);
|
closeDialog(props.dispatch);
|
||||||
|
props.dispatch({
|
||||||
const onJoplinCloudLoginClick = useCallback(async () => {
|
type: 'NAV_GO',
|
||||||
setJoplinCloudLoginInProgress(true);
|
routeName: route.name,
|
||||||
|
});
|
||||||
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',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [props.dispatch]);
|
}, [props.dispatch]);
|
||||||
|
|
||||||
function renderSelectArea(info: SyncTargetInfo) {
|
function renderSelectArea(info: SyncTargetInfo) {
|
||||||
if (info.name === 'joplinCloud' && showJoplinCloudForm) {
|
return (
|
||||||
return renderJoplinCloudLoginForm();
|
<SelectButton
|
||||||
} else {
|
level={ButtonLevel.Primary}
|
||||||
return (
|
title={_('Select')}
|
||||||
<SelectButton
|
onClick={() => onSelectButtonClick(info.name as SyncTargetInfoName)}
|
||||||
level={ButtonLevel.Primary}
|
disabled={false}
|
||||||
title={_('Select')}
|
/>
|
||||||
onClick={() => onSelectButtonClick(info.name)}
|
);
|
||||||
disabled={joplinCloudLoginInProgress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSyncTarget(info: SyncTargetInfo) {
|
function renderSyncTarget(info: SyncTargetInfo) {
|
||||||
@ -289,7 +210,7 @@ export default function(props: Props) {
|
|||||||
const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/${logoImageName}` : '';
|
const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/${logoImageName}` : '';
|
||||||
const logo = logoImageSrc ? <SyncTargetLogo src={logoImageSrc}/> : null;
|
const logo = logoImageSrc ? <SyncTargetLogo src={logoImageSrc}/> : null;
|
||||||
const descriptionComp = <SyncTargetDescription height={height} ref={info.name === 'joplinCloud' ? joplinCloudDescriptionRef : null}>{info.description}</SyncTargetDescription>;
|
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 = () => {
|
const renderSlowSyncWarning = () => {
|
||||||
if (info.name === 'joplinCloud') return null;
|
if (info.name === 'joplinCloud') return null;
|
||||||
@ -297,7 +218,7 @@ export default function(props: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SyncTargetBox id={key} key={key} faded={showJoplinCloudForm && info.name !== 'joplinCloud'}>
|
<SyncTargetBox id={key} key={key}>
|
||||||
<SyncTargetTitle>{logo}{info.label}</SyncTargetTitle>
|
<SyncTargetTitle>{logo}{info.label}</SyncTargetTitle>
|
||||||
{descriptionComp}
|
{descriptionComp}
|
||||||
{featuresComp}
|
{featuresComp}
|
||||||
@ -328,7 +249,7 @@ export default function(props: Props) {
|
|||||||
boxes.push(renderSyncTarget(info));
|
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 (
|
return (
|
||||||
<ContentRoot>
|
<ContentRoot>
|
||||||
|
@ -122,7 +122,7 @@ a {
|
|||||||
margin: 40px 20px;
|
margin: 40px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-message #loading-animation {
|
#loading-animation {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
@use 'gui/Dropdown/style.scss' as dropdown-control;
|
@use 'gui/Dropdown/style.scss' as dropdown-control;
|
||||||
@use 'gui/ShareFolderDialog/style.scss' as share-folder-dialog;
|
@use 'gui/ShareFolderDialog/style.scss' as share-folder-dialog;
|
||||||
@use 'gui/NoteList/style.scss' as note-list;
|
@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/NoteListHeader/style.scss' as note-list-header;
|
||||||
@use 'gui/TrashNotification/style.scss' as trash-notification;
|
@use 'gui/TrashNotification/style.scss' as trash-notification;
|
||||||
@use 'main.scss' as main;
|
@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.userContentPath', 'https://joplinusercontent.com');
|
||||||
Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
|
Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
|
||||||
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.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:
|
// 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 Logger from '@joplin/utils/Logger';
|
||||||
import personalizedUserContentBaseUrl from './services/joplinServer/personalizedUserContentBaseUrl';
|
import personalizedUserContentBaseUrl from './services/joplinServer/personalizedUserContentBaseUrl';
|
||||||
import { getHttpStatusMessage } from './net-utils';
|
import { getHttpStatusMessage } from './net-utils';
|
||||||
|
import { getApplicationInformation } from './services/joplinCloudUtils';
|
||||||
const { stringify } = require('query-string');
|
const { stringify } = require('query-string');
|
||||||
|
|
||||||
const logger = Logger.create('JoplinServerApi');
|
const logger = Logger.create('JoplinServerApi');
|
||||||
@ -63,13 +64,27 @@ export default class JoplinServerApi {
|
|||||||
return personalizedUserContentBaseUrl(userId, this.baseUrl(), this.options_.userContentBaseUrl());
|
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() {
|
private async session() {
|
||||||
if (this.session_) return this.session_;
|
if (this.session_) return this.session_;
|
||||||
|
|
||||||
|
const clientInfo = await this.getClientInfo();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.session_ = await this.exec_('POST', 'api/sessions', null, {
|
this.session_ = await this.exec_('POST', 'api/sessions', null, {
|
||||||
email: this.options_.username(),
|
email: this.options_.username(),
|
||||||
password: this.options_.password(),
|
password: this.options_.password(),
|
||||||
|
...clientInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.session_;
|
return this.session_;
|
||||||
|
@ -46,7 +46,19 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async isAuthenticated() {
|
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() {
|
public static requiresPassword() {
|
||||||
|
@ -705,26 +705,22 @@ class Setting extends BaseModel {
|
|||||||
public: false,
|
public: false,
|
||||||
storage: SettingStorage.Database,
|
storage: SettingStorage.Database,
|
||||||
},
|
},
|
||||||
|
'sync.10.website': {
|
||||||
|
value: 'https://joplincloud.com',
|
||||||
|
type: SettingItemType.String,
|
||||||
|
public: false,
|
||||||
|
storage: SettingStorage.Database,
|
||||||
|
},
|
||||||
'sync.10.username': {
|
'sync.10.username': {
|
||||||
value: '',
|
value: '',
|
||||||
type: SettingItemType.String,
|
type: SettingItemType.String,
|
||||||
section: 'sync',
|
public: false,
|
||||||
show: (settings: any) => {
|
|
||||||
return settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud');
|
|
||||||
},
|
|
||||||
public: true,
|
|
||||||
label: () => _('Joplin Cloud email'),
|
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
},
|
},
|
||||||
'sync.10.password': {
|
'sync.10.password': {
|
||||||
value: '',
|
value: '',
|
||||||
type: SettingItemType.String,
|
type: SettingItemType.String,
|
||||||
section: 'sync',
|
public: false,
|
||||||
show: (settings: any) => {
|
|
||||||
return settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud');
|
|
||||||
},
|
|
||||||
public: true,
|
|
||||||
label: () => _('Joplin Cloud password'),
|
|
||||||
secure: true,
|
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,
|
Linux = 2,
|
||||||
MacOs = 3,
|
MacOs = 3,
|
||||||
Android = 4,
|
Android = 4,
|
||||||
|
Ios = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ApplicationType {
|
export enum ApplicationType {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user