@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.
@@ -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
- if (!verifyFileExists(fileId, filesService, res, 'file-not-found')) {
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.20251229",
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.20251229",
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",