diff --git a/packages/desktop-client/src/auth/AuthProvider.tsx b/packages/desktop-client/src/auth/AuthProvider.tsx index 7ec030f13e6..cfeea44af3b 100644 --- a/packages/desktop-client/src/auth/AuthProvider.tsx +++ b/packages/desktop-client/src/auth/AuthProvider.tsx @@ -25,7 +25,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { return ( (userData?.offline ?? false) || - (userData?.permissions?.includes(permission?.toUpperCase()) ?? false) + userData?.permission?.toUpperCase() === permission?.toUpperCase() ); }; diff --git a/packages/desktop-client/src/auth/types.ts b/packages/desktop-client/src/auth/types.ts index f183730b6e7..7c88e304d7e 100644 --- a/packages/desktop-client/src/auth/types.ts +++ b/packages/desktop-client/src/auth/types.ts @@ -1,3 +1,3 @@ export enum Permissions { - ADMINISTRATOR = 'ADMINISTRATOR', + ADMINISTRATOR = 'ADMIN', } diff --git a/packages/desktop-client/src/components/ServerContext.tsx b/packages/desktop-client/src/components/ServerContext.tsx index 71ffe83fc42..dd59c14cec4 100644 --- a/packages/desktop-client/src/components/ServerContext.tsx +++ b/packages/desktop-client/src/components/ServerContext.tsx @@ -8,6 +8,7 @@ import React, { } from 'react'; import { send } from 'loot-core/src/platform/client/fetch'; +import { type Handlers } from 'loot-core/types/handlers'; type LoginMethods = { method: string; @@ -25,6 +26,8 @@ type ServerContextValue = { opts?: { validate?: boolean }, ) => Promise<{ error?: string }>; refreshLoginMethods: () => Promise; + setMultiuserEnabled: (enabled: boolean) => void; + setLoginMethods: (methods: LoginMethods[]) => void; }; const ServerContext = createContext({ @@ -35,6 +38,8 @@ const ServerContext = createContext({ setURL: () => Promise.reject(new Error('ServerContext not initialized')), refreshLoginMethods: () => Promise.reject(new Error('ServerContext not initialized')), + setMultiuserEnabled: () => {}, + setLoginMethods: () => {}, }); export const useServerURL = () => useContext(ServerContext).url; @@ -69,6 +74,12 @@ async function getServerVersion() { export const useRefreshLoginMethods = () => useContext(ServerContext).refreshLoginMethods; +export const useSetMultiuserEnabled = () => + useContext(ServerContext).setMultiuserEnabled; + +export const useSetLoginMethods = () => + useContext(ServerContext).setLoginMethods; + export function ServerProvider({ children }: { children: ReactNode }) { const [serverURL, setServerURL] = useState(''); const [version, setVersion] = useState(''); @@ -93,13 +104,17 @@ export function ServerProvider({ children }: { children: ReactNode }) { }, [serverURL]); useEffect(() => { - refreshLoginMethods(); if (serverURL) { - send('multiuser-get').then((data: boolean) => { - setMultiuserEnabled(data); - }); + send('subscribe-needs-bootstrap').then( + (data: Awaited>) => { + if ('hasServer' in data && data.hasServer) { + setAvailableLoginMethods(data.loginMethods); + setMultiuserEnabled(data.multiuser); + } + }, + ); } - }, [serverURL, refreshLoginMethods]); + }, [serverURL]); const setURL = useCallback( async (url: string, opts: { validate?: boolean } = {}) => { @@ -122,6 +137,8 @@ export function ServerProvider({ children }: { children: ReactNode }) { setURL, version: version ? `v${version}` : 'N/A', refreshLoginMethods, + setMultiuserEnabled, + setLoginMethods: setAvailableLoginMethods, }} > {children} diff --git a/packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx b/packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx index ab808f7367c..1bb1bbcc56f 100644 --- a/packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx +++ b/packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx @@ -10,11 +10,12 @@ import React, { } from 'react'; import { useDispatch } from 'react-redux'; -import { css } from 'glamor'; +import { css } from '@emotion/css'; -import { pushModal } from 'loot-core/src/client/actions/modals'; +import { pushModal } from 'loot-core/client/actions'; import { send } from 'loot-core/src/platform/client/fetch'; import * as undo from 'loot-core/src/platform/client/undo'; +import { type Handlers } from 'loot-core/types/handlers'; import { type UserAvailable } from 'loot-core/types/models'; import { type UserAccessEntity } from 'loot-core/types/models/userAccess'; @@ -100,8 +101,16 @@ function UserAccessContent({ }, [cloudFileId, setLoading]); const loadOwner = useCallback(async () => { - const owner = (await send('file-owner-get', cloudFileId as string)) ?? {}; - return owner; + debugger; + const file: Awaited> = + (await send('get-user-file-info', cloudFileId as string)) ?? {}; + const owner = file?.usersWithAccess.filter(user => user.owner); + + if (owner.length > 0) { + return owner[0]; + } + + return null; }, [cloudFileId]); useEffect(() => { @@ -325,7 +334,7 @@ const iconStyle = css({ }); const LockToggle = props => ( -
+
diff --git a/packages/desktop-client/src/components/manager/subscribe/Login.tsx b/packages/desktop-client/src/components/manager/subscribe/Login.tsx index f2d5188a460..d95c7c1e7a9 100644 --- a/packages/desktop-client/src/components/manager/subscribe/Login.tsx +++ b/packages/desktop-client/src/components/manager/subscribe/Login.tsx @@ -77,7 +77,7 @@ function OpenIdLogin({ setError }) { async function onSetOpenId(config: OpenIdConfig) { setError(null); - const { error } = await send('subscribe-bootstrap', { openid: config }); + const { error } = await send('subscribe-bootstrap', { openId: config }); if (error) { setError(error); diff --git a/packages/desktop-client/src/components/manager/subscribe/common.tsx b/packages/desktop-client/src/components/manager/subscribe/common.tsx index 3d3697d00ee..d20638347ef 100644 --- a/packages/desktop-client/src/components/manager/subscribe/common.tsx +++ b/packages/desktop-client/src/components/manager/subscribe/common.tsx @@ -3,10 +3,15 @@ import React, { useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { send } from 'loot-core/src/platform/client/fetch'; +import { type Handlers } from 'loot-core/types/handlers'; import { useNavigate } from '../../../hooks/useNavigate'; import { theme } from '../../../style'; -import { useSetServerURL } from '../../ServerContext'; +import { + useSetLoginMethods, + useSetMultiuserEnabled, + 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 @@ -22,6 +27,8 @@ export function useBootstrapped(redirect = true) { const navigate = useNavigate(); const location = useLocation(); const setServerURL = useSetServerURL(); + const setMultiuserEnabled = useSetMultiuserEnabled(); + const setLoginMethods = useSetLoginMethods(); useEffect(() => { async function run() { @@ -40,7 +47,9 @@ export function useBootstrapped(redirect = true) { if (url == null && !bootstrapped) { // A server hasn't been specified yet const serverURL = window.location.origin; - const result = await send('subscribe-needs-bootstrap', { + const result: Awaited< + ReturnType + > = await send('subscribe-needs-bootstrap', { url: serverURL, }); @@ -52,17 +61,28 @@ export function useBootstrapped(redirect = true) { await setServerURL(serverURL, { validate: false }); + setMultiuserEnabled(result.multiuser); + setLoginMethods(result.loginMethods); + if (result.bootstrapped) { ensure(`/login`); } else { ensure('/bootstrap'); } } else { - const result = await send('subscribe-needs-bootstrap'); + const result: Awaited< + ReturnType + > = await send('subscribe-needs-bootstrap'); + if ('error' in result) { navigate('/error', { state: { error: result.error } }); } else if (result.bootstrapped) { ensure(`/login`); + + if ('hasServer' in result && result.hasServer) { + setMultiuserEnabled(result.multiuser); + setLoginMethods(result.loginMethods); + } } else { ensure('/bootstrap'); } diff --git a/packages/desktop-client/src/components/modals/EditUser.tsx b/packages/desktop-client/src/components/modals/EditUser.tsx index 38768dcb639..fa08078fb2e 100644 --- a/packages/desktop-client/src/components/modals/EditUser.tsx +++ b/packages/desktop-client/src/components/modals/EditUser.tsx @@ -124,9 +124,7 @@ function EditUser({ defaultUser, onSave: originalOnSave }: EditUserProps) { defaultUser.displayName ?? '', ); const [enabled, setEnabled] = useState(defaultUser.enabled); - const [role, setRole] = useState( - defaultUser.role ?? '213733c1-5645-46ad-8784-a7b20b400f93', - ); + const [role, setRole] = useState(defaultUser.role ?? 'BASIC'); const [error, setError] = useState(''); async function onSave() { diff --git a/packages/loot-core/src/server/admin/app.ts b/packages/loot-core/src/server/admin/app.ts index d7112fe5f1b..0eee9f0212e 100644 --- a/packages/loot-core/src/server/admin/app.ts +++ b/packages/loot-core/src/server/admin/app.ts @@ -149,7 +149,6 @@ app.method('access-delete-all', async function ({ fileId, ids }) { const userToken = await asyncStorage.getItem('user-token'); if (userToken) { try { - debugger; const res = await del( getServer().BASE_SERVER + `/admin/access?fileId=${fileId}`, { @@ -231,18 +230,8 @@ app.method('file-owner-get', async function (fileId) { return null; }); -app.method('multiuser-get', async function () { - const res = await get(getServer().BASE_SERVER + '/admin/multiuser/'); - - if (res) { - return (JSON.parse(res) as boolean) || false; - } - - return null; -}); - app.method('owner-created', async function () { - const res = await get(getServer().BASE_SERVER + '/admin/ownerCreated/'); + const res = await get(getServer().BASE_SERVER + '/admin/owner-created/'); if (res) { return JSON.parse(res) as boolean; diff --git a/packages/loot-core/src/server/admin/types/handlers.ts b/packages/loot-core/src/server/admin/types/handlers.ts index 784b721c723..8f84cd30d94 100644 --- a/packages/loot-core/src/server/admin/types/handlers.ts +++ b/packages/loot-core/src/server/admin/types/handlers.ts @@ -49,7 +49,5 @@ export interface AdminHandlers { 'file-owner-get': (fileId: string) => Promise; - 'multiuser-get': () => Promise; - 'owner-created': () => Promise; } diff --git a/packages/loot-core/src/server/cloud-storage.ts b/packages/loot-core/src/server/cloud-storage.ts index d0864ea9749..48a367c925e 100644 --- a/packages/loot-core/src/server/cloud-storage.ts +++ b/packages/loot-core/src/server/cloud-storage.ts @@ -389,6 +389,38 @@ export async function listRemoteFiles(): Promise { })); } +export async function getRemoteFile( + fileId: string, +): Promise { + const userToken = await asyncStorage.getItem('user-token'); + if (!userToken) { + return null; + } + + let res; + try { + res = await fetchJSON(getServer().SYNC_SERVER + '/get-user-file-info', { + headers: { + 'X-ACTUAL-TOKEN': userToken, + 'X-ACTUAL-FILE-ID': fileId, + }, + }); + } catch (e) { + console.log('Unexpected error fetching file from server', e); + return null; + } + + if (res.status === 'error') { + console.log('Error fetching file from server', res); + return null; + } + + return { + ...res.data, + hasKey: encryption.hasKey(res.data.encryptKeyId), + }; +} + export async function download(fileId) { const userToken = await asyncStorage.getItem('user-token'); const syncServer = getServer().SYNC_SERVER; diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index 19dc94326c8..73d7c3e3da7 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -1541,7 +1541,10 @@ handlers['subscribe-needs-bootstrap'] = async function ({ return { bootstrapped: res.data.bootstrapped, - loginMethod: res.data.loginMethod || 'password', + loginMethods: res.data.loginMethods || [ + { method: 'password', active: true, displayName: 'Password' }, + ], + multiuser: res.data.multiuser || false, hasServer: true, }; }; @@ -1597,7 +1600,7 @@ handlers['subscribe-get-user'] = async function () { reason, data: { userName = null, - permissions = [], + permission = '', userId = null, displayName = null, loginMethod = null, @@ -1617,7 +1620,7 @@ handlers['subscribe-get-user'] = async function () { return { offline: false, userName, - permissions, + permission, userId, displayName, loginMethod, @@ -1783,6 +1786,10 @@ handlers['get-remote-files'] = async function () { return cloudStorage.listRemoteFiles(); }; +handlers['get-user-file-info'] = async function (fileId: string) { + return cloudStorage.getRemoteFile(fileId); +}; + handlers['reset-budget-cache'] = mutator(async function () { // Recomputing everything will update the cache await sheet.loadUserBudgets(db); diff --git a/packages/loot-core/src/types/models/user.ts b/packages/loot-core/src/types/models/user.ts index 936b1061e0d..43b85030d0e 100644 --- a/packages/loot-core/src/types/models/user.ts +++ b/packages/loot-core/src/types/models/user.ts @@ -25,6 +25,6 @@ export interface UserAvailable { } export const PossibleRoles = { - '213733c1-5645-46ad-8784-a7b20b400f93': 'Admin', - 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc': 'Basic', + ADMIN: 'Admin', + BASIC: 'Basic', }; diff --git a/packages/loot-core/src/types/server-handlers.d.ts b/packages/loot-core/src/types/server-handlers.d.ts index 4f61fe192c5..f57ec8e02e3 100644 --- a/packages/loot-core/src/types/server-handlers.d.ts +++ b/packages/loot-core/src/types/server-handlers.d.ts @@ -267,10 +267,22 @@ export interface ServerHandlers { 'get-did-bootstrap': () => Promise; - 'subscribe-needs-bootstrap': (args: { - url; - }) => Promise< - { error: string } | { bootstrapped: unknown; hasServer: boolean } + 'subscribe-needs-bootstrap': (args: { url }) => Promise< + | { error: string } + | { + bootstrapped: boolean; + hasServer: false; + } + | { + bootstrapped: boolean; + hasServer: true; + loginMethods: { + method: string; + displayName: string; + active: boolean; + }[]; + multiuser: boolean; + } >; 'subscribe-get-login-methods': () => Promise<{ @@ -280,7 +292,7 @@ export interface ServerHandlers { 'subscribe-bootstrap': (arg: { password?: string; - openid?: OpenIdConfig; + openId?: OpenIdConfig; }) => Promise<{ error?: string }>; 'subscribe-get-user': () => Promise<{ @@ -288,7 +300,7 @@ export interface ServerHandlers { userName?: string; userId?: string; displayName?: string; - permissions?: string[]; + permission?: string; loginMethod?: string; tokenExpired?: boolean; } | null>; @@ -331,6 +343,8 @@ export interface ServerHandlers { 'get-remote-files': () => Promise; + 'get-user-file-info': (fileId: string) => Promise; + 'reset-budget-cache': () => Promise; 'upload-budget': (arg: { id }) => Promise<{ error?: string }>;