1
0
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:
pedr 2024-03-11 12:17:23 -03:00 committed by GitHub
parent 55cafb8891
commit 8bdac6ffbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 206 additions and 3 deletions

View File

@ -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
View File

@ -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

View File

@ -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']);

View File

@ -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;

View File

@ -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 },