mirror of
https://github.com/actualbudget/actual.git
synced 2026-05-06 07:01:45 -05:00
Compare commits
6 Commits
matiss/chu
...
cursor/syn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6debbf77b6 | ||
|
|
235535a68f | ||
|
|
358549bdd4 | ||
|
|
f97a89dc28 | ||
|
|
a4bd301ec6 | ||
|
|
18072e1d8b |
@@ -74,7 +74,7 @@ export const menuKeybindingText = colorPalette.purple200;
|
||||
export const menuAutoCompleteBackground = colorPalette.gray600;
|
||||
export const menuAutoCompleteBackgroundHover = colorPalette.gray500;
|
||||
export const menuAutoCompleteText = colorPalette.gray100;
|
||||
export const menuAutoCompleteTextHover = colorPalette.green900;
|
||||
export const menuAutoCompleteTextHover = colorPalette.green400;
|
||||
export const menuAutoCompleteTextHeader = colorPalette.purple200;
|
||||
export const menuAutoCompleteItemTextHover = colorPalette.gray50;
|
||||
export const menuAutoCompleteItemText = menuItemText;
|
||||
|
||||
@@ -59,6 +59,42 @@ export function listenForSyncEvent(store: AppStore, queryClient: QueryClient) {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'success' &&
|
||||
event.messageCount != null &&
|
||||
event.messageCount > 20000
|
||||
) {
|
||||
const budgetId = prefs?.id;
|
||||
const dismissedKey = `${budgetId}-flags.syncPerformanceNotificationDismissed`;
|
||||
const dismissed = localStorage.getItem(dismissedKey);
|
||||
|
||||
if (!dismissed || dismissed !== 'true') {
|
||||
store.dispatch(
|
||||
addNotification({
|
||||
notification: {
|
||||
type: 'message',
|
||||
title: t('Sync performance issue'),
|
||||
message: t(
|
||||
'Your budget has {{messageCount}} sync messages which may cause slow performance. Resetting sync will clear old messages and speed things up.',
|
||||
{ messageCount: event.messageCount.toLocaleString() },
|
||||
),
|
||||
sticky: true,
|
||||
id: 'sync-performance',
|
||||
button: {
|
||||
title: t('Reset sync'),
|
||||
action: () => {
|
||||
void store.dispatch(resetSync());
|
||||
},
|
||||
},
|
||||
onClose: () => {
|
||||
localStorage.setItem(dismissedKey, JSON.stringify(true));
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const tables = event.tables;
|
||||
|
||||
if (tables.includes('prefs')) {
|
||||
|
||||
@@ -638,10 +638,18 @@ export const fullSync = once(async function (): Promise<
|
||||
|
||||
const tables = getTablesFromMessages(messages);
|
||||
|
||||
const messageCountResult = db.runQuery<{ count: number }>(
|
||||
'SELECT COUNT(*) as count FROM messages_crdt',
|
||||
[],
|
||||
true,
|
||||
);
|
||||
const messageCount = messageCountResult[0]?.count ?? 0;
|
||||
|
||||
app.events.emit('sync', {
|
||||
type: 'success',
|
||||
tables,
|
||||
syncDisabled: checkSyncingMode('disabled'),
|
||||
messageCount,
|
||||
});
|
||||
return { messages };
|
||||
});
|
||||
|
||||
@@ -80,6 +80,7 @@ export type LocalPrefs = Partial<{
|
||||
'budget.showHiddenCategories': boolean;
|
||||
'budget.startMonth': string;
|
||||
'flags.updateNotificationShownForVersion': string;
|
||||
'flags.syncPerformanceNotificationDismissed': boolean;
|
||||
'schedules.showCompleted': boolean;
|
||||
reportsViewLegend: boolean;
|
||||
reportsViewSummary: boolean;
|
||||
|
||||
@@ -30,6 +30,7 @@ type SyncEvent = {
|
||||
type: 'success';
|
||||
tables: string[];
|
||||
syncDisabled?: boolean;
|
||||
messageCount?: number;
|
||||
}
|
||||
| {
|
||||
type: 'error';
|
||||
|
||||
@@ -366,6 +366,19 @@ describe('/upload-user-file', () => {
|
||||
expect(res.text).toBe('fileId is required');
|
||||
});
|
||||
|
||||
it('returns 400 for invalid fileId format', async () => {
|
||||
const res = await request(app)
|
||||
.post('/upload-user-file')
|
||||
.set('Content-Type', 'application/encrypted-file')
|
||||
.set('x-actual-token', 'valid-token')
|
||||
.set('x-actual-name', 'test-file')
|
||||
.set('x-actual-file-id', 'budget@2026')
|
||||
.send(Buffer.from('file content'));
|
||||
|
||||
expect(res.statusCode).toEqual(400);
|
||||
expect(res.text).toBe('invalid fileId');
|
||||
});
|
||||
|
||||
it('uploads a new file successfully', async () => {
|
||||
const fileId = crypto.randomBytes(16).toString('hex');
|
||||
const fileName = 'test-file.txt';
|
||||
@@ -670,6 +683,16 @@ describe('/download-user-file', () => {
|
||||
expect(res.text).toBe('User or file not found');
|
||||
});
|
||||
|
||||
it('returns 400 for invalid fileId format', async () => {
|
||||
const res = await request(app)
|
||||
.get('/download-user-file')
|
||||
.set('x-actual-token', 'valid-token')
|
||||
.set('x-actual-file-id', 'budget@2026');
|
||||
|
||||
expect(res.statusCode).toEqual(400);
|
||||
expect(res.text).toBe('invalid fileId');
|
||||
});
|
||||
|
||||
it('returns 500 error if the file does not exist on the filesystem', async () => {
|
||||
getAccountDb().mutate(
|
||||
'INSERT INTO files (id, deleted) VALUES (?, FALSE)',
|
||||
|
||||
@@ -49,11 +49,16 @@ app.use(express.json({ limit: `${config.get('upload.fileSizeLimitMB')}mb` }));
|
||||
export { app as handlers };
|
||||
|
||||
const OK_RESPONSE = { status: 'ok' };
|
||||
const FILE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
||||
|
||||
function boolToInt(deleted) {
|
||||
return deleted ? 1 : 0;
|
||||
}
|
||||
|
||||
function isValidFileId(fileId: unknown): fileId is string {
|
||||
return typeof fileId === 'string' && FILE_ID_PATTERN.test(fileId);
|
||||
}
|
||||
|
||||
const verifyFileExists = (fileId, filesService, res, errorObject) => {
|
||||
try {
|
||||
return filesService.get(fileId);
|
||||
@@ -256,6 +261,10 @@ app.post('/upload-user-file', async (req, res) => {
|
||||
res.status(400).send('fileId is required');
|
||||
return;
|
||||
}
|
||||
if (!isValidFileId(fileId)) {
|
||||
res.status(400).send('invalid fileId');
|
||||
return;
|
||||
}
|
||||
|
||||
let groupId = req.headers['x-actual-group-id'] || null;
|
||||
const encryptMeta = req.headers['x-actual-encrypt-meta'] || null;
|
||||
@@ -352,6 +361,10 @@ app.get('/download-user-file', async (req, res) => {
|
||||
res.status(400).send('Single file ID is required');
|
||||
return;
|
||||
}
|
||||
if (!isValidFileId(fileId)) {
|
||||
res.status(400).send('invalid fileId');
|
||||
return;
|
||||
}
|
||||
|
||||
const filesService = new FilesService(getAccountDb());
|
||||
const file = verifyFileExists(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import path, { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
import { load } from 'migrate';
|
||||
|
||||
@@ -30,7 +30,9 @@ export async function run(direction: 'up' | 'down' = 'up'): Promise<void> {
|
||||
for (const f of files
|
||||
.filter(f => f.endsWith('.js') || f.endsWith('.ts'))
|
||||
.sort()) {
|
||||
migrationsModules[f] = await import(path.join(migrationsDir, f));
|
||||
migrationsModules[f] = await import(
|
||||
pathToFileURL(path.join(migrationsDir, f)).href
|
||||
);
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
6
upcoming-release-notes/7048.md
Normal file
6
upcoming-release-notes/7048.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfixes
|
||||
authors: [Juulz]
|
||||
---
|
||||
|
||||
Change menuAutoCompleteTextHover color to green400 in Midnight theme.
|
||||
6
upcoming-release-notes/7067.md
Normal file
6
upcoming-release-notes/7067.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfixes
|
||||
authors: [jfdoming]
|
||||
---
|
||||
|
||||
Validate file IDs for correctness
|
||||
6
upcoming-release-notes/7076.md
Normal file
6
upcoming-release-notes/7076.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [MikesGlitch]
|
||||
---
|
||||
|
||||
Fix server migrations when running on Windows
|
||||
Reference in New Issue
Block a user