mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-23 18:53:36 +02:00
Mobile: Change Joplin Cloud login process to allow MFA via browser (#9776)
This commit is contained in:
parent
55cafb8891
commit
8bdac6ffbf
@ -570,6 +570,7 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/isPluginInstal
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/openWebsiteForPlugin.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -550,6 +550,7 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/isPluginInstal
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/openWebsiteForPlugin.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
|
@ -22,7 +22,6 @@ import NoteExportButton, { exportButtonDescription, exportButtonTitle } from './
|
||||
import SettingsButton from './SettingsButton';
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import SectionHeader from './SectionHeader';
|
||||
import ExportProfileButton, { exportProfileButtonTitle } from './NoteExportSection/ExportProfileButton';
|
||||
import SettingComponent from './SettingComponent';
|
||||
@ -53,8 +52,6 @@ interface ConfigScreenProps {
|
||||
settings: any;
|
||||
themeId: number;
|
||||
navigation: any;
|
||||
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, ConfigScreenState> {
|
||||
@ -84,6 +81,13 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
}
|
||||
|
||||
private checkSyncConfig_ = async () => {
|
||||
if (this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud')) {
|
||||
const isAuthenticated = await reg.syncTarget().isAuthenticated();
|
||||
if (!isAuthenticated) {
|
||||
void NavService.go('JoplinCloudLogin');
|
||||
return;
|
||||
}
|
||||
}
|
||||
// to ignore TLS errors we need to change the global state of the app, if the check fails we need to restore the original state
|
||||
// this call sets the new value and returns the previous one which we can use later to revert the change
|
||||
const prevIgnoreTlsErrors = await setIgnoreTlsErrors(this.state.settings['net.ignoreTlsErrors']);
|
||||
|
@ -0,0 +1,194 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { View, Text, StyleSheet, Linking, Animated, Easing } from 'react-native';
|
||||
const { connect } = require('react-redux');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
import { AppState } from '../../utils/types';
|
||||
import { generateApplicationConfirmUrl, reducer, checkIfLoginWasSuccessful, defaultState } from '@joplin/lib/services/joplinCloudUtils';
|
||||
import { uuidgen } from '@joplin/lib/uuid';
|
||||
import { Button } from 'react-native-paper';
|
||||
import createRootStyle from '../../utils/createRootStyle';
|
||||
import ScreenHeader from '../ScreenHeader';
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('JoplinCloudLoginScreen');
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
joplinCloudWebsite: string;
|
||||
joplinCloudApi: string;
|
||||
}
|
||||
const syncIconRotationValue = new Animated.Value(0);
|
||||
|
||||
const syncIconRotation = syncIconRotationValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '360deg'],
|
||||
});
|
||||
|
||||
let syncIconAnimation: Animated.CompositeAnimation;
|
||||
|
||||
const useStyle = (themeId: number) => {
|
||||
return React.useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
return StyleSheet.create({
|
||||
...createRootStyle(themeId),
|
||||
buttonsContainer: {
|
||||
display: 'flex',
|
||||
marginVertical: theme.fontSize * 1.5,
|
||||
},
|
||||
containerStyle: {
|
||||
padding: theme.margin,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
flex: 1,
|
||||
},
|
||||
text: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
},
|
||||
smallTextStyle: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize * 0.8,
|
||||
paddingBottom: theme.fontSize * 1.2,
|
||||
textAlign: 'center',
|
||||
},
|
||||
bold: {
|
||||
...theme.normalText,
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
loadingIcon: {
|
||||
marginVertical: theme.fontSize * 1.2,
|
||||
fontSize: 38,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
}, [themeId]);
|
||||
};
|
||||
|
||||
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] = React.useState(undefined);
|
||||
const [state, dispatch] = React.useReducer(reducer, defaultState);
|
||||
|
||||
const applicationAuthId = React.useMemo(() => uuidgen(), []);
|
||||
|
||||
const styles = useStyle(props.themeId);
|
||||
|
||||
const periodicallyCheckForCredentials = () => {
|
||||
if (intervalIdentifier) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const response = await checkIfLoginWasSuccessful(applicationAuthUrl(applicationAuthId));
|
||||
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 onAuthoriseClicked = async () => {
|
||||
const url = await generateApplicationConfirmUrl(confirmUrl(applicationAuthId));
|
||||
await Linking.openURL(url);
|
||||
onButtonUsed();
|
||||
};
|
||||
|
||||
const onCopyToClipboardClicked = async () => {
|
||||
const url = await generateApplicationConfirmUrl(confirmUrl(applicationAuthId));
|
||||
Clipboard.setString(url);
|
||||
onButtonUsed();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
clearInterval(intervalIdentifier);
|
||||
};
|
||||
}, [intervalIdentifier]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (intervalIdentifier && state.next === 'COMPLETED') {
|
||||
syncIconAnimation = Animated.loop(
|
||||
Animated.timing(syncIconRotationValue, {
|
||||
toValue: 1,
|
||||
duration: 1800,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
);
|
||||
|
||||
syncIconAnimation.start();
|
||||
}
|
||||
}, [intervalIdentifier, state]);
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<ScreenHeader title={_('Login with Joplin Cloud')} />
|
||||
<View style={styles.containerStyle}>
|
||||
<Text style={styles.text}>
|
||||
{_('To allow Joplin to synchronise with Joplin Cloud, open this URL in your browser to authorise the application:')}
|
||||
</Text>
|
||||
<View style={styles.buttonsContainer}>
|
||||
<View style={{ marginBottom: 20 }}>
|
||||
<Button
|
||||
onPress={onAuthoriseClicked}
|
||||
icon='open-in-new'
|
||||
mode='contained'
|
||||
>
|
||||
{_('Authorise')}
|
||||
</Button>
|
||||
</View>
|
||||
<Text style={styles.smallTextStyle}>Or</Text>
|
||||
<Button
|
||||
onPress={onCopyToClipboardClicked}
|
||||
icon='content-copy'
|
||||
mode='outlined'
|
||||
>{_('Copy link to website')}
|
||||
</Button>
|
||||
|
||||
</View>
|
||||
<Text style={styles[state.className]}>{state.message()}
|
||||
{state.active === 'ERROR' ? (
|
||||
<Text style={styles[state.className]}>{state.errorMessage}</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
{state.active === 'LINK_USED' ? (
|
||||
<Animated.View style={{ transform: [{ rotate: syncIconRotation }] }}>
|
||||
<Icon name='sync' style={styles.loadingIcon}/>
|
||||
</Animated.View>
|
||||
) : null }
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const JoplinCloudLoginScreen = connect((state: AppState) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
joplinCloudWebsite: state.settings['sync.10.website'],
|
||||
joplinCloudApi: state.settings['sync.10.path'],
|
||||
};
|
||||
})(JoplinCloudScreenComponent);
|
||||
|
||||
export default JoplinCloudLoginScreen;
|
||||
|
@ -85,6 +85,7 @@ const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js');
|
||||
const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
|
||||
import BiometricPopup from './components/biometrics/BiometricPopup';
|
||||
import initLib from '@joplin/lib/initLib';
|
||||
import JoplinCloudLoginScreen from './components/screens/JoplinCloudLoginScreen';
|
||||
|
||||
SyncTargetRegistry.addClass(SyncTargetNone);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
@ -603,6 +604,7 @@ async function initialize(dispatch: Function) {
|
||||
// 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');
|
||||
|
||||
// Setting.setValue('sync.target', 10);
|
||||
// Setting.setValue('sync.10.username', 'user1@example.com');
|
||||
@ -1095,6 +1097,7 @@ class AppComponent extends React.Component {
|
||||
Folder: { screen: FolderScreen },
|
||||
OneDriveLogin: { screen: OneDriveLoginScreen },
|
||||
DropboxLogin: { screen: DropboxLoginScreen },
|
||||
JoplinCloudLogin: { screen: JoplinCloudLoginScreen },
|
||||
EncryptionConfig: { screen: EncryptionConfigScreen },
|
||||
UpgradeSyncTarget: { screen: UpgradeSyncTargetScreen },
|
||||
ProfileSwitcher: { screen: ProfileSwitcher },
|
||||
|
Loading…
x
Reference in New Issue
Block a user