From fc308ececc1b2423b8f9f32d940ec520a784b9d1 Mon Sep 17 00:00:00 2001 From: Jed Fox <git@jedfox.com> Date: Tue, 21 Feb 2023 13:36:11 -0500 Subject: [PATCH] Allow the server to auto-configure the server URL for the client (#649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow the server to auto-configure the server URL for the client * Extract server URL/version logic out to ensure consistent updates * () * Be more explicit about when the server version is re-fetched * Use a single layer of context provider * Move the bootstrap route to /account/needs-bootstrap * No more `isActual` * Refactor to call subscribe-needs-bootstrap instead of fetch() * Dedupe calls to subscribe-needs-bootstrap * Don’t revalidate the server when we just validated it * simplify * Fix setServerURL --- .../src/components/LoggedInUser.js | 2 +- .../src/components/ServerContext.js | 68 +++++++++++++++++++ .../desktop-client/src/components/Titlebar.js | 3 +- .../src/components/manager/ConfigServer.js | 12 ++-- .../src/components/manager/ManagementApp.js | 2 +- .../src/components/manager/ServerURL.js | 2 +- .../components/manager/subscribe/common.js | 22 +++++- .../src/components/settings/Encryption.js | 2 +- .../src/components/settings/index.js | 2 +- .../desktop-client/src/hooks/useServerURL.js | 18 ----- .../src/hooks/useServerVersion.js | 23 ------- packages/desktop-client/src/index.js | 5 +- packages/loot-core/src/server/main.js | 20 +++--- 13 files changed, 116 insertions(+), 65 deletions(-) create mode 100644 packages/desktop-client/src/components/ServerContext.js delete mode 100644 packages/desktop-client/src/hooks/useServerURL.js delete mode 100644 packages/desktop-client/src/hooks/useServerVersion.js diff --git a/packages/desktop-client/src/components/LoggedInUser.js b/packages/desktop-client/src/components/LoggedInUser.js index 41fec806e..63306f797 100644 --- a/packages/desktop-client/src/components/LoggedInUser.js +++ b/packages/desktop-client/src/components/LoggedInUser.js @@ -12,7 +12,7 @@ import { } from 'loot-design/src/components/common'; import { colors } from 'loot-design/src/style'; -import { useServerURL } from '../hooks/useServerURL'; +import { useServerURL } from './ServerContext'; function LoggedInUser({ files, diff --git a/packages/desktop-client/src/components/ServerContext.js b/packages/desktop-client/src/components/ServerContext.js new file mode 100644 index 000000000..270503318 --- /dev/null +++ b/packages/desktop-client/src/components/ServerContext.js @@ -0,0 +1,68 @@ +import React, { + createContext, + useState, + useCallback, + useEffect, + useContext, +} from 'react'; + +import { send } from 'loot-core/src/platform/client/fetch'; + +const ServerContext = createContext({}); + +export const useServerURL = () => useContext(ServerContext).url; +export const useServerVersion = () => useContext(ServerContext).version; +export const useSetServerURL = () => useContext(ServerContext).setURL; + +async function getServerUrl() { + let url = (await send('get-server-url')) || ''; + if (url === 'https://not-configured/') { + url = ''; + } + return url; +} + +async function getServerVersion() { + let { error, version } = await send('get-server-version'); + if (error) { + return ''; + } + return version; +} + +export function ServerProvider({ children }) { + let [serverURL, setServerURL] = useState(''); + let [version, setVersion] = useState(''); + + useEffect(() => { + async function run() { + setServerURL(await getServerUrl()); + setVersion(await getServerVersion()); + } + run(); + }, []); + + let setURL = useCallback( + async (url, opts = {}) => { + let { error } = await send('set-server-url', { ...opts, url }); + if (!error) { + setServerURL(await getServerUrl()); + setVersion(await getServerVersion()); + } + return { error }; + }, + [setServerURL], + ); + + return ( + <ServerContext.Provider + value={{ + url: serverURL, + setURL, + version: version ? `v${version}` : 'N/A', + }} + > + {children} + </ServerContext.Provider> + ); +} diff --git a/packages/desktop-client/src/components/Titlebar.js b/packages/desktop-client/src/components/Titlebar.js index 8585f0cec..585af85a8 100644 --- a/packages/desktop-client/src/components/Titlebar.js +++ b/packages/desktop-client/src/components/Titlebar.js @@ -25,13 +25,12 @@ import ArrowButtonRight1 from 'loot-design/src/svg/v2/ArrowButtonRight1'; import NavigationMenu from 'loot-design/src/svg/v2/NavigationMenu'; import tokens from 'loot-design/src/tokens'; -import { useServerURL } from '../hooks/useServerURL'; - import AccountSyncCheck from './accounts/AccountSyncCheck'; import AnimatedRefresh from './AnimatedRefresh'; import { MonthCountSelector } from './budget/MonthCountSelector'; import { useSidebar } from './FloatableSidebar'; import LoggedInUser from './LoggedInUser'; +import { useServerURL } from './ServerContext'; export let TitlebarContext = React.createContext(); diff --git a/packages/desktop-client/src/components/manager/ConfigServer.js b/packages/desktop-client/src/components/manager/ConfigServer.js index a9385ddef..d00f0e563 100644 --- a/packages/desktop-client/src/components/manager/ConfigServer.js +++ b/packages/desktop-client/src/components/manager/ConfigServer.js @@ -4,7 +4,6 @@ import { useHistory } from 'react-router-dom'; import { createBudget } from 'loot-core/src/client/actions/budgets'; import { signOut, loggedIn } from 'loot-core/src/client/actions/user'; -import { send } from 'loot-core/src/platform/client/fetch'; import { View, Text, @@ -18,7 +17,7 @@ import { isPreviewEnvironment, } from 'loot-design/src/util/environment'; -import { useServerURL } from '../../hooks/useServerURL'; +import { useServerURL, useSetServerURL } from '../ServerContext'; import { Title, Input } from './subscribe/common'; @@ -28,6 +27,7 @@ export default function ConfigServer() { let history = useHistory(); let [url, setUrl] = useState(''); let currentUrl = useServerURL(); + let setServerUrl = useSetServerURL(); useEffect(() => { setUrl(currentUrl); }, [currentUrl]); @@ -50,14 +50,14 @@ export default function ConfigServer() { setError(null); setLoading(true); - let { error } = await send('set-server-url', { url }); + let { error } = await setServerUrl(url); if ( error === 'network-failure' && !url.startsWith('http://') && !url.startsWith('https://') ) { - let { error } = await send('set-server-url', { url: 'https://' + url }); + let { error } = await setServerUrl('https://' + url); if (error) { setUrl('https://' + url); setError(error); @@ -81,13 +81,13 @@ export default function ConfigServer() { } async function onSkip() { - await send('set-server-url', { url: null }); + await setServerUrl(null); await dispatch(loggedIn()); history.push('/'); } async function onCreateTestFile() { - await send('set-server-url', { url: null }); + await setServerUrl(null); await dispatch(createBudget({ testMode: true })); window.__history.push('/'); } diff --git a/packages/desktop-client/src/components/manager/ManagementApp.js b/packages/desktop-client/src/components/manager/ManagementApp.js index 96b0e0925..bc29ceade 100644 --- a/packages/desktop-client/src/components/manager/ManagementApp.js +++ b/packages/desktop-client/src/components/manager/ManagementApp.js @@ -9,9 +9,9 @@ import { View, Text } from 'loot-design/src/components/common'; import { colors } from 'loot-design/src/style'; import tokens from 'loot-design/src/tokens'; -import useServerVersion from '../../hooks/useServerVersion'; import LoggedInUser from '../LoggedInUser'; import Notifications from '../Notifications'; +import { useServerVersion } from '../ServerContext'; import ConfigServer from './ConfigServer'; import Modals from './Modals'; diff --git a/packages/desktop-client/src/components/manager/ServerURL.js b/packages/desktop-client/src/components/manager/ServerURL.js index 48ce36c3a..6d3538135 100644 --- a/packages/desktop-client/src/components/manager/ServerURL.js +++ b/packages/desktop-client/src/components/manager/ServerURL.js @@ -2,7 +2,7 @@ import React from 'react'; import { View, Text, AnchorLink } from 'loot-design/src/components/common'; -import { useServerURL } from '../../hooks/useServerURL'; +import { useServerURL } from '../ServerContext'; export default function ServerURL() { const url = useServerURL(); diff --git a/packages/desktop-client/src/components/manager/subscribe/common.js b/packages/desktop-client/src/components/manager/subscribe/common.js index 26ad67eda..16516e36d 100644 --- a/packages/desktop-client/src/components/manager/subscribe/common.js +++ b/packages/desktop-client/src/components/manager/subscribe/common.js @@ -9,6 +9,8 @@ import { } from 'loot-design/src/components/common'; import { colors, styles } from 'loot-design/src/style'; +import { useSetServerURL } from '../../ServerContext'; + // There are two URLs that dance with each other: `/login` and // `/bootstrap`. Both of these URLs check the state of the the server // and make sure the user is looking at the right page. For example, @@ -22,6 +24,7 @@ export function useBootstrapped() { let [checked, setChecked] = useState(false); let history = useHistory(); let location = useLocation(); + let setServerURL = useSetServerURL(); useEffect(() => { async function run() { @@ -36,7 +39,24 @@ export function useBootstrapped() { let url = await send('get-server-url'); if (url == null) { // A server hasn't been specified yet - history.push('/config-server'); + let serverURL = window.location.origin; + let { error, hasServer, bootstrapped } = await send( + 'subscribe-needs-bootstrap', + { url: serverURL }, + ); + if (error || !hasServer) { + console.log(error); + history.push('/config-server'); + return; + } + + await setServerURL(serverURL, { validate: false }); + + if (bootstrapped) { + ensure('/login'); + } else { + ensure('/bootstrap'); + } } else { let { error, bootstrapped } = await send('subscribe-needs-bootstrap'); if (error) { diff --git a/packages/desktop-client/src/components/settings/Encryption.js b/packages/desktop-client/src/components/settings/Encryption.js index 5b3176aea..b20ec99c5 100644 --- a/packages/desktop-client/src/components/settings/Encryption.js +++ b/packages/desktop-client/src/components/settings/Encryption.js @@ -3,7 +3,7 @@ import React from 'react'; import { Text, Button } from 'loot-design/src/components/common'; import { colors } from 'loot-design/src/style'; -import { useServerURL } from '../../hooks/useServerURL'; +import { useServerURL } from '../ServerContext'; import { Setting } from './UI'; diff --git a/packages/desktop-client/src/components/settings/index.js b/packages/desktop-client/src/components/settings/index.js index c5a39923d..bb7b2f589 100644 --- a/packages/desktop-client/src/components/settings/index.js +++ b/packages/desktop-client/src/components/settings/index.js @@ -12,9 +12,9 @@ import { colors } from 'loot-design/src/style'; import tokens from 'loot-design/src/tokens'; import { withThemeColor } from 'loot-design/src/util/withThemeColor'; -import useServerVersion from '../../hooks/useServerVersion'; import { isMobile } from '../../util'; import { Page } from '../Page'; +import { useServerVersion } from '../ServerContext'; import EncryptionSettings from './Encryption'; import ExperimentalFeatures from './Experimental'; diff --git a/packages/desktop-client/src/hooks/useServerURL.js b/packages/desktop-client/src/hooks/useServerURL.js deleted file mode 100644 index 8adfb05dd..000000000 --- a/packages/desktop-client/src/hooks/useServerURL.js +++ /dev/null @@ -1,18 +0,0 @@ -import { useState, useEffect } from 'react'; - -import { send } from 'loot-core/src/platform/client/fetch'; - -export function useServerURL() { - let [serverUrl, setServerUrl] = useState(''); - useEffect(() => { - async function run() { - let url = (await send('get-server-url')) || ''; - if (url === 'https://not-configured/') { - url = ''; - } - setServerUrl(url); - } - run(); - }, []); - return serverUrl; -} diff --git a/packages/desktop-client/src/hooks/useServerVersion.js b/packages/desktop-client/src/hooks/useServerVersion.js deleted file mode 100644 index 07fd31de8..000000000 --- a/packages/desktop-client/src/hooks/useServerVersion.js +++ /dev/null @@ -1,23 +0,0 @@ -import { useState, useEffect } from 'react'; - -import { send } from 'loot-core/src/platform/client/fetch'; - -function useServerVersion() { - let [version, setVersion] = useState(''); - - useEffect(() => { - (async () => { - const { error, version } = await send('get-server-version'); - - if (error) { - setVersion(''); - } else { - setVersion(version); - } - })(); - }, []); - - return version ? `v${version}` : 'N/A'; -} - -export default useServerVersion; diff --git a/packages/desktop-client/src/index.js b/packages/desktop-client/src/index.js index 1cd3eabad..5658e3b87 100644 --- a/packages/desktop-client/src/index.js +++ b/packages/desktop-client/src/index.js @@ -27,6 +27,7 @@ import { initialState as initialAppState } from 'loot-core/src/client/reducers/a import { send } from 'loot-core/src/platform/client/fetch'; import App from './components/App'; +import { ServerProvider } from './components/ServerContext'; import { handleGlobalEvents } from './global-events'; // See https://github.com/WICG/focus-visible. Only makes the blue @@ -71,7 +72,9 @@ window.$q = q; ReactDOM.render( <Provider store={store}> - <App /> + <ServerProvider> + <App /> + </ServerProvider> </Provider>, document.getElementById('root'), ); diff --git a/packages/loot-core/src/server/main.js b/packages/loot-core/src/server/main.js index 62bcaeb94..52a5a4c5c 100644 --- a/packages/loot-core/src/server/main.js +++ b/packages/loot-core/src/server/main.js @@ -1370,7 +1370,7 @@ handlers['key-test'] = async function ({ fileId, password }) { handlers['subscribe-needs-bootstrap'] = async function ({ url } = {}) { if (getServer(url).BASE_SERVER === UNCONFIGURED_SERVER) { - return { bootstrapped: true }; + return { bootstrapped: true, hasServer: false }; } let res; @@ -1390,7 +1390,7 @@ handlers['subscribe-needs-bootstrap'] = async function ({ url } = {}) { return { error: res.reason }; } - return { bootstrapped: res.data.bootstrapped }; + return { bootstrapped: res.data.bootstrapped, hasServer: true }; }; handlers['subscribe-bootstrap'] = async function ({ password }) { @@ -1505,14 +1505,16 @@ handlers['get-server-url'] = async function () { return getServer() && getServer().BASE_SERVER; }; -handlers['set-server-url'] = async function ({ url }) { +handlers['set-server-url'] = async function ({ url, validate = true }) { if (url != null) { - // Validate the server is running - let { error } = await runHandler(handlers['subscribe-needs-bootstrap'], { - url, - }); - if (error) { - return { error }; + if (validate) { + // Validate the server is running + let { error } = await runHandler(handlers['subscribe-needs-bootstrap'], { + url, + }); + if (error) { + return { error }; + } } } else { // When the server isn't configured, we just use a placeholder -- GitLab