@actual-app/sync-server 26.1.0-nightly.20251230 → 26.1.0-nightly.20260101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/app-sync.js +15 -2
- package/build/src/app-sync.test.js +58 -0
- package/package.json +2 -2
package/build/src/app-sync.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolve } from 'node:path';
|
|
|
5
5
|
import { SyncProtoBuf } from '@actual-app/crdt';
|
|
6
6
|
import express from 'express';
|
|
7
7
|
import { v4 as uuidv4 } from 'uuid';
|
|
8
|
-
import { getAccountDb } from './account-db.js';
|
|
8
|
+
import { getAccountDb, isAdmin } from './account-db.js';
|
|
9
9
|
import { FileNotFound } from './app-sync/errors.js';
|
|
10
10
|
import { File, FilesService, FileUpdate, } from './app-sync/services/files-service.js';
|
|
11
11
|
import { validateSyncedFile, validateUploadedFile, } from './app-sync/validation.js';
|
|
@@ -308,7 +308,20 @@ app.post('/delete-user-file', (req, res) => {
|
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
310
|
const filesService = new FilesService(getAccountDb());
|
|
311
|
-
|
|
311
|
+
const file = verifyFileExists(fileId, filesService, res, 'file-not-found');
|
|
312
|
+
if (!file) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// Check if user has permission to delete the file
|
|
316
|
+
const { user_id: userId } = res.locals;
|
|
317
|
+
const isOwner = file.owner === userId;
|
|
318
|
+
const isServerAdmin = isAdmin(userId);
|
|
319
|
+
if (!isOwner && !isServerAdmin) {
|
|
320
|
+
res.status(403).send({
|
|
321
|
+
status: 'error',
|
|
322
|
+
reason: 'forbidden',
|
|
323
|
+
details: 'file-delete-not-allowed',
|
|
324
|
+
});
|
|
312
325
|
return;
|
|
313
326
|
}
|
|
314
327
|
filesService.update(fileId, new FileUpdate({ deleted: true }));
|
|
@@ -541,6 +541,64 @@ describe('/delete-user-file', () => {
|
|
|
541
541
|
]);
|
|
542
542
|
expect(rows[0].deleted).toBe(1);
|
|
543
543
|
});
|
|
544
|
+
it('returns 403 if the user is not the owner and not an admin', async () => {
|
|
545
|
+
const accountDb = getAccountDb();
|
|
546
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
547
|
+
// Insert a file owned by another user
|
|
548
|
+
accountDb.mutate('INSERT OR IGNORE INTO files (id, deleted, owner) VALUES (?, FALSE, ?)', [fileId, 'differentUser']);
|
|
549
|
+
// Try to delete with a non-admin, non-owner user
|
|
550
|
+
const res = await request(app)
|
|
551
|
+
.post('/delete-user-file')
|
|
552
|
+
.set('x-actual-token', 'valid-token-user')
|
|
553
|
+
.send({ fileId });
|
|
554
|
+
expect(res.statusCode).toEqual(403);
|
|
555
|
+
expect(res.body).toEqual({
|
|
556
|
+
status: 'error',
|
|
557
|
+
reason: 'forbidden',
|
|
558
|
+
details: 'file-delete-not-allowed',
|
|
559
|
+
});
|
|
560
|
+
// Verify that the file is NOT deleted
|
|
561
|
+
const rows = accountDb.all('SELECT deleted FROM files WHERE id = ?', [
|
|
562
|
+
fileId,
|
|
563
|
+
]);
|
|
564
|
+
expect(rows[0].deleted).toBe(0);
|
|
565
|
+
});
|
|
566
|
+
it('allows the file owner to delete the file', async () => {
|
|
567
|
+
const accountDb = getAccountDb();
|
|
568
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
569
|
+
// Insert a file owned by genericUser
|
|
570
|
+
accountDb.mutate('INSERT OR IGNORE INTO files (id, deleted, owner) VALUES (?, FALSE, ?)', [fileId, 'genericUser']);
|
|
571
|
+
// Delete with the owner user
|
|
572
|
+
const res = await request(app)
|
|
573
|
+
.post('/delete-user-file')
|
|
574
|
+
.set('x-actual-token', 'valid-token-user')
|
|
575
|
+
.send({ fileId });
|
|
576
|
+
expect(res.statusCode).toEqual(200);
|
|
577
|
+
expect(res.body).toEqual({ status: 'ok' });
|
|
578
|
+
// Verify that the file is deleted
|
|
579
|
+
const rows = accountDb.all('SELECT deleted FROM files WHERE id = ?', [
|
|
580
|
+
fileId,
|
|
581
|
+
]);
|
|
582
|
+
expect(rows[0].deleted).toBe(1);
|
|
583
|
+
});
|
|
584
|
+
it('allows an admin to delete any file', async () => {
|
|
585
|
+
const accountDb = getAccountDb();
|
|
586
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
587
|
+
// Insert a file owned by another user
|
|
588
|
+
accountDb.mutate('INSERT OR IGNORE INTO files (id, deleted, owner) VALUES (?, FALSE, ?)', [fileId, 'someOtherUser']);
|
|
589
|
+
// Delete with an admin user
|
|
590
|
+
const res = await request(app)
|
|
591
|
+
.post('/delete-user-file')
|
|
592
|
+
.set('x-actual-token', 'valid-token-admin')
|
|
593
|
+
.send({ fileId });
|
|
594
|
+
expect(res.statusCode).toEqual(200);
|
|
595
|
+
expect(res.body).toEqual({ status: 'ok' });
|
|
596
|
+
// Verify that the file is deleted
|
|
597
|
+
const rows = accountDb.all('SELECT deleted FROM files WHERE id = ?', [
|
|
598
|
+
fileId,
|
|
599
|
+
]);
|
|
600
|
+
expect(rows[0].deleted).toBe(1);
|
|
601
|
+
});
|
|
544
602
|
});
|
|
545
603
|
describe('/sync', () => {
|
|
546
604
|
it('returns 401 if the user is not authenticated', async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actual-app/sync-server",
|
|
3
|
-
"version": "26.1.0-nightly.
|
|
3
|
+
"version": "26.1.0-nightly.20260101",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "actual syncing server",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@actual-app/crdt": "2.1.0",
|
|
32
|
-
"@actual-app/web": "26.1.0-nightly.
|
|
32
|
+
"@actual-app/web": "26.1.0-nightly.20260101",
|
|
33
33
|
"bcrypt": "^6.0.0",
|
|
34
34
|
"better-sqlite3": "^12.4.1",
|
|
35
35
|
"convict": "^6.2.4",
|