Skip to content

Commit

Permalink
Merge branch 'master' into enhancement/context_menus/sidebar
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-fidd authored Dec 29, 2024
2 parents 5298937 + 5c577aa commit 8a817f1
Show file tree
Hide file tree
Showing 121 changed files with 5,303 additions and 536 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ packages/crdt/dist

packages/desktop-client/bundle.browser.js
packages/desktop-client/build/
packages/desktop-client/build-electron/
packages/desktop-client/build-stats/
packages/desktop-client/public/kcab/
packages/desktop-client/public/data/
Expand All @@ -16,6 +17,7 @@ packages/desktop-client/src/icons/**/*
packages/desktop-client/test-results/
packages/desktop-client/playwright-report/

packages/desktop-electron/build/
packages/desktop-electron/client-build/
packages/desktop-electron/dist/

Expand Down
4 changes: 4 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ runs:
uses: actions/setup-node@v4
with:
node-version: 18.16.0
- name: Install yarn
run: npm install -g yarn
shell: bash
if: ${{ env.ACT }}
- name: Cache
uses: actions/cache@v4
id: cache
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/i18n-string-extract-master.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Extract and upload i18n strings

on:
push:
branches:
- master
workflow_dispatch:

jobs:
extract-and-upload-i18n-strings:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Configure i18n client
run: |
pip install wlc
- name: Generate i18n strings
run: yarn generate:i18n
- name: Upload i18n strings
run: |
if [[ ! -f packages/desktop-client/locale/en.json ]]; then
echo "File packages/desktop-client/locale/en.json not found. Ensure the file was generated correctly."
exit 1
fi
wlc \
--url https://hosted.weblate.org/api/ \
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
upload \
--author-name "Actual Budget" \
--author-email "[email protected]" \
--method add \
--input packages/desktop-client/locale/en.json \
actualbudget/actual/en
echo "Translations uploaded"
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
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
},
"dependencies": {
"@actual-app/crdt": "workspace:^",
"better-sqlite3": "^9.6.0",
"better-sqlite3": "^11.7.0",
"compare-versions": "^6.1.0",
"node-fetch": "^3.3.2",
"uuid": "^9.0.1"
Expand Down
3 changes: 3 additions & 0 deletions packages/desktop-client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ public/kcab
public/data
public/data-file-index.txt
public/*.wasm

# translations
locale/
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.
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.
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.
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.
2 changes: 1 addition & 1 deletion packages/desktop-client/i18next-parser.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
input: ['src/**/*.{js,jsx,ts,tsx}', '../loot-core/src/**/*.{js,jsx,ts,tsx}'],
output: 'src/locale/$LOCALE.json',
output: 'locale/$LOCALE.json',
locales: ['en'],
sort: true,
keySeparator: false,
Expand Down
48 changes: 48 additions & 0 deletions packages/desktop-client/src/auth/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { createContext, useContext, type ReactNode } from 'react';
import { useSelector } from 'react-redux';

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

import { useServerURL } from '../components/ServerContext';

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 serverUrl = useServerURL();

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

return (
!serverUrl ||
userData?.permission?.toUpperCase() === permission?.toUpperCase()
);
};

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;
};
65 changes: 65 additions & 0 deletions packages/desktop-client/src/auth/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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 is SyncedLocalFile | RemoteFile =>
f.state === 'remote' || f.state === 'synced' || f.state === 'detached',
);
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>
);
};
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',
}
2 changes: 2 additions & 0 deletions packages/desktop-client/src/browser-preload.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ global.Actual = {
});
},

startOAuthServer: () => {},

restartElectronServer: () => {},

openFileDialog: async ({ filters = [] }) => {
Expand Down
22 changes: 21 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,22 @@ function AppInner() {
global.Actual.updateAppMenu(budgetId);
}, [budgetId]);

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

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

0 comments on commit 8a817f1

Please sign in to comment.