From 01f45d50721e29fab19744ab87e2b8e9c44916f4 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Wed, 9 Apr 2025 08:34:44 -0700 Subject: [PATCH] Extract auth related server handlers from main.ts to server/auth/app.ts (#4660) * Extract auth related server handlers from main.ts to server/auth/app.ts * Release notes * Update get-openid-config * Fix typecheck error * Fix lint and typecheck errors --- .../src/browser-preload.browser.js | 4 +- .../src/components/ServerContext.tsx | 12 +- .../components/manager/subscribe/Login.tsx | 8 +- packages/loot-core/src/client/app/appSlice.ts | 2 +- packages/loot-core/src/server/auth/app.ts | 23 +- packages/loot-core/src/server/main.ts | 301 +----------------- packages/loot-core/src/types/handlers.d.ts | 4 +- .../loot-core/src/types/server-handlers.d.ts | 82 ----- packages/loot-core/typings/window.d.ts | 2 +- upcoming-release-notes/4660.md | 6 + 10 files changed, 46 insertions(+), 398 deletions(-) create mode 100644 upcoming-release-notes/4660.md diff --git a/packages/desktop-client/src/browser-preload.browser.js b/packages/desktop-client/src/browser-preload.browser.js index 500acd69e7..3c29ddabf1 100644 --- a/packages/desktop-client/src/browser-preload.browser.js +++ b/packages/desktop-client/src/browser-preload.browser.js @@ -86,7 +86,9 @@ global.Actual = { }); }, - startOAuthServer: () => {}, + startOAuthServer: () => { + return ''; + }, restartElectronServer: () => {}, diff --git a/packages/desktop-client/src/components/ServerContext.tsx b/packages/desktop-client/src/components/ServerContext.tsx index aaa92a0576..5dd17a4bb2 100644 --- a/packages/desktop-client/src/components/ServerContext.tsx +++ b/packages/desktop-client/src/components/ServerContext.tsx @@ -15,7 +15,7 @@ import { type Handlers } from 'loot-core/types/handlers'; import { useDispatch } from '../redux'; -type LoginMethods = { +type LoginMethod = { method: string; displayName: string; active: boolean; @@ -25,14 +25,14 @@ type ServerContextValue = { url: string | null; version: string; multiuserEnabled: boolean; - availableLoginMethods: LoginMethods[]; + availableLoginMethods: LoginMethod[]; setURL: ( url: string, opts?: { validate?: boolean }, ) => Promise<{ error?: string }>; refreshLoginMethods: () => Promise; setMultiuserEnabled: (enabled: boolean) => void; - setLoginMethods: (methods: LoginMethods[]) => void; + setLoginMethods: (methods: LoginMethod[]) => void; }; const ServerContext = createContext({ @@ -91,7 +91,7 @@ export function ServerProvider({ children }: { children: ReactNode }) { const [version, setVersion] = useState(''); const [multiuserEnabled, setMultiuserEnabled] = useState(false); const [availableLoginMethods, setAvailableLoginMethods] = useState< - LoginMethods[] + LoginMethod[] >([]); useEffect(() => { @@ -134,8 +134,8 @@ export function ServerProvider({ children }: { children: ReactNode }) { send('subscribe-needs-bootstrap').then( (data: Awaited>) => { if ('hasServer' in data && data.hasServer) { - setAvailableLoginMethods(data.availableLoginMethods); - setMultiuserEnabled(data.multiuser); + setAvailableLoginMethods(data.availableLoginMethods || []); + setMultiuserEnabled(data.multiuser || false); } }, ); diff --git a/packages/desktop-client/src/components/manager/subscribe/Login.tsx b/packages/desktop-client/src/components/manager/subscribe/Login.tsx index 8809d4f089..3be12791d9 100644 --- a/packages/desktop-client/src/components/manager/subscribe/Login.tsx +++ b/packages/desktop-client/src/components/manager/subscribe/Login.tsx @@ -113,8 +113,8 @@ function OpenIdLogin({ setError }) { }, []); async function onSubmitOpenId() { - const { error, redirect_url } = await send('subscribe-sign-in', { - return_url: isElectron() + const { error, redirectUrl } = await send('subscribe-sign-in', { + returnUrl: isElectron() ? await window.Actual.startOAuthServer() : window.location.origin, loginMethod: 'openid', @@ -125,9 +125,9 @@ function OpenIdLogin({ setError }) { setError(error); } else { if (isElectron()) { - window.Actual?.openURLInBrowser(redirect_url); + window.Actual?.openURLInBrowser(redirectUrl); } else { - window.location.href = redirect_url; + window.location.href = redirectUrl; } } } diff --git a/packages/loot-core/src/client/app/appSlice.ts b/packages/loot-core/src/client/app/appSlice.ts index db2e194b07..653dd0a86f 100644 --- a/packages/loot-core/src/client/app/appSlice.ts +++ b/packages/loot-core/src/client/app/appSlice.ts @@ -91,7 +91,7 @@ export const sync = createAppAsyncThunk( const prefs = getState().prefs.local; if (prefs && prefs.id) { const result = await send('sync'); - if ('error' in result) { + if (result && 'error' in result) { return { error: result.error }; } diff --git a/packages/loot-core/src/server/auth/app.ts b/packages/loot-core/src/server/auth/app.ts index 608abce8a2..d42812eee2 100644 --- a/packages/loot-core/src/server/auth/app.ts +++ b/packages/loot-core/src/server/auth/app.ts @@ -322,23 +322,36 @@ async function enableOpenId(openIdConfig: { openId: OpenIdConfig }) { return {}; } -async function getOpenIdConfig() { +async function getOpenIdConfig({ password }: { password: string }) { try { + const userToken = await asyncStorage.getItem('user-token'); + const serverConfig = getServer(); if (!serverConfig) { throw new Error('No sync server configured.'); } - const res = await get(serverConfig.BASE_SERVER + '/openid/config'); + const res = await post( + serverConfig.BASE_SERVER + '/openid/config', + { password }, + { + 'X-ACTUAL-TOKEN': userToken, + }, + ); if (res) { - const config = JSON.parse(res) as OpenIdConfig; - return { openId: config }; + return res as { openId: OpenIdConfig }; } return null; } catch (err) { - return { error: 'config-fetch-failed' }; + if (err instanceof PostError) { + return { + error: err.reason || 'network-failure', + }; + } + + throw err; } } diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index 9571668547..25cb9f25dd 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -9,12 +9,12 @@ import * as fs from '../platform/server/fs'; import * as sqlite from '../platform/server/sqlite'; import { q } from '../shared/query'; import { Handlers } from '../types/handlers'; -import { OpenIdConfig } from '../types/models/openid'; import { app as accountsApp } from './accounts/app'; import { app as adminApp } from './admin/app'; import { installAPI } from './api'; import { runQuery as aqlQuery } from './aql'; +import { app as authApp } from './auth/app'; import { app as budgetApp } from './budget/app'; import { app as budgetFilesApp } from './budgetfiles/app'; import { app as dashboardApp } from './dashboard/app'; @@ -27,13 +27,13 @@ import { mutator, runHandler } from './mutators'; import { app as notesApp } from './notes/app'; import { app as payeesApp } from './payees/app'; import * as Platform from './platform'; -import { get, post } from './post'; +import { get } from './post'; import { app as preferencesApp } from './preferences/app'; import * as prefs from './prefs'; import { app as reportsApp } from './reports/app'; import { app as rulesApp } from './rules/app'; import { app as schedulesApp } from './schedules/app'; -import { getServer, isValidBaseURL, setServer } from './server-config'; +import { getServer, setServer } from './server-config'; import { app as spreadsheetApp } from './spreadsheet/app'; import { fullSync, setSyncingMode } from './sync'; import { app as syncApp } from './sync/app'; @@ -71,195 +71,6 @@ handlers['query'] = async function (query) { return aqlQuery(query); }; -handlers['get-did-bootstrap'] = async function () { - return Boolean(await asyncStorage.getItem('did-bootstrap')); -}; - -handlers['subscribe-needs-bootstrap'] = async function ({ - url, -}: { url? } = {}) { - if (url && !isValidBaseURL(url)) { - return { error: 'get-server-failure' }; - } - - try { - if (!getServer(url)) { - return { bootstrapped: true, hasServer: false }; - } - } catch (err) { - return { error: 'get-server-failure' }; - } - - let res; - try { - res = await get(getServer(url).SIGNUP_SERVER + '/needs-bootstrap'); - } catch (err) { - return { error: 'network-failure' }; - } - - try { - res = JSON.parse(res); - } catch (err) { - return { error: 'parse-failure' }; - } - - if (res.status === 'error') { - return { error: res.reason }; - } - - return { - bootstrapped: res.data.bootstrapped, - availableLoginMethods: res.data.availableLoginMethods || [ - { method: 'password', active: true, displayName: 'Password' }, - ], - multiuser: res.data.multiuser || false, - hasServer: true, - }; -}; - -handlers['subscribe-bootstrap'] = async function (loginConfig) { - try { - await post(getServer().SIGNUP_SERVER + '/bootstrap', loginConfig); - } catch (err) { - return { error: err.reason || 'network-failure' }; - } - return {}; -}; - -handlers['subscribe-get-login-methods'] = async function () { - let res; - try { - res = await fetch(getServer().SIGNUP_SERVER + '/login-methods').then(res => - res.json(), - ); - } catch (err) { - return { error: err.reason || 'network-failure' }; - } - - if (res.methods) { - return { methods: res.methods }; - } - return { error: 'internal' }; -}; - -handlers['subscribe-get-user'] = async function () { - if (!getServer()) { - if (!(await asyncStorage.getItem('did-bootstrap'))) { - return null; - } - return { offline: false }; - } - - const userToken = await asyncStorage.getItem('user-token'); - - if (!userToken) { - return null; - } - - try { - const res = await get(getServer().SIGNUP_SERVER + '/validate', { - headers: { - 'X-ACTUAL-TOKEN': userToken, - }, - }); - let tokenExpired = false; - const { - status, - reason, - data: { - userName = null, - permission = '', - userId = null, - displayName = null, - loginMethod = null, - } = {}, - } = JSON.parse(res) || {}; - - if (status === 'error') { - if (reason === 'unauthorized') { - return null; - } else if (reason === 'token-expired') { - tokenExpired = true; - } else { - return { offline: true }; - } - } - - return { - offline: false, - userName, - permission, - userId, - displayName, - loginMethod, - tokenExpired, - }; - } catch (e) { - console.log(e); - return { offline: true }; - } -}; - -handlers['subscribe-change-password'] = async function ({ password }) { - const userToken = await asyncStorage.getItem('user-token'); - if (!userToken) { - return { error: 'not-logged-in' }; - } - - try { - await post(getServer().SIGNUP_SERVER + '/change-password', { - token: userToken, - password, - }); - } catch (err) { - return { error: err.reason || 'network-failure' }; - } - - return {}; -}; - -handlers['subscribe-sign-in'] = async function (loginInfo) { - if ( - typeof loginInfo.loginMethod !== 'string' || - loginInfo.loginMethod == null - ) { - loginInfo.loginMethod = 'password'; - } - let res; - - try { - res = await post(getServer().SIGNUP_SERVER + '/login', loginInfo); - } catch (err) { - return { error: err.reason || 'network-failure' }; - } - - if (res.redirect_url) { - return { redirect_url: res.redirect_url }; - } - - if (!res.token) { - throw new Error('login: User token not set'); - } - - await asyncStorage.setItem('user-token', res.token); - return {}; -}; - -handlers['subscribe-sign-out'] = async function () { - encryption.unloadAllKeys(); - await asyncStorage.multiRemove([ - 'user-token', - 'encrypt-keys', - 'lastBudget', - 'readOnly', - ]); - return 'ok'; -}; - -handlers['subscribe-set-token'] = async function ({ token }) { - await asyncStorage.setItem('user-token', token); -}; - handlers['get-server-version'] = async function () { if (!getServer()) { return { error: 'no-server' }; @@ -305,111 +116,6 @@ handlers['set-server-url'] = async function ({ url, validate = true }) { return {}; }; -handlers['enable-openid'] = async function (loginConfig) { - try { - const userToken = await asyncStorage.getItem('user-token'); - - if (!userToken) { - return { error: 'unauthorized' }; - } - - await post(getServer().BASE_SERVER + '/openid/enable', loginConfig, { - 'X-ACTUAL-TOKEN': userToken, - }); - } catch (err) { - return { error: err.reason || 'network-failure' }; - } - return {}; -}; - -handlers['enable-password'] = async function (loginConfig) { - try { - const userToken = await asyncStorage.getItem('user-token'); - - if (!userToken) { - return { error: 'unauthorized' }; - } - - await post(getServer().BASE_SERVER + '/openid/disable', loginConfig, { - 'X-ACTUAL-TOKEN': userToken, - }); - } catch (err) { - return { error: err.reason || 'network-failure' }; - } - return {}; -}; - -handlers['get-openid-config'] = async function () { - try { - const res = await get(getServer().BASE_SERVER + '/openid/config'); - - if (res) { - const config = JSON.parse(res) as OpenIdConfig; - return { openId: config }; - } - - return null; - } catch (err) { - return { error: 'config-fetch-failed' }; - } -}; - -handlers['enable-openid'] = async function (loginConfig) { - try { - const userToken = await asyncStorage.getItem('user-token'); - - if (!userToken) { - return { error: 'unauthorized' }; - } - - await post(getServer().BASE_SERVER + '/openid/enable', loginConfig, { - 'X-ACTUAL-TOKEN': userToken, - }); - } catch (err) { - return { error: err.reason || 'network-failure' }; - } - return {}; -}; - -handlers['enable-password'] = async function (loginConfig) { - try { - const userToken = await asyncStorage.getItem('user-token'); - - if (!userToken) { - return { error: 'unauthorized' }; - } - - await post(getServer().BASE_SERVER + '/openid/disable', loginConfig, { - 'X-ACTUAL-TOKEN': userToken, - }); - } catch (err) { - return { error: err.reason || 'network-failure' }; - } - return {}; -}; - -handlers['get-openid-config'] = async function ({ password }) { - try { - const userToken = await asyncStorage.getItem('user-token'); - - const res = await post( - getServer().BASE_SERVER + '/openid/config', - { password }, - { - 'X-ACTUAL-TOKEN': userToken, - }, - ); - - if (res) { - return res as { openId: OpenIdConfig }; - } - - return null; - } catch (err) { - return { error: err.reason }; - } -}; - handlers['app-focused'] = async function () { if (prefs.getPrefs() && prefs.getPrefs().id) { // First we sync @@ -424,6 +130,7 @@ injectAPI.override((name, args) => runHandler(app.handlers[name], args)); // A hack for now until we clean up everything app.handlers = handlers; app.combine( + authApp, schedulesApp, budgetApp, dashboardApp, diff --git a/packages/loot-core/src/types/handlers.d.ts b/packages/loot-core/src/types/handlers.d.ts index 49054e7709..51d8e9387f 100644 --- a/packages/loot-core/src/types/handlers.d.ts +++ b/packages/loot-core/src/types/handlers.d.ts @@ -1,5 +1,6 @@ import type { AccountHandlers } from '../server/accounts/app'; import type { AdminHandlers } from '../server/admin/app'; +import type { AuthHandlers } from '../server/auth/app'; import type { BudgetHandlers } from '../server/budget/app'; import type { BudgetFileHandlers } from '../server/budgetfiles/app'; import type { DashboardHandlers } from '../server/dashboard/app'; @@ -38,6 +39,7 @@ export interface Handlers SpreadsheetHandlers, SyncHandlers, BudgetFileHandlers, - EncryptionHandlers {} + EncryptionHandlers, + AuthHandlers {} export type HandlerFunctions = Handlers[keyof Handlers]; diff --git a/packages/loot-core/src/types/server-handlers.d.ts b/packages/loot-core/src/types/server-handlers.d.ts index 0b2e1ea96f..f37ad10383 100644 --- a/packages/loot-core/src/types/server-handlers.d.ts +++ b/packages/loot-core/src/types/server-handlers.d.ts @@ -1,8 +1,5 @@ -import { Message } from '../server/sync'; import { QueryState } from '../shared/query'; -import { OpenIdConfig } from './models/openid'; - export interface ServerHandlers { undo: () => Promise; redo: () => Promise; @@ -15,66 +12,6 @@ export interface ServerHandlers { // eslint-disable-next-line @typescript-eslint/no-explicit-any query: (query: QueryState) => Promise<{ data: any; dependencies: string[] }>; - 'get-did-bootstrap': () => Promise; - - 'subscribe-needs-bootstrap': (args: { url }) => Promise< - | { error: string } - | { - bootstrapped: boolean; - hasServer: false; - } - | { - bootstrapped: boolean; - hasServer: true; - availableLoginMethods: { - method: string; - displayName: string; - active: boolean; - }[]; - multiuser: boolean; - } - >; - - 'subscribe-get-login-methods': () => Promise<{ - methods?: { method: string; displayName: string; active: boolean }[]; - error?: string; - }>; - - 'subscribe-bootstrap': (arg: { - password?: string; - openId?: OpenIdConfig; - }) => Promise<{ error?: string }>; - - 'subscribe-get-user': () => Promise<{ - offline: boolean; - userName?: string; - userId?: string; - displayName?: string; - permission?: string; - loginMethod?: string; - tokenExpired?: boolean; - } | null>; - - 'subscribe-change-password': (arg: { - password; - }) => Promise<{ error?: string }>; - - 'subscribe-sign-in': ( - arg: - | { - password; - loginMethod?: string; - } - | { - return_url; - loginMethod?: 'openid'; - }, - ) => Promise<{ error?: string; redirect_url?: string }>; - - 'subscribe-sign-out': () => Promise<'ok'>; - - 'subscribe-set-token': (arg: { token: string }) => Promise; - 'get-server-version': () => Promise<{ error?: string } | { version: string }>; 'get-server-url': () => Promise; @@ -84,24 +21,5 @@ export interface ServerHandlers { validate?: boolean; }) => Promise<{ error?: string }>; - sync: () => Promise< - | { error: { message: string; reason: string; meta: unknown } } - | { messages: Message[] } - >; - 'app-focused': () => Promise; - - 'enable-openid': (arg: { - openId?: OpenIdConfig; - }) => Promise<{ error?: string }>; - - 'enable-password': (arg: { password: string }) => Promise<{ error?: string }>; - - 'get-openid-config': (arg: { password: string }) => Promise< - | { - openId: OpenIdConfig; - } - | { error: string } - | null - >; } diff --git a/packages/loot-core/typings/window.d.ts b/packages/loot-core/typings/window.d.ts index afe10ea622..53ed497e04 100644 --- a/packages/loot-core/typings/window.d.ts +++ b/packages/loot-core/typings/window.d.ts @@ -29,7 +29,7 @@ type Actual = { ) => void; isUpdateReadyForDownload: () => boolean; waitForUpdateReadyForDownload: () => Promise; - startOAuthServer: () => Promise; + startOAuthServer: () => Promise; }; declare global { diff --git a/upcoming-release-notes/4660.md b/upcoming-release-notes/4660.md new file mode 100644 index 0000000000..450d22fa4b --- /dev/null +++ b/upcoming-release-notes/4660.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Extract auth related server handlers from main.ts to server/auth/app.ts