mirror of
https://github.com/laurent22/joplin.git
synced 2026-10-25 02:47:28 +00:00
update
This commit is contained in:
1 parent
f439835281
commit
fa07eb3db0
8 files changed
+189
-3
No files matched your search
@@ -878,6 +878,7 @@ packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/folder.js
|
||||
packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/screens/tags.js
|
||||
packages/app-mobile/components/screens/webdav-oidc-login.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/testing/TestProviderStack.js
|
||||
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.js
|
||||
|
||||
@@ -851,6 +851,7 @@ packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/folder.js
|
||||
packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/screens/tags.js
|
||||
packages/app-mobile/components/screens/webdav-oidc-login.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/testing/TestProviderStack.js
|
||||
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.js
|
||||
|
||||
@@ -739,6 +739,14 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
return false;
|
||||
};
|
||||
|
||||
private handleSettingButtonPress = async (key: string) => {
|
||||
if (key === 'sync.6.oidcLogin') {
|
||||
// Save current settings before navigating to login
|
||||
await shared.saveSettings(this);
|
||||
await NavService.go('WebDavOidcLogin');
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public settingToComponent(key: string, value: any) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -755,6 +763,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
themeId={this.props.themeId}
|
||||
updateSettingValue={updateSettingValue}
|
||||
styles={this.styles()}
|
||||
onSettingButtonPress={this.handleSettingButtonPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { UpdateSettingValueCallback } from './types';
|
||||
import { View, Text } from 'react-native';
|
||||
import { View, Text, Button } from 'react-native';
|
||||
import Setting, { AppType } from '@joplin/lib/models/Setting';
|
||||
import Dropdown from '../../Dropdown';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
@@ -23,6 +23,7 @@ interface Props {
|
||||
themeId: number;
|
||||
|
||||
updateSettingValue: UpdateSettingValueCallback;
|
||||
onSettingButtonPress?: (key: string)=> void;
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +128,21 @@ const SettingComponent: React.FunctionComponent<Props> = props => {
|
||||
/>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BUTTON) {
|
||||
// TODO: Not yet supported
|
||||
return (
|
||||
<View key={props.settingId} style={containerStyles.outerContainer}>
|
||||
<View style={containerStyles.innerContainer}>
|
||||
<Button
|
||||
title={md.label()}
|
||||
onPress={() => {
|
||||
if (props.onSettingButtonPress) {
|
||||
props.onSettingButtonPress(props.settingId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{descriptionComp}
|
||||
</View>
|
||||
);
|
||||
} else if (Setting.value('env') === 'dev') {
|
||||
throw new Error(`Unsupported setting type: ${md.type}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { View, Button } from 'react-native';
|
||||
import { WebView, WebViewNavigation } from 'react-native-webview';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { ScreenHeader } from '../ScreenHeader';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { themeStyle } from '../global-style';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import OidcApi from '@joplin/lib/OidcApi';
|
||||
const parseUri = require('@joplin/lib/parseUri');
|
||||
|
||||
const WebDavOidcLoginScreen: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const themeId = useSelector((state: { settings: { theme: number } }) => state.settings.theme);
|
||||
|
||||
const [webviewUrl, setWebviewUrl] = useState('');
|
||||
const [oidcApi, setOidcApi] = useState<OidcApi | null>(null);
|
||||
const [redirectUri, setRedirectUri] = useState('');
|
||||
const [oauthState, setOauthState] = useState('');
|
||||
|
||||
const authCodeRef = useRef<string | null>(null);
|
||||
|
||||
const styles = useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
return {
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
};
|
||||
}, [themeId]);
|
||||
|
||||
useEffect(() => {
|
||||
const initOidc = async () => {
|
||||
const api = new OidcApi({
|
||||
issuerUrl: Setting.value('sync.6.oidcIssuerUrl'),
|
||||
clientId: Setting.value('sync.6.oidcClientId'),
|
||||
clientSecret: Setting.value('sync.6.oidcClientSecret'),
|
||||
ignoreTlsErrors: Setting.value('net.ignoreTlsErrors'),
|
||||
});
|
||||
|
||||
// Use a custom redirect URI that the WebView can intercept
|
||||
// This is a common pattern for mobile OAuth - using a non-http URI
|
||||
const redirect = 'joplin://oidc-callback';
|
||||
const state = Math.random().toString(36).substring(7);
|
||||
|
||||
const authCodeUrl = await api.authCodeUrl(redirect, state);
|
||||
|
||||
setOidcApi(api);
|
||||
setRedirectUri(redirect);
|
||||
setOauthState(state);
|
||||
setWebviewUrl(authCodeUrl);
|
||||
};
|
||||
|
||||
void initOidc();
|
||||
}, []);
|
||||
|
||||
const handleWebviewLoad = useCallback(async (event: WebViewNavigation) => {
|
||||
const url = event.url;
|
||||
|
||||
// Check if this is our callback URL
|
||||
if (url.startsWith('joplin://oidc-callback')) {
|
||||
const parsedUrl = parseUri(url);
|
||||
const query = parsedUrl.queryKey;
|
||||
|
||||
if (query.error) {
|
||||
const errorDesc = query.error_description || query.error;
|
||||
alert(`${_('Authentication failed')}: ${errorDesc}`);
|
||||
dispatch({ type: 'NAV_BACK' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authCodeRef.current && query.code) {
|
||||
// Verify state to prevent CSRF
|
||||
if (query.state !== oauthState) {
|
||||
alert(_('Authentication failed: Invalid state parameter'));
|
||||
dispatch({ type: 'NAV_BACK' });
|
||||
return;
|
||||
}
|
||||
|
||||
authCodeRef.current = query.code;
|
||||
|
||||
try {
|
||||
await oidcApi.execTokenRequest(authCodeRef.current, redirectUri);
|
||||
const auth = oidcApi.auth();
|
||||
|
||||
const syncTargetId = Setting.value('sync.target');
|
||||
Setting.setValue(`sync.${syncTargetId}.oidcAuth`, auth ? JSON.stringify(auth) : '');
|
||||
|
||||
// Update the sync target's API with the new auth
|
||||
const syncTarget = reg.syncTarget(syncTargetId);
|
||||
if (syncTarget.api && syncTarget.api()) {
|
||||
syncTarget.api().setAuth(auth);
|
||||
}
|
||||
|
||||
dispatch({ type: 'NAV_BACK' });
|
||||
void reg.scheduleSync(0);
|
||||
} catch (error) {
|
||||
alert(`${_('Could not authenticate with OIDC provider. Please try again')}\n\n${(error as Error).message}`);
|
||||
}
|
||||
|
||||
authCodeRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [dispatch, oidcApi, oauthState, redirectUri]);
|
||||
|
||||
const handleWebviewError = useCallback(() => {
|
||||
alert(_('Could not load page. Please check your connection and try again.'));
|
||||
}, []);
|
||||
|
||||
const handleRetryPress = useCallback(() => {
|
||||
// Reload the page by setting a temporary URL then back to the auth URL
|
||||
const authUrl = webviewUrl;
|
||||
|
||||
setWebviewUrl('about:blank');
|
||||
|
||||
shim.setTimeout(() => {
|
||||
setWebviewUrl(authUrl);
|
||||
}, 500);
|
||||
}, [webviewUrl]);
|
||||
|
||||
const handleShouldStartLoadWithRequest = useCallback((request: { url: string }) => {
|
||||
// Intercept the callback URL
|
||||
if (request.url.startsWith('joplin://oidc-callback')) {
|
||||
void handleWebviewLoad({ url: request.url } as WebViewNavigation);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [handleWebviewLoad]);
|
||||
|
||||
const source = useMemo(() => ({ uri: webviewUrl }), [webviewUrl]);
|
||||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
<ScreenHeader title={_('WebDAV OIDC Login')} />
|
||||
<WebView
|
||||
source={source}
|
||||
onNavigationStateChange={(event: WebViewNavigation) => {
|
||||
void handleWebviewLoad(event);
|
||||
}}
|
||||
onError={handleWebviewError}
|
||||
// Allow the custom joplin:// scheme to be intercepted
|
||||
originWhitelist={['*']}
|
||||
onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
|
||||
/>
|
||||
<Button
|
||||
title={_('Refresh')}
|
||||
onPress={handleRetryPress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebDavOidcLoginScreen;
|
||||
@@ -42,6 +42,7 @@ import SearchScreen from './components/screens/SearchScreen';
|
||||
const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js');
|
||||
import EncryptionConfigScreen from './components/screens/encryption-config';
|
||||
import DropboxLoginScreen from './components/screens/dropbox-login.js';
|
||||
import WebDavOidcLoginScreen from './components/screens/webdav-oidc-login';
|
||||
import { MenuProvider } from 'react-native-popup-menu';
|
||||
import SideMenu, { SideMenuPosition } from './components/SideMenu';
|
||||
import SideMenuContent from './components/side-menu-content';
|
||||
@@ -712,6 +713,7 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
||||
OneDriveLogin: { screen: OneDriveLoginScreen },
|
||||
DropboxLogin: { screen: DropboxLoginScreen },
|
||||
JoplinCloudLogin: { screen: JoplinCloudLoginScreen },
|
||||
WebDavOidcLogin: { screen: WebDavOidcLoginScreen },
|
||||
JoplinServerSamlLogin: { screen: SsoLoginScreen(new SamlShared()) },
|
||||
EncryptionConfig: { screen: EncryptionConfigScreen },
|
||||
UpgradeSyncTarget: { screen: UpgradeSyncTargetScreen },
|
||||
|
||||
@@ -21,6 +21,7 @@ function historyCanGoBackTo(route: any) {
|
||||
// it can be buggy to do so, due to incorrectly relying on global state (reg.syncTarget...)
|
||||
if (route.routeName === 'OneDriveLogin') return false;
|
||||
if (route.routeName === 'DropboxLogin') return false;
|
||||
if (route.routeName === 'WebDavOidcLogin') return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
||||
value: null as null,
|
||||
type: SettingItemType.Button,
|
||||
public: true,
|
||||
appTypes: [AppType.Desktop],
|
||||
appTypes: [AppType.Desktop, AppType.Mobile],
|
||||
section: 'sync',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- No type is available for settings in this context
|
||||
show: (settings: any) => {
|
||||
|
||||
Reference in new issue
Block a user