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
This commit is contained in:
Joel Jeremy Marquez
2025-04-09 08:34:44 -07:00
committed by GitHub
parent 1b5be7f9d2
commit 01f45d5072
10 changed files with 46 additions and 398 deletions

View File

@@ -86,7 +86,9 @@ global.Actual = {
});
},
startOAuthServer: () => {},
startOAuthServer: () => {
return '';
},
restartElectronServer: () => {},

View File

@@ -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<void>;
setMultiuserEnabled: (enabled: boolean) => void;
setLoginMethods: (methods: LoginMethods[]) => void;
setLoginMethods: (methods: LoginMethod[]) => void;
};
const ServerContext = createContext<ServerContextValue>({
@@ -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<ReturnType<Handlers['subscribe-needs-bootstrap']>>) => {
if ('hasServer' in data && data.hasServer) {
setAvailableLoginMethods(data.availableLoginMethods);
setMultiuserEnabled(data.multiuser);
setAvailableLoginMethods(data.availableLoginMethods || []);
setMultiuserEnabled(data.multiuser || false);
}
},
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,5 @@
import { Message } from '../server/sync';
import { QueryState } from '../shared/query';
import { OpenIdConfig } from './models/openid';
export interface ServerHandlers {
undo: () => Promise<void>;
redo: () => Promise<void>;
@@ -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<boolean>;
'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<void>;
'get-server-version': () => Promise<{ error?: string } | { version: string }>;
'get-server-url': () => Promise<string | null>;
@@ -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<void>;
'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
>;
}

View File

@@ -29,7 +29,7 @@ type Actual = {
) => void;
isUpdateReadyForDownload: () => boolean;
waitForUpdateReadyForDownload: () => Promise<void>;
startOAuthServer: () => Promise<void>;
startOAuthServer: () => Promise<string>;
};
declare global {

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [joel-jeremy]
---
Extract auth related server handlers from main.ts to server/auth/app.ts