@actual-app/sync-server 26.1.0-nightly.20251229 → 26.1.0-nightly.20251231
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 +62 -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 }));
|
|
@@ -10,6 +10,9 @@ const ADMIN_ROLE = 'ADMIN';
|
|
|
10
10
|
const createUser = (userId, userName, role, owner = 0, enabled = 1) => {
|
|
11
11
|
getAccountDb().mutate('INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, ?, ?)', [userId, userName, `${userName} display`, enabled, owner, role]);
|
|
12
12
|
};
|
|
13
|
+
const deleteUser = (userId) => {
|
|
14
|
+
getAccountDb().mutate('DELETE FROM users WHERE id = ?', [userId]);
|
|
15
|
+
};
|
|
13
16
|
describe('/user-get-key', () => {
|
|
14
17
|
it('returns 401 if the user is not authenticated', async () => {
|
|
15
18
|
const res = await request(app).post('/user-get-key');
|
|
@@ -402,6 +405,7 @@ describe('/list-user-files', () => {
|
|
|
402
405
|
});
|
|
403
406
|
it('returns a list of user files for an authenticated user', async () => {
|
|
404
407
|
createUser('fileListAdminId', 'admin', ADMIN_ROLE, 1);
|
|
408
|
+
onTestFinished(() => deleteUser('fileListAdminId'));
|
|
405
409
|
const fileId1 = crypto.randomBytes(16).toString('hex');
|
|
406
410
|
const fileId2 = crypto.randomBytes(16).toString('hex');
|
|
407
411
|
const fileName1 = 'file1.txt';
|
|
@@ -537,6 +541,64 @@ describe('/delete-user-file', () => {
|
|
|
537
541
|
]);
|
|
538
542
|
expect(rows[0].deleted).toBe(1);
|
|
539
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
|
+
});
|
|
540
602
|
});
|
|
541
603
|
describe('/sync', () => {
|
|
542
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.20251231",
|
|
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.20251231",
|
|
33
33
|
"bcrypt": "^6.0.0",
|
|
34
34
|
"better-sqlite3": "^12.4.1",
|
|
35
35
|
"convict": "^6.2.4",
|