@actual-app/sync-server 25.4.0-alpha.0
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/.dockerignore +12 -0
- package/README.md +19 -0
- package/app.js +11 -0
- package/babel.config.json +3 -0
- package/bin/@actual-app/sync-server +55 -0
- package/docker/alpine.Dockerfile +62 -0
- package/docker/ubuntu.Dockerfile +63 -0
- package/docker-compose.yml +29 -0
- package/jest.config.json +19 -0
- package/jest.global-setup.js +101 -0
- package/jest.global-teardown.js +6 -0
- package/migrations/1694360000000-create-folders.js +25 -0
- package/migrations/1694360479680-create-account-db.js +30 -0
- package/migrations/1694362247011-create-secret-table.js +16 -0
- package/migrations/1702667624000-rename-nordigen-secrets.js +19 -0
- package/migrations/1718889148000-openid.js +41 -0
- package/migrations/1719409568000-multiuser.js +116 -0
- package/package.json +64 -0
- package/src/account-db.js +239 -0
- package/src/accounts/openid.js +361 -0
- package/src/accounts/password.js +149 -0
- package/src/app-account.js +155 -0
- package/src/app-admin.js +410 -0
- package/src/app-admin.test.js +381 -0
- package/src/app-gocardless/README.md +198 -0
- package/src/app-gocardless/app-gocardless.js +274 -0
- package/src/app-gocardless/bank-factory.js +91 -0
- package/src/app-gocardless/banks/abanca_caglesmm.js +22 -0
- package/src/app-gocardless/banks/abnamro_abnanl2a.js +57 -0
- package/src/app-gocardless/banks/american_express_aesudef1.js +40 -0
- package/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +31 -0
- package/src/app-gocardless/banks/bank.interface.ts +51 -0
- package/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +39 -0
- package/src/app-gocardless/banks/bankinter_bkbkesmm.js +24 -0
- package/src/app-gocardless/banks/belfius_gkccbebb.js +17 -0
- package/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +61 -0
- package/src/app-gocardless/banks/bnp_be_gebabebb.js +73 -0
- package/src/app-gocardless/banks/cbc_cregbebb.js +34 -0
- package/src/app-gocardless/banks/commerzbank_cobadeff.js +51 -0
- package/src/app-gocardless/banks/danskebank_dabno22.js +39 -0
- package/src/app-gocardless/banks/direkt_heladef1822.js +18 -0
- package/src/app-gocardless/banks/easybank_bawaatww.js +50 -0
- package/src/app-gocardless/banks/entercard_swednokk.js +40 -0
- package/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +46 -0
- package/src/app-gocardless/banks/hype_hyeeit22.js +74 -0
- package/src/app-gocardless/banks/ing_ingbrobu.js +70 -0
- package/src/app-gocardless/banks/ing_ingddeff.js +47 -0
- package/src/app-gocardless/banks/ing_pl_ingbplpw.js +46 -0
- package/src/app-gocardless/banks/integration-bank.js +115 -0
- package/src/app-gocardless/banks/isybank_itbbitmm.js +18 -0
- package/src/app-gocardless/banks/kbc_kredbebb.js +33 -0
- package/src/app-gocardless/banks/lhv-lhvbee22.js +36 -0
- package/src/app-gocardless/banks/mbank_retail_brexplpw.js +56 -0
- package/src/app-gocardless/banks/nationwide_naiagb21.js +46 -0
- package/src/app-gocardless/banks/nbg_ethngraaxxx.js +51 -0
- package/src/app-gocardless/banks/norwegian_xx_norwnok1.js +74 -0
- package/src/app-gocardless/banks/revolut_revolt21.js +37 -0
- package/src/app-gocardless/banks/sandboxfinance_sfin0000.js +28 -0
- package/src/app-gocardless/banks/seb_kort_bank_ab.js +58 -0
- package/src/app-gocardless/banks/seb_privat.js +29 -0
- package/src/app-gocardless/banks/sparnord_spnodk22.js +24 -0
- package/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +61 -0
- package/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +30 -0
- package/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +19 -0
- package/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +50 -0
- package/src/app-gocardless/banks/swedbank_habalv22.js +47 -0
- package/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +21 -0
- package/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +61 -0
- package/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +53 -0
- package/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +22 -0
- package/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +34 -0
- package/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +110 -0
- package/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +54 -0
- package/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +206 -0
- package/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +302 -0
- package/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +202 -0
- package/src/app-gocardless/banks/tests/integration_bank.spec.js +158 -0
- package/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +38 -0
- package/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +68 -0
- package/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +171 -0
- package/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +105 -0
- package/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +48 -0
- package/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +42 -0
- package/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +133 -0
- package/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +256 -0
- package/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +102 -0
- package/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +57 -0
- package/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +54 -0
- package/src/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js +36 -0
- package/src/app-gocardless/banks/virgin_nrnbgb22.js +39 -0
- package/src/app-gocardless/errors.js +84 -0
- package/src/app-gocardless/gocardless-node.types.ts +497 -0
- package/src/app-gocardless/gocardless.types.ts +93 -0
- package/src/app-gocardless/link.html +18 -0
- package/src/app-gocardless/services/gocardless-service.js +620 -0
- package/src/app-gocardless/services/tests/fixtures.js +181 -0
- package/src/app-gocardless/services/tests/gocardless-service.spec.js +537 -0
- package/src/app-gocardless/tests/bank-factory.spec.js +20 -0
- package/src/app-gocardless/tests/utils.spec.js +162 -0
- package/src/app-gocardless/util/handle-error.js +16 -0
- package/src/app-gocardless/utils.js +45 -0
- package/src/app-openid.js +108 -0
- package/src/app-pluggyai/app-pluggyai.js +215 -0
- package/src/app-pluggyai/pluggyai-service.js +120 -0
- package/src/app-secrets.js +61 -0
- package/src/app-simplefin/app-simplefin.js +418 -0
- package/src/app-sync/errors.js +13 -0
- package/src/app-sync/services/files-service.js +243 -0
- package/src/app-sync/tests/services/files-service.test.js +250 -0
- package/src/app-sync/validation.js +77 -0
- package/src/app-sync.js +391 -0
- package/src/app-sync.test.js +877 -0
- package/src/app.js +145 -0
- package/src/config-types.ts +44 -0
- package/src/db.js +58 -0
- package/src/load-config.js +307 -0
- package/src/migrations.js +36 -0
- package/src/run-migrations.js +8 -0
- package/src/scripts/disable-openid.js +44 -0
- package/src/scripts/enable-openid.js +53 -0
- package/src/scripts/health-check.js +23 -0
- package/src/scripts/reset-password.js +51 -0
- package/src/secrets.test.js +83 -0
- package/src/services/secrets-service.js +94 -0
- package/src/services/user-service.js +272 -0
- package/src/sql/messages.sql +9 -0
- package/src/sync-simple.js +95 -0
- package/src/util/hash.js +5 -0
- package/src/util/middlewares.js +62 -0
- package/src/util/paths.js +13 -0
- package/src/util/payee-name.js +45 -0
- package/src/util/prompt.js +88 -0
- package/src/util/title/index.js +59 -0
- package/src/util/title/lower-case.js +93 -0
- package/src/util/title/specials.js +21 -0
- package/src/util/validate-user.js +68 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { getAccountDb, isAdmin } from '../../account-db.js';
|
|
2
|
+
import { FileNotFound, GenericFileError } from '../errors.js';
|
|
3
|
+
|
|
4
|
+
class FileBase {
|
|
5
|
+
constructor(
|
|
6
|
+
name,
|
|
7
|
+
groupId,
|
|
8
|
+
encryptSalt,
|
|
9
|
+
encryptTest,
|
|
10
|
+
encryptKeyId,
|
|
11
|
+
encryptMeta,
|
|
12
|
+
syncVersion,
|
|
13
|
+
deleted,
|
|
14
|
+
owner,
|
|
15
|
+
) {
|
|
16
|
+
this.name = name;
|
|
17
|
+
this.groupId = groupId;
|
|
18
|
+
this.encryptSalt = encryptSalt;
|
|
19
|
+
this.encryptTest = encryptTest;
|
|
20
|
+
this.encryptKeyId = encryptKeyId;
|
|
21
|
+
this.encryptMeta = encryptMeta;
|
|
22
|
+
this.syncVersion = syncVersion;
|
|
23
|
+
this.deleted = typeof deleted === 'boolean' ? deleted : Boolean(deleted);
|
|
24
|
+
this.owner = owner;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class File extends FileBase {
|
|
29
|
+
constructor({
|
|
30
|
+
id,
|
|
31
|
+
name = null,
|
|
32
|
+
groupId = null,
|
|
33
|
+
encryptSalt = null,
|
|
34
|
+
encryptTest = null,
|
|
35
|
+
encryptKeyId = null,
|
|
36
|
+
encryptMeta = null,
|
|
37
|
+
syncVersion = null,
|
|
38
|
+
deleted = false,
|
|
39
|
+
owner = null,
|
|
40
|
+
}) {
|
|
41
|
+
super(
|
|
42
|
+
name,
|
|
43
|
+
groupId,
|
|
44
|
+
encryptSalt,
|
|
45
|
+
encryptTest,
|
|
46
|
+
encryptKeyId,
|
|
47
|
+
encryptMeta,
|
|
48
|
+
syncVersion,
|
|
49
|
+
deleted,
|
|
50
|
+
owner,
|
|
51
|
+
);
|
|
52
|
+
this.id = id;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Represents a file update. Will only update the fields that are defined.
|
|
58
|
+
* @class
|
|
59
|
+
* @extends FileBase
|
|
60
|
+
*/
|
|
61
|
+
class FileUpdate extends FileBase {
|
|
62
|
+
constructor({
|
|
63
|
+
name = undefined,
|
|
64
|
+
groupId = undefined,
|
|
65
|
+
encryptSalt = undefined,
|
|
66
|
+
encryptTest = undefined,
|
|
67
|
+
encryptKeyId = undefined,
|
|
68
|
+
encryptMeta = undefined,
|
|
69
|
+
syncVersion = undefined,
|
|
70
|
+
deleted = undefined,
|
|
71
|
+
owner = undefined,
|
|
72
|
+
}) {
|
|
73
|
+
super(
|
|
74
|
+
name,
|
|
75
|
+
groupId,
|
|
76
|
+
encryptSalt,
|
|
77
|
+
encryptTest,
|
|
78
|
+
encryptKeyId,
|
|
79
|
+
encryptMeta,
|
|
80
|
+
syncVersion,
|
|
81
|
+
deleted,
|
|
82
|
+
owner,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const boolToInt = bool => {
|
|
88
|
+
return bool ? 1 : 0;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
class FilesService {
|
|
92
|
+
constructor(accountDb) {
|
|
93
|
+
this.accountDb = accountDb;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get(fileId) {
|
|
97
|
+
const rawFile = this.getRaw(fileId);
|
|
98
|
+
if (!rawFile || (rawFile && rawFile.deleted)) {
|
|
99
|
+
throw new FileNotFound();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return this.validate(rawFile);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
set(file) {
|
|
106
|
+
const deletedInt = boolToInt(file.deleted);
|
|
107
|
+
this.accountDb.mutate(
|
|
108
|
+
'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_salt, encrypt_test, encrypt_keyid, deleted, owner) VALUES (?, ?, ?, ?, ?, ?, ?, ? ,?, ?)',
|
|
109
|
+
[
|
|
110
|
+
file.id,
|
|
111
|
+
file.groupId,
|
|
112
|
+
file.syncVersion.toString(),
|
|
113
|
+
file.name,
|
|
114
|
+
file.encryptMeta,
|
|
115
|
+
file.encryptSalt,
|
|
116
|
+
file.encrypt_test,
|
|
117
|
+
file.encrypt_keyid,
|
|
118
|
+
deletedInt,
|
|
119
|
+
file.owner,
|
|
120
|
+
],
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
find({ userId, limit = 1000 }) {
|
|
125
|
+
const canSeeAll = isAdmin(userId);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
canSeeAll
|
|
129
|
+
? this.accountDb.all('SELECT * FROM files WHERE deleted = 0 LIMIT ?', [
|
|
130
|
+
limit,
|
|
131
|
+
])
|
|
132
|
+
: this.accountDb.all(
|
|
133
|
+
`SELECT files.*
|
|
134
|
+
FROM files
|
|
135
|
+
WHERE files.owner = ? and deleted = 0
|
|
136
|
+
UNION
|
|
137
|
+
SELECT files.*
|
|
138
|
+
FROM files
|
|
139
|
+
JOIN user_access
|
|
140
|
+
ON user_access.file_id = files.id
|
|
141
|
+
AND user_access.user_id = ?
|
|
142
|
+
WHERE files.deleted = 0 LIMIT ?`,
|
|
143
|
+
[userId, userId, limit],
|
|
144
|
+
)
|
|
145
|
+
).map(this.validate);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
findUsersWithAccess(fileId) {
|
|
149
|
+
const userAccess =
|
|
150
|
+
this.accountDb.all(
|
|
151
|
+
`SELECT UA.user_id as userId, users.display_name displayName, users.user_name userName
|
|
152
|
+
FROM files
|
|
153
|
+
JOIN user_access UA ON UA.file_id = files.id
|
|
154
|
+
JOIN users on users.id = UA.user_id
|
|
155
|
+
WHERE files.id = ?
|
|
156
|
+
UNION ALL
|
|
157
|
+
SELECT users.id, users.display_name, users.user_name
|
|
158
|
+
FROM files
|
|
159
|
+
JOIN users on users.id = files.owner
|
|
160
|
+
WHERE files.id = ?
|
|
161
|
+
`,
|
|
162
|
+
[fileId, fileId],
|
|
163
|
+
) || [];
|
|
164
|
+
|
|
165
|
+
return userAccess;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
update(id, fileUpdate) {
|
|
169
|
+
let query = 'UPDATE files SET';
|
|
170
|
+
const params = [];
|
|
171
|
+
const updates = [];
|
|
172
|
+
|
|
173
|
+
if (fileUpdate.name !== undefined) {
|
|
174
|
+
updates.push('name = ?');
|
|
175
|
+
params.push(fileUpdate.name);
|
|
176
|
+
}
|
|
177
|
+
if (fileUpdate.groupId !== undefined) {
|
|
178
|
+
updates.push('group_id = ?');
|
|
179
|
+
params.push(fileUpdate.groupId);
|
|
180
|
+
}
|
|
181
|
+
if (fileUpdate.encryptSalt !== undefined) {
|
|
182
|
+
updates.push('encrypt_salt = ?');
|
|
183
|
+
params.push(fileUpdate.encryptSalt);
|
|
184
|
+
}
|
|
185
|
+
if (fileUpdate.encryptTest !== undefined) {
|
|
186
|
+
updates.push('encrypt_test = ?');
|
|
187
|
+
params.push(fileUpdate.encryptTest);
|
|
188
|
+
}
|
|
189
|
+
if (fileUpdate.encryptKeyId !== undefined) {
|
|
190
|
+
updates.push('encrypt_keyid = ?');
|
|
191
|
+
params.push(fileUpdate.encryptKeyId);
|
|
192
|
+
}
|
|
193
|
+
if (fileUpdate.encryptMeta !== undefined) {
|
|
194
|
+
updates.push('encrypt_meta = ?');
|
|
195
|
+
params.push(fileUpdate.encryptMeta);
|
|
196
|
+
}
|
|
197
|
+
if (fileUpdate.syncVersion !== undefined) {
|
|
198
|
+
updates.push('sync_version = ?');
|
|
199
|
+
params.push(fileUpdate.syncVersion);
|
|
200
|
+
}
|
|
201
|
+
if (fileUpdate.deleted !== undefined) {
|
|
202
|
+
updates.push('deleted = ?');
|
|
203
|
+
params.push(boolToInt(fileUpdate.deleted));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (updates.length > 0) {
|
|
207
|
+
query += ' ' + updates.join(', ') + ' WHERE id = ?';
|
|
208
|
+
params.push(id);
|
|
209
|
+
|
|
210
|
+
const res = this.accountDb.mutate(query, params);
|
|
211
|
+
|
|
212
|
+
if (res.changes !== 1) {
|
|
213
|
+
throw new GenericFileError('Could not update File', { id });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Return the modified object
|
|
218
|
+
return this.validate(this.getRaw(id));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getRaw(fileId) {
|
|
222
|
+
return this.accountDb.first(`SELECT * FROM files WHERE id = ?`, [fileId]);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
validate(rawFile) {
|
|
226
|
+
return new File({
|
|
227
|
+
id: rawFile.id,
|
|
228
|
+
name: rawFile.name,
|
|
229
|
+
groupId: rawFile.group_id,
|
|
230
|
+
encryptSalt: rawFile.encrypt_salt,
|
|
231
|
+
encryptTest: rawFile.encrypt_test,
|
|
232
|
+
encryptKeyId: rawFile.encrypt_keyid,
|
|
233
|
+
encryptMeta: rawFile.encrypt_meta,
|
|
234
|
+
syncVersion: rawFile.sync_version,
|
|
235
|
+
deleted: Boolean(rawFile.deleted),
|
|
236
|
+
owner: rawFile.owner,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const filesService = new FilesService(getAccountDb());
|
|
242
|
+
|
|
243
|
+
export { filesService, FilesService, File, FileUpdate };
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { getAccountDb } from '../../../account-db.js';
|
|
4
|
+
import { FileNotFound } from '../../errors.js';
|
|
5
|
+
import {
|
|
6
|
+
FilesService,
|
|
7
|
+
File,
|
|
8
|
+
FileUpdate,
|
|
9
|
+
} from '../../services/files-service.js'; // Adjust the path as necessary
|
|
10
|
+
describe('FilesService', () => {
|
|
11
|
+
let filesService;
|
|
12
|
+
let accountDb;
|
|
13
|
+
|
|
14
|
+
const insertToyExampleData = () => {
|
|
15
|
+
accountDb.mutate(
|
|
16
|
+
'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_salt, encrypt_test, encrypt_keyid, deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
17
|
+
[
|
|
18
|
+
'1',
|
|
19
|
+
'group1',
|
|
20
|
+
1,
|
|
21
|
+
'file1',
|
|
22
|
+
'{"key":"value"}',
|
|
23
|
+
'salt',
|
|
24
|
+
'test',
|
|
25
|
+
'keyid',
|
|
26
|
+
0,
|
|
27
|
+
],
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const clearDatabase = () => {
|
|
32
|
+
accountDb.mutate('DELETE FROM user_access');
|
|
33
|
+
accountDb.mutate('DELETE FROM files');
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
beforeAll(done => {
|
|
37
|
+
accountDb = getAccountDb();
|
|
38
|
+
filesService = new FilesService(accountDb);
|
|
39
|
+
done();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
beforeEach(done => {
|
|
43
|
+
insertToyExampleData();
|
|
44
|
+
done();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(done => {
|
|
48
|
+
clearDatabase();
|
|
49
|
+
done();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('get should return a file', () => {
|
|
53
|
+
const file = filesService.get('1');
|
|
54
|
+
const expectedFile = new File({
|
|
55
|
+
id: '1',
|
|
56
|
+
groupId: 'group1',
|
|
57
|
+
syncVersion: 1,
|
|
58
|
+
name: 'file1',
|
|
59
|
+
encryptMeta: '{"key":"value"}',
|
|
60
|
+
encryptSalt: 'salt',
|
|
61
|
+
encryptTest: 'test',
|
|
62
|
+
encryptKeyId: 'keyid',
|
|
63
|
+
deleted: false,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(file).toEqual(expectedFile);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('get should throw FileNotFound if file is deleted or does not exist', () => {
|
|
70
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
71
|
+
accountDb.mutate(
|
|
72
|
+
'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_salt, encrypt_test, encrypt_keyid, deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
73
|
+
[
|
|
74
|
+
fileId,
|
|
75
|
+
'group1',
|
|
76
|
+
1,
|
|
77
|
+
'file1',
|
|
78
|
+
'{"key":"value"}',
|
|
79
|
+
'salt',
|
|
80
|
+
'test',
|
|
81
|
+
'keyid',
|
|
82
|
+
1,
|
|
83
|
+
],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(() => {
|
|
87
|
+
filesService.get(fileId);
|
|
88
|
+
}).toThrow(FileNotFound);
|
|
89
|
+
|
|
90
|
+
expect(() => {
|
|
91
|
+
filesService.get(crypto.randomBytes(16).toString('hex'));
|
|
92
|
+
}).toThrow(FileNotFound);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test.each([true, false])(
|
|
96
|
+
'set should insert a new file with deleted: %p',
|
|
97
|
+
deleted => {
|
|
98
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
99
|
+
const newFile = new File({
|
|
100
|
+
id: fileId,
|
|
101
|
+
groupId: 'group2',
|
|
102
|
+
syncVersion: 1,
|
|
103
|
+
name: 'file2',
|
|
104
|
+
encryptMeta: '{"key":"value2"}',
|
|
105
|
+
deleted,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
filesService.set(newFile);
|
|
109
|
+
|
|
110
|
+
const file = filesService.validate(filesService.getRaw(fileId));
|
|
111
|
+
const expectedFile = new File({
|
|
112
|
+
id: fileId,
|
|
113
|
+
groupId: 'group2',
|
|
114
|
+
syncVersion: 1,
|
|
115
|
+
name: 'file2',
|
|
116
|
+
encryptMeta: '{"key":"value2"}',
|
|
117
|
+
encryptSalt: null, // default value
|
|
118
|
+
encryptTest: null, // default value
|
|
119
|
+
encryptKeyId: null, // default value
|
|
120
|
+
deleted,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(file).toEqual(expectedFile);
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
test('find should return a list of files', () => {
|
|
128
|
+
const files = filesService.find({ userId: 'genericAdmin' });
|
|
129
|
+
expect(files.length).toBe(1);
|
|
130
|
+
expect(files[0]).toEqual(
|
|
131
|
+
new File({
|
|
132
|
+
id: '1',
|
|
133
|
+
groupId: 'group1',
|
|
134
|
+
syncVersion: 1,
|
|
135
|
+
name: 'file1',
|
|
136
|
+
encryptMeta: '{"key":"value"}',
|
|
137
|
+
encryptSalt: 'salt',
|
|
138
|
+
encryptTest: 'test',
|
|
139
|
+
encryptKeyId: 'keyid',
|
|
140
|
+
deleted: false,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('find should respect the limit parameter', () => {
|
|
146
|
+
filesService.set(
|
|
147
|
+
new File({
|
|
148
|
+
id: crypto.randomBytes(16).toString('hex'),
|
|
149
|
+
groupId: 'group2',
|
|
150
|
+
syncVersion: 1,
|
|
151
|
+
name: 'file2',
|
|
152
|
+
encryptMeta: '{"key":"value2"}',
|
|
153
|
+
deleted: false,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
// Make sure that the file was inserted
|
|
157
|
+
const allFiles = filesService.find({ userId: 'genericAdmin' });
|
|
158
|
+
expect(allFiles.length).toBe(2);
|
|
159
|
+
|
|
160
|
+
// Limit the number of files returned
|
|
161
|
+
const limitedFiles = filesService.find({
|
|
162
|
+
userId: 'genericAdmin',
|
|
163
|
+
limit: 1,
|
|
164
|
+
});
|
|
165
|
+
expect(limitedFiles.length).toBe(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('update should modify all attributes of an existing file', () => {
|
|
169
|
+
const fileUpdate = new FileUpdate({
|
|
170
|
+
name: 'updatedFile1',
|
|
171
|
+
groupId: 'updatedGroup1',
|
|
172
|
+
encryptSalt: 'updatedSalt',
|
|
173
|
+
encryptTest: 'updatedTest',
|
|
174
|
+
encryptKeyId: 'updatedKeyId',
|
|
175
|
+
encryptMeta: '{"key":"updatedValue"}',
|
|
176
|
+
syncVersion: 2,
|
|
177
|
+
deleted: true,
|
|
178
|
+
});
|
|
179
|
+
const updatedFile = filesService.update('1', fileUpdate);
|
|
180
|
+
|
|
181
|
+
expect(updatedFile).toEqual(
|
|
182
|
+
new File({
|
|
183
|
+
id: '1',
|
|
184
|
+
name: 'updatedFile1',
|
|
185
|
+
groupId: 'updatedGroup1',
|
|
186
|
+
encryptSalt: 'updatedSalt',
|
|
187
|
+
encryptTest: 'updatedTest',
|
|
188
|
+
encryptMeta: '{"key":"updatedValue"}',
|
|
189
|
+
encryptKeyId: 'updatedKeyId',
|
|
190
|
+
syncVersion: 2,
|
|
191
|
+
deleted: true,
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('find should return only files accessible to the user', () => {
|
|
197
|
+
filesService.set(
|
|
198
|
+
new File({
|
|
199
|
+
id: crypto.randomBytes(16).toString('hex'),
|
|
200
|
+
groupId: 'group2',
|
|
201
|
+
syncVersion: 1,
|
|
202
|
+
name: 'file2',
|
|
203
|
+
encryptMeta: '{"key":"value2"}',
|
|
204
|
+
deleted: false,
|
|
205
|
+
owner: 'genericAdmin',
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
filesService.set(
|
|
210
|
+
new File({
|
|
211
|
+
id: crypto.randomBytes(16).toString('hex'),
|
|
212
|
+
groupId: 'group2',
|
|
213
|
+
syncVersion: 1,
|
|
214
|
+
name: 'file2',
|
|
215
|
+
encryptMeta: '{"key":"value2"}',
|
|
216
|
+
deleted: false,
|
|
217
|
+
owner: 'genericUser',
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
expect(filesService.find({ userId: 'genericUser' })).toHaveLength(1);
|
|
222
|
+
expect(
|
|
223
|
+
filesService.find({ userId: 'genericAdmin' }).length,
|
|
224
|
+
).toBeGreaterThan(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test.each([['update-group', null]])(
|
|
228
|
+
'update should modify a single attribute with groupId = $groupId',
|
|
229
|
+
newGroupId => {
|
|
230
|
+
const fileUpdate = new FileUpdate({
|
|
231
|
+
groupId: newGroupId,
|
|
232
|
+
});
|
|
233
|
+
const updatedFile = filesService.update('1', fileUpdate);
|
|
234
|
+
|
|
235
|
+
expect(updatedFile).toEqual(
|
|
236
|
+
new File({
|
|
237
|
+
id: '1',
|
|
238
|
+
name: 'file1',
|
|
239
|
+
groupId: newGroupId,
|
|
240
|
+
syncVersion: 1,
|
|
241
|
+
encryptMeta: '{"key":"value"}',
|
|
242
|
+
encryptSalt: 'salt',
|
|
243
|
+
encryptTest: 'test',
|
|
244
|
+
encryptKeyId: 'keyid',
|
|
245
|
+
deleted: false,
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// This is a version representing the internal format of sync
|
|
2
|
+
// messages. When this changes, all sync files need to be reset. We
|
|
3
|
+
// will check this version when syncing and notify the user if they
|
|
4
|
+
// need to reset.
|
|
5
|
+
const SYNC_FORMAT_VERSION = 2;
|
|
6
|
+
|
|
7
|
+
const validateSyncedFile = (groupId, keyId, currentFile) => {
|
|
8
|
+
if (
|
|
9
|
+
currentFile.syncVersion == null ||
|
|
10
|
+
currentFile.syncVersion < SYNC_FORMAT_VERSION
|
|
11
|
+
) {
|
|
12
|
+
return 'file-old-version';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// When resetting sync state, something went wrong. There is no
|
|
16
|
+
// group id and it's awaiting a file to be uploaded.
|
|
17
|
+
if (currentFile.groupId == null) {
|
|
18
|
+
return 'file-needs-upload';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check to make sure the uploaded file is valid and has been
|
|
22
|
+
// encrypted with the same key it is registered with (this might
|
|
23
|
+
// be wrong if there was an error during the key creation
|
|
24
|
+
// process)
|
|
25
|
+
const uploadedKeyId = currentFile.encryptMeta
|
|
26
|
+
? JSON.parse(currentFile.encryptMeta).keyId
|
|
27
|
+
: null;
|
|
28
|
+
if (uploadedKeyId !== currentFile.encryptKeyId) {
|
|
29
|
+
return 'file-key-mismatch';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The changes being synced are part of an old group, which
|
|
33
|
+
// means the file has been reset. User needs to re-download.
|
|
34
|
+
if (groupId !== currentFile.groupId) {
|
|
35
|
+
return 'file-has-reset';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The data is encrypted with a different key which is
|
|
39
|
+
// unacceptable. We can't accept these changes. Reject them and
|
|
40
|
+
// tell the user that they need to generate the correct key
|
|
41
|
+
// (which necessitates a sync reset so they need to re-download).
|
|
42
|
+
if (keyId !== currentFile.encryptKeyId) {
|
|
43
|
+
return 'file-has-new-key';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const validateUploadedFile = (groupId, keyId, currentFile) => {
|
|
50
|
+
if (!currentFile) {
|
|
51
|
+
// File is new, so no need to validate
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
// The uploading file is part of an old group, so reject
|
|
55
|
+
// it. All of its internal sync state is invalid because its
|
|
56
|
+
// old. The sync state has been reset, so user needs to
|
|
57
|
+
// either reset again or download from the current group.
|
|
58
|
+
if (groupId !== currentFile.groupId) {
|
|
59
|
+
return 'file-has-reset';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The key that the file is encrypted with is different than
|
|
63
|
+
// the current registered key. All data must always be
|
|
64
|
+
// encrypted with the registered key for consistency. Key
|
|
65
|
+
// changes always necessitate a sync reset, which means this
|
|
66
|
+
// upload is trying to overwrite another reset. That might
|
|
67
|
+
// be be fine, but since we definitely cannot accept a file
|
|
68
|
+
// encrypted with the wrong key, we bail and suggest the
|
|
69
|
+
// user download the latest file.
|
|
70
|
+
if (keyId !== currentFile.encryptKeyId) {
|
|
71
|
+
return 'file-has-new-key';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export { validateSyncedFile, validateUploadedFile };
|