Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenId Implementation #3878

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
5 changes: 4 additions & 1 deletion packages/api/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ export function addTransactions(
}

export function importTransactions(accountId, transactions) {
return send('api/transactions-import', { accountId, transactions });
return send('api/transactions-import', {
accountId,
transactions,
});
}

export function getTransactions(accountId, startDate, endDate) {
Expand Down
lelemm marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions packages/desktop-client/src/auth/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { createContext, useContext, type ReactNode } from 'react';
import { useSelector } from 'react-redux';

import { type State } from 'loot-core/client/state-types';

import { type Permissions } from './types';

type AuthContextType = {
hasPermission: (permission?: Permissions) => boolean;
};

const AuthContext = createContext<AuthContextType | undefined>(undefined);

type AuthProviderProps = {
children?: ReactNode;
};

export const AuthProvider = ({ children }: AuthProviderProps) => {
const userData = useSelector((state: State) => state.user.data);

const hasPermission = (permission?: Permissions) => {
if (!permission) {
return true;
}

return (
(userData?.offline ?? false) ||
userData?.permission?.toUpperCase() === permission?.toUpperCase()
);
};
lelemm marked this conversation as resolved.
Show resolved Hide resolved

return (
<AuthContext.Provider value={{ hasPermission }}>
{children}
</AuthContext.Provider>
);
};

export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
64 changes: 64 additions & 0 deletions packages/desktop-client/src/auth/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useEffect, useState, type ReactElement } from 'react';
import { useSelector } from 'react-redux';

import { type RemoteFile, type SyncedLocalFile } from 'loot-core/types/file';

import { View } from '../components/common/View';
import { useMetadataPref } from '../hooks/useMetadataPref';

import { useAuth } from './AuthProvider';
import { type Permissions } from './types';

type ProtectedRouteProps = {
permission: Permissions;
element: ReactElement;
validateOwner?: boolean;
};

export const ProtectedRoute = ({
element,
permission,
validateOwner,
}: ProtectedRouteProps) => {
const { hasPermission } = useAuth();
const [permissionGranted, setPermissionGranted] = useState(false);
const [cloudFileId] = useMetadataPref('cloudFileId');
const allFiles = useSelector(state => state.budgets.allFiles || []);
const remoteFiles = allFiles.filter(
f => f.state === 'remote' || f.state === 'synced' || f.state === 'detached',
) as (SyncedLocalFile | RemoteFile)[];
lelemm marked this conversation as resolved.
Show resolved Hide resolved
const currentFile = remoteFiles.find(f => f.cloudFileId === cloudFileId);
const userData = useSelector(state => state.user.data);

useEffect(() => {
const hasRequiredPermission = hasPermission(permission);
setPermissionGranted(hasRequiredPermission);

if (!hasRequiredPermission && validateOwner) {
if (currentFile) {
setPermissionGranted(
currentFile.usersWithAccess.some(u => u.userId === userData?.userId),
);
}
}
}, [
cloudFileId,
permission,
validateOwner,
hasPermission,
currentFile,
userData,
]);

return permissionGranted ? (
element
) : (
<View
style={{
margin: '50px',
}}
>
<h3>You don&apos;t have permission to view this page</h3>
</View>
);
};
lelemm marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions packages/desktop-client/src/auth/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum Permissions {
ADMINISTRATOR = 'ADMIN',
}
42 changes: 41 additions & 1 deletion packages/desktop-client/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from 'react-error-boundary';
import { HotkeysProvider } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';

import {
Expand All @@ -20,12 +20,14 @@ import {
sync,
} from 'loot-core/client/actions';
import { SpreadsheetProvider } from 'loot-core/client/SpreadsheetProvider';
import { type State } from 'loot-core/client/state-types';
import * as Platform from 'loot-core/src/client/platform';
import {
init as initConnection,
send,
} from 'loot-core/src/platform/client/fetch';

import { useActions } from '../hooks/useActions';
import { useMetadataPref } from '../hooks/useMetadataPref';
import { installPolyfills } from '../polyfills';
import { styles, hasHiddenScrollbars, ThemeStyle, useTheme } from '../style';
Expand All @@ -49,6 +51,8 @@ function AppInner() {
const { t } = useTranslation();
const { showBoundary: showErrorBoundary } = useErrorBoundary();
const dispatch = useDispatch();
const userData = useSelector((state: State) => state.user.data);
const { signOut, addNotification } = useActions();

const maybeUpdate = async <T,>(cb?: () => T): Promise<T> => {
if (global.Actual.isUpdateReadyForDownload()) {
Expand Down Expand Up @@ -123,6 +127,42 @@ function AppInner() {
global.Actual.updateAppMenu(budgetId);
}, [budgetId]);

useEffect(() => {
if (userData?.tokenExpired) {
addNotification({
type: 'error',
id: 'login-expired',
title: 'Login expired',
sticky: true,
message: 'Login expired, please login again.',
button: {
title: 'Go to login',
action: () => {
signOut();
},
},
});
}
}, [userData, userData?.tokenExpired]);

useEffect(() => {
if (userData?.tokenExpired) {
addNotification({
type: 'error',
id: 'login-expired',
title: 'Login expired',
sticky: true,
message: 'Login expired, please login again.',
button: {
title: 'Go to login',
action: () => {
signOut();
},
},
});
}
}, [userData, userData?.tokenExpired]);
lelemm marked this conversation as resolved.
Show resolved Hide resolved

return budgetId ? <FinancesApp /> : <ManagementApp />;
}

Expand Down
31 changes: 30 additions & 1 deletion packages/desktop-client/src/components/FinancesApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import { addNotification, sync } from 'loot-core/client/actions';
import { type State } from 'loot-core/src/client/state-types';
import * as undo from 'loot-core/src/platform/client/undo';

import { ProtectedRoute } from '../auth/ProtectedRoute';
import { Permissions } from '../auth/types';
import { useAccounts } from '../hooks/useAccounts';
import { useLocalPref } from '../hooks/useLocalPref';
import { useMetaThemeColor } from '../hooks/useMetaThemeColor';
import { useNavigate } from '../hooks/useNavigate';
import { theme } from '../style';
import { getIsOutdated, getLatestVersion } from '../util/versions';

import { UserAccessPage } from './admin/UserAccess/UserAccessPage';
import { BankSyncStatus } from './BankSyncStatus';
import { View } from './common/View';
import { GlobalKeys } from './GlobalKeys';
Expand All @@ -34,7 +37,9 @@ import { Reports } from './reports';
import { LoadingIndicator } from './reports/LoadingIndicator';
import { NarrowAlternate, WideComponent } from './responsive';
import { useResponsive } from './responsive/ResponsiveProvider';
import { UserDirectoryPage } from './responsive/wide';
import { ScrollProvider } from './ScrollProvider';
import { useMultiuserEnabled } from './ServerContext';
import { Settings } from './settings';
import { FloatableSidebar } from './sidebar';
import { Titlebar } from './Titlebar';
Expand Down Expand Up @@ -93,6 +98,8 @@ export function FinancesApp() {
'flags.updateNotificationShownForVersion',
);

const multiuserEnabled = useMultiuserEnabled();

useEffect(() => {
// Wait a little bit to make sure the sync button will get the
// sync start event. This can be improved later.
Expand Down Expand Up @@ -281,7 +288,29 @@ export function FinancesApp() {
</WideNotSupported>
}
/>

{multiuserEnabled && (
<Route
path="/user-directory"
element={
<ProtectedRoute
permission={Permissions.ADMINISTRATOR}
element={<UserDirectoryPage />}
/>
}
/>
)}
{multiuserEnabled && (
<Route
path="/user-access"
element={
<ProtectedRoute
permission={Permissions.ADMINISTRATOR}
validateOwner={true}
element={<UserAccessPage />}
/>
}
/>
)}
{/* redirect all other traffic to the budget page */}
<Route path="/*" element={<Navigate to="/budget" replace />} />
</Routes>
Expand Down
Loading
Loading