@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.
@@ -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 }));
@@ -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.20251230",
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.20251230",
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",