@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.
Files changed (137) hide show
  1. package/.dockerignore +12 -0
  2. package/README.md +19 -0
  3. package/app.js +11 -0
  4. package/babel.config.json +3 -0
  5. package/bin/@actual-app/sync-server +55 -0
  6. package/docker/alpine.Dockerfile +62 -0
  7. package/docker/ubuntu.Dockerfile +63 -0
  8. package/docker-compose.yml +29 -0
  9. package/jest.config.json +19 -0
  10. package/jest.global-setup.js +101 -0
  11. package/jest.global-teardown.js +6 -0
  12. package/migrations/1694360000000-create-folders.js +25 -0
  13. package/migrations/1694360479680-create-account-db.js +30 -0
  14. package/migrations/1694362247011-create-secret-table.js +16 -0
  15. package/migrations/1702667624000-rename-nordigen-secrets.js +19 -0
  16. package/migrations/1718889148000-openid.js +41 -0
  17. package/migrations/1719409568000-multiuser.js +116 -0
  18. package/package.json +64 -0
  19. package/src/account-db.js +239 -0
  20. package/src/accounts/openid.js +361 -0
  21. package/src/accounts/password.js +149 -0
  22. package/src/app-account.js +155 -0
  23. package/src/app-admin.js +410 -0
  24. package/src/app-admin.test.js +381 -0
  25. package/src/app-gocardless/README.md +198 -0
  26. package/src/app-gocardless/app-gocardless.js +274 -0
  27. package/src/app-gocardless/bank-factory.js +91 -0
  28. package/src/app-gocardless/banks/abanca_caglesmm.js +22 -0
  29. package/src/app-gocardless/banks/abnamro_abnanl2a.js +57 -0
  30. package/src/app-gocardless/banks/american_express_aesudef1.js +40 -0
  31. package/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +31 -0
  32. package/src/app-gocardless/banks/bank.interface.ts +51 -0
  33. package/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +39 -0
  34. package/src/app-gocardless/banks/bankinter_bkbkesmm.js +24 -0
  35. package/src/app-gocardless/banks/belfius_gkccbebb.js +17 -0
  36. package/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +61 -0
  37. package/src/app-gocardless/banks/bnp_be_gebabebb.js +73 -0
  38. package/src/app-gocardless/banks/cbc_cregbebb.js +34 -0
  39. package/src/app-gocardless/banks/commerzbank_cobadeff.js +51 -0
  40. package/src/app-gocardless/banks/danskebank_dabno22.js +39 -0
  41. package/src/app-gocardless/banks/direkt_heladef1822.js +18 -0
  42. package/src/app-gocardless/banks/easybank_bawaatww.js +50 -0
  43. package/src/app-gocardless/banks/entercard_swednokk.js +40 -0
  44. package/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +46 -0
  45. package/src/app-gocardless/banks/hype_hyeeit22.js +74 -0
  46. package/src/app-gocardless/banks/ing_ingbrobu.js +70 -0
  47. package/src/app-gocardless/banks/ing_ingddeff.js +47 -0
  48. package/src/app-gocardless/banks/ing_pl_ingbplpw.js +46 -0
  49. package/src/app-gocardless/banks/integration-bank.js +115 -0
  50. package/src/app-gocardless/banks/isybank_itbbitmm.js +18 -0
  51. package/src/app-gocardless/banks/kbc_kredbebb.js +33 -0
  52. package/src/app-gocardless/banks/lhv-lhvbee22.js +36 -0
  53. package/src/app-gocardless/banks/mbank_retail_brexplpw.js +56 -0
  54. package/src/app-gocardless/banks/nationwide_naiagb21.js +46 -0
  55. package/src/app-gocardless/banks/nbg_ethngraaxxx.js +51 -0
  56. package/src/app-gocardless/banks/norwegian_xx_norwnok1.js +74 -0
  57. package/src/app-gocardless/banks/revolut_revolt21.js +37 -0
  58. package/src/app-gocardless/banks/sandboxfinance_sfin0000.js +28 -0
  59. package/src/app-gocardless/banks/seb_kort_bank_ab.js +58 -0
  60. package/src/app-gocardless/banks/seb_privat.js +29 -0
  61. package/src/app-gocardless/banks/sparnord_spnodk22.js +24 -0
  62. package/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +61 -0
  63. package/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +30 -0
  64. package/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +19 -0
  65. package/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +50 -0
  66. package/src/app-gocardless/banks/swedbank_habalv22.js +47 -0
  67. package/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +21 -0
  68. package/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +61 -0
  69. package/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +53 -0
  70. package/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +22 -0
  71. package/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +34 -0
  72. package/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +110 -0
  73. package/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +54 -0
  74. package/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +206 -0
  75. package/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +302 -0
  76. package/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +202 -0
  77. package/src/app-gocardless/banks/tests/integration_bank.spec.js +158 -0
  78. package/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +38 -0
  79. package/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +68 -0
  80. package/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +171 -0
  81. package/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +105 -0
  82. package/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +48 -0
  83. package/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +42 -0
  84. package/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +133 -0
  85. package/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +256 -0
  86. package/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +102 -0
  87. package/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +57 -0
  88. package/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +54 -0
  89. package/src/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js +36 -0
  90. package/src/app-gocardless/banks/virgin_nrnbgb22.js +39 -0
  91. package/src/app-gocardless/errors.js +84 -0
  92. package/src/app-gocardless/gocardless-node.types.ts +497 -0
  93. package/src/app-gocardless/gocardless.types.ts +93 -0
  94. package/src/app-gocardless/link.html +18 -0
  95. package/src/app-gocardless/services/gocardless-service.js +620 -0
  96. package/src/app-gocardless/services/tests/fixtures.js +181 -0
  97. package/src/app-gocardless/services/tests/gocardless-service.spec.js +537 -0
  98. package/src/app-gocardless/tests/bank-factory.spec.js +20 -0
  99. package/src/app-gocardless/tests/utils.spec.js +162 -0
  100. package/src/app-gocardless/util/handle-error.js +16 -0
  101. package/src/app-gocardless/utils.js +45 -0
  102. package/src/app-openid.js +108 -0
  103. package/src/app-pluggyai/app-pluggyai.js +215 -0
  104. package/src/app-pluggyai/pluggyai-service.js +120 -0
  105. package/src/app-secrets.js +61 -0
  106. package/src/app-simplefin/app-simplefin.js +418 -0
  107. package/src/app-sync/errors.js +13 -0
  108. package/src/app-sync/services/files-service.js +243 -0
  109. package/src/app-sync/tests/services/files-service.test.js +250 -0
  110. package/src/app-sync/validation.js +77 -0
  111. package/src/app-sync.js +391 -0
  112. package/src/app-sync.test.js +877 -0
  113. package/src/app.js +145 -0
  114. package/src/config-types.ts +44 -0
  115. package/src/db.js +58 -0
  116. package/src/load-config.js +307 -0
  117. package/src/migrations.js +36 -0
  118. package/src/run-migrations.js +8 -0
  119. package/src/scripts/disable-openid.js +44 -0
  120. package/src/scripts/enable-openid.js +53 -0
  121. package/src/scripts/health-check.js +23 -0
  122. package/src/scripts/reset-password.js +51 -0
  123. package/src/secrets.test.js +83 -0
  124. package/src/services/secrets-service.js +94 -0
  125. package/src/services/user-service.js +272 -0
  126. package/src/sql/messages.sql +9 -0
  127. package/src/sync-simple.js +95 -0
  128. package/src/util/hash.js +5 -0
  129. package/src/util/middlewares.js +62 -0
  130. package/src/util/paths.js +13 -0
  131. package/src/util/payee-name.js +45 -0
  132. package/src/util/prompt.js +88 -0
  133. package/src/util/title/index.js +59 -0
  134. package/src/util/title/lower-case.js +93 -0
  135. package/src/util/title/specials.js +21 -0
  136. package/src/util/validate-user.js +68 -0
  137. package/tsconfig.json +21 -0
@@ -0,0 +1,149 @@
1
+ import * as bcrypt from 'bcrypt';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ import { clearExpiredSessions, getAccountDb } from '../account-db.js';
5
+ import { config } from '../load-config.js';
6
+ import { TOKEN_EXPIRATION_NEVER } from '../util/validate-user.js';
7
+
8
+ function isValidPassword(password) {
9
+ return password != null && password !== '';
10
+ }
11
+
12
+ function hashPassword(password) {
13
+ return bcrypt.hashSync(password, 12);
14
+ }
15
+
16
+ export function bootstrapPassword(password) {
17
+ if (!isValidPassword(password)) {
18
+ return { error: 'invalid-password' };
19
+ }
20
+
21
+ const hashed = hashPassword(password);
22
+ const accountDb = getAccountDb();
23
+ accountDb.transaction(() => {
24
+ accountDb.mutate('DELETE FROM auth WHERE method = ?', ['password']);
25
+ accountDb.mutate('UPDATE auth SET active = 0');
26
+ accountDb.mutate(
27
+ "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('password', 'Password', ?, 1)",
28
+ [hashed],
29
+ );
30
+ });
31
+
32
+ return {};
33
+ }
34
+
35
+ export function loginWithPassword(password) {
36
+ if (!isValidPassword(password)) {
37
+ return { error: 'invalid-password' };
38
+ }
39
+
40
+ const accountDb = getAccountDb();
41
+ const { extra_data: passwordHash } =
42
+ accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [
43
+ 'password',
44
+ ]) || {};
45
+
46
+ if (!passwordHash) {
47
+ return { error: 'invalid-password' };
48
+ }
49
+
50
+ const confirmed = bcrypt.compareSync(password, passwordHash);
51
+
52
+ if (!confirmed) {
53
+ return { error: 'invalid-password' };
54
+ }
55
+
56
+ const sessionRow = accountDb.first(
57
+ 'SELECT * FROM sessions WHERE auth_method = ?',
58
+ ['password'],
59
+ );
60
+
61
+ const token = sessionRow ? sessionRow.token : uuidv4();
62
+
63
+ const { totalOfUsers } = accountDb.first(
64
+ 'SELECT count(*) as totalOfUsers FROM users',
65
+ );
66
+ let userId = null;
67
+ if (totalOfUsers === 0) {
68
+ userId = uuidv4();
69
+ accountDb.mutate(
70
+ 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, 1, 1, ?)',
71
+ [userId, '', '', 'ADMIN'],
72
+ );
73
+ } else {
74
+ const { id: userIdFromDb } = accountDb.first(
75
+ 'SELECT id FROM users WHERE user_name = ?',
76
+ [''],
77
+ );
78
+
79
+ userId = userIdFromDb;
80
+
81
+ if (!userId) {
82
+ return { error: 'user-not-found' };
83
+ }
84
+ }
85
+
86
+ let expiration = TOKEN_EXPIRATION_NEVER;
87
+ if (
88
+ config.get('token_expiration') !== 'never' &&
89
+ config.get('token_expiration') !== 'openid-provider' &&
90
+ typeof config.get('token_expiration') === 'number'
91
+ ) {
92
+ expiration =
93
+ Math.floor(Date.now() / 1000) + config.get('token_expiration') * 60;
94
+ }
95
+
96
+ if (!sessionRow) {
97
+ accountDb.mutate(
98
+ 'INSERT INTO sessions (token, expires_at, user_id, auth_method) VALUES (?, ?, ?, ?)',
99
+ [token, expiration, userId, 'password'],
100
+ );
101
+ } else {
102
+ accountDb.mutate(
103
+ 'UPDATE sessions SET user_id = ?, expires_at = ? WHERE token = ?',
104
+ [userId, expiration, token],
105
+ );
106
+ }
107
+
108
+ clearExpiredSessions();
109
+
110
+ return { token };
111
+ }
112
+
113
+ export function changePassword(newPassword) {
114
+ const accountDb = getAccountDb();
115
+
116
+ if (!isValidPassword(newPassword)) {
117
+ return { error: 'invalid-password' };
118
+ }
119
+
120
+ const hashed = hashPassword(newPassword);
121
+ accountDb.mutate("UPDATE auth SET extra_data = ? WHERE method = 'password'", [
122
+ hashed,
123
+ ]);
124
+ return {};
125
+ }
126
+
127
+ export function checkPassword(password) {
128
+ if (!isValidPassword(password)) {
129
+ return false;
130
+ }
131
+
132
+ const accountDb = getAccountDb();
133
+ const { extra_data: passwordHash } =
134
+ accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [
135
+ 'password',
136
+ ]) || {};
137
+
138
+ if (!passwordHash) {
139
+ return false;
140
+ }
141
+
142
+ const confirmed = bcrypt.compareSync(password, passwordHash);
143
+
144
+ if (!confirmed) {
145
+ return false;
146
+ }
147
+
148
+ return true;
149
+ }
@@ -0,0 +1,155 @@
1
+ import express from 'express';
2
+
3
+ import {
4
+ bootstrap,
5
+ needsBootstrap,
6
+ getLoginMethod,
7
+ listLoginMethods,
8
+ getUserInfo,
9
+ getActiveLoginMethod,
10
+ } from './account-db.js';
11
+ import { isValidRedirectUrl, loginWithOpenIdSetup } from './accounts/openid.js';
12
+ import { changePassword, loginWithPassword } from './accounts/password.js';
13
+ import {
14
+ errorMiddleware,
15
+ requestLoggerMiddleware,
16
+ } from './util/middlewares.js';
17
+ import { validateAuthHeader, validateSession } from './util/validate-user.js';
18
+
19
+ const app = express();
20
+ app.use(express.json());
21
+ app.use(express.urlencoded({ extended: true }));
22
+ app.use(errorMiddleware);
23
+ app.use(requestLoggerMiddleware);
24
+ export { app as handlers };
25
+
26
+ // Non-authenticated endpoints:
27
+ //
28
+ // /needs-bootstrap
29
+ // /boostrap (special endpoint for setting up the instance, cant call again)
30
+ // /login
31
+
32
+ app.get('/needs-bootstrap', (req, res) => {
33
+ const availableLoginMethods = listLoginMethods();
34
+ res.send({
35
+ status: 'ok',
36
+ data: {
37
+ bootstrapped: !needsBootstrap(),
38
+ loginMethod:
39
+ availableLoginMethods.length === 1
40
+ ? availableLoginMethods[0].method
41
+ : getLoginMethod(),
42
+ availableLoginMethods,
43
+ multiuser: getActiveLoginMethod() === 'openid',
44
+ },
45
+ });
46
+ });
47
+
48
+ app.post('/bootstrap', async (req, res) => {
49
+ const boot = await bootstrap(req.body);
50
+
51
+ if (boot?.error) {
52
+ res.status(400).send({ status: 'error', reason: boot?.error });
53
+ return;
54
+ }
55
+ res.send({ status: 'ok', data: boot });
56
+ });
57
+
58
+ app.get('/login-methods', (req, res) => {
59
+ const methods = listLoginMethods();
60
+ res.send({ status: 'ok', methods });
61
+ });
62
+
63
+ app.post('/login', async (req, res) => {
64
+ const loginMethod = getLoginMethod(req);
65
+ console.log('Logging in via ' + loginMethod);
66
+ let tokenRes = null;
67
+ switch (loginMethod) {
68
+ case 'header': {
69
+ const headerVal = req.get('x-actual-password') || '';
70
+ const obfuscated =
71
+ '*'.repeat(headerVal.length) || 'No password provided.';
72
+ console.debug('HEADER VALUE: ' + obfuscated);
73
+ if (headerVal === '') {
74
+ res.send({ status: 'error', reason: 'invalid-header' });
75
+ return;
76
+ } else {
77
+ if (validateAuthHeader(req)) {
78
+ tokenRes = loginWithPassword(headerVal);
79
+ } else {
80
+ res.send({ status: 'error', reason: 'proxy-not-trusted' });
81
+ return;
82
+ }
83
+ }
84
+ break;
85
+ }
86
+ case 'openid': {
87
+ if (!isValidRedirectUrl(req.body.return_url)) {
88
+ res
89
+ .status(400)
90
+ .send({ status: 'error', reason: 'Invalid redirect URL' });
91
+ return;
92
+ }
93
+
94
+ const { error, url } = await loginWithOpenIdSetup(
95
+ req.body.return_url,
96
+ req.body.password,
97
+ );
98
+ if (error) {
99
+ res.status(400).send({ status: 'error', reason: error });
100
+ return;
101
+ }
102
+ res.send({ status: 'ok', data: { redirect_url: url } });
103
+ return;
104
+ }
105
+
106
+ default:
107
+ tokenRes = loginWithPassword(req.body.password);
108
+ break;
109
+ }
110
+ const { error, token } = tokenRes;
111
+
112
+ if (error) {
113
+ res.status(400).send({ status: 'error', reason: error });
114
+ return;
115
+ }
116
+
117
+ res.send({ status: 'ok', data: { token } });
118
+ });
119
+
120
+ app.post('/change-password', (req, res) => {
121
+ const session = validateSession(req, res);
122
+ if (!session) return;
123
+
124
+ const { error } = changePassword(req.body.password);
125
+
126
+ if (error) {
127
+ res.status(400).send({ status: 'error', reason: error });
128
+ return;
129
+ }
130
+
131
+ res.send({ status: 'ok', data: {} });
132
+ });
133
+
134
+ app.get('/validate', (req, res) => {
135
+ const session = validateSession(req, res);
136
+ if (session) {
137
+ const user = getUserInfo(session.user_id);
138
+ if (!user) {
139
+ res.status(400).send({ status: 'error', reason: 'User not found' });
140
+ return;
141
+ }
142
+
143
+ res.send({
144
+ status: 'ok',
145
+ data: {
146
+ validated: true,
147
+ userName: user?.user_name,
148
+ permission: user?.role,
149
+ userId: session?.user_id,
150
+ displayName: user?.display_name,
151
+ loginMethod: session?.auth_method,
152
+ },
153
+ });
154
+ }
155
+ });
@@ -0,0 +1,410 @@
1
+ import express from 'express';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ import { isAdmin } from './account-db.js';
5
+ import * as UserService from './services/user-service.js';
6
+ import {
7
+ errorMiddleware,
8
+ requestLoggerMiddleware,
9
+ validateSessionMiddleware,
10
+ } from './util/middlewares.js';
11
+ import { validateSession } from './util/validate-user.js';
12
+
13
+ const app = express();
14
+ app.use(express.json());
15
+ app.use(express.urlencoded({ extended: true }));
16
+ app.use(requestLoggerMiddleware);
17
+
18
+ export { app as handlers };
19
+
20
+ app.get('/owner-created/', (req, res) => {
21
+ try {
22
+ const ownerCount = UserService.getOwnerCount();
23
+ res.json(ownerCount > 0);
24
+ } catch (error) {
25
+ res.status(500).json({ error: 'Failed to retrieve owner count' });
26
+ }
27
+ });
28
+
29
+ app.get('/users/', validateSessionMiddleware, (req, res) => {
30
+ const users = UserService.getAllUsers();
31
+ res.json(
32
+ users.map(u => ({
33
+ ...u,
34
+ owner: u.owner === 1,
35
+ enabled: u.enabled === 1,
36
+ })),
37
+ );
38
+ });
39
+
40
+ app.post('/users', validateSessionMiddleware, async (req, res) => {
41
+ if (!isAdmin(res.locals.user_id)) {
42
+ res.status(403).send({
43
+ status: 'error',
44
+ reason: 'forbidden',
45
+ details: 'permission-not-found',
46
+ });
47
+ return;
48
+ }
49
+
50
+ const { userName, role, displayName, enabled } = req.body;
51
+
52
+ if (!userName || !role) {
53
+ res.status(400).send({
54
+ status: 'error',
55
+ reason: `${!userName ? 'user-cant-be-empty' : 'role-cant-be-empty'}`,
56
+ details: `${!userName ? 'Username' : 'Role'} cannot be empty`,
57
+ });
58
+ return;
59
+ }
60
+
61
+ const roleIdFromDb = UserService.validateRole(role);
62
+ if (!roleIdFromDb) {
63
+ res.status(400).send({
64
+ status: 'error',
65
+ reason: 'role-does-not-exists',
66
+ details: 'Selected role does not exist',
67
+ });
68
+ return;
69
+ }
70
+
71
+ const userIdInDb = UserService.getUserByUsername(userName);
72
+ if (userIdInDb) {
73
+ res.status(400).send({
74
+ status: 'error',
75
+ reason: 'user-already-exists',
76
+ details: `User ${userName} already exists`,
77
+ });
78
+ return;
79
+ }
80
+
81
+ const userId = uuidv4();
82
+ UserService.insertUser(
83
+ userId,
84
+ userName,
85
+ displayName || null,
86
+ enabled ? 1 : 0,
87
+ );
88
+
89
+ res.status(200).send({ status: 'ok', data: { id: userId } });
90
+ });
91
+
92
+ app.patch('/users', validateSessionMiddleware, async (req, res) => {
93
+ if (!isAdmin(res.locals.user_id)) {
94
+ res.status(403).send({
95
+ status: 'error',
96
+ reason: 'forbidden',
97
+ details: 'permission-not-found',
98
+ });
99
+ return;
100
+ }
101
+
102
+ const { id, userName, role, displayName, enabled } = req.body;
103
+
104
+ if (!userName || !role) {
105
+ res.status(400).send({
106
+ status: 'error',
107
+ reason: `${!userName ? 'user-cant-be-empty' : 'role-cant-be-empty'}`,
108
+ details: `${!userName ? 'Username' : 'Role'} cannot be empty`,
109
+ });
110
+ return;
111
+ }
112
+
113
+ const roleIdFromDb = UserService.validateRole(role);
114
+ if (!roleIdFromDb) {
115
+ res.status(400).send({
116
+ status: 'error',
117
+ reason: 'role-does-not-exists',
118
+ details: 'Selected role does not exist',
119
+ });
120
+ return;
121
+ }
122
+
123
+ const userIdInDb = UserService.getUserById(id);
124
+ if (!userIdInDb) {
125
+ res.status(400).send({
126
+ status: 'error',
127
+ reason: 'cannot-find-user-to-update',
128
+ details: `Cannot find user ${userName} to update`,
129
+ });
130
+ return;
131
+ }
132
+
133
+ UserService.updateUserWithRole(
134
+ userIdInDb,
135
+ userName,
136
+ displayName || null,
137
+ enabled ? 1 : 0,
138
+ role,
139
+ );
140
+
141
+ res.status(200).send({ status: 'ok', data: { id: userIdInDb } });
142
+ });
143
+
144
+ app.delete('/users', validateSessionMiddleware, async (req, res) => {
145
+ if (!isAdmin(res.locals.user_id)) {
146
+ res.status(403).send({
147
+ status: 'error',
148
+ reason: 'forbidden',
149
+ details: 'permission-not-found',
150
+ });
151
+ return;
152
+ }
153
+
154
+ const ids = req.body.ids;
155
+ let totalDeleted = 0;
156
+ ids.forEach(item => {
157
+ const ownerId = UserService.getOwnerId();
158
+
159
+ if (item === ownerId) return;
160
+
161
+ UserService.deleteUserAccess(item);
162
+ UserService.transferAllFilesFromUser(ownerId, item);
163
+ const usersDeleted = UserService.deleteUser(item);
164
+ totalDeleted += usersDeleted;
165
+ });
166
+
167
+ if (ids.length === totalDeleted) {
168
+ res
169
+ .status(200)
170
+ .send({ status: 'ok', data: { someDeletionsFailed: false } });
171
+ } else {
172
+ res.status(400).send({
173
+ status: 'error',
174
+ reason: 'not-all-deleted',
175
+ details: '',
176
+ });
177
+ }
178
+ });
179
+
180
+ app.get('/access', validateSessionMiddleware, (req, res) => {
181
+ const fileId = req.query.fileId;
182
+
183
+ const { granted } = UserService.checkFilePermission(
184
+ fileId,
185
+ res.locals.user_id,
186
+ ) || {
187
+ granted: 0,
188
+ };
189
+
190
+ if (granted === 0 && !isAdmin(res.locals.user_id)) {
191
+ res.status(403).send({
192
+ status: 'error',
193
+ reason: 'forbidden',
194
+ details: 'permission-not-found',
195
+ });
196
+ return false;
197
+ }
198
+
199
+ const fileIdInDb = UserService.getFileById(fileId);
200
+ if (!fileIdInDb) {
201
+ res.status(404).send({
202
+ status: 'error',
203
+ reason: 'invalid-file-id',
204
+ details: 'File not found at server',
205
+ });
206
+ return false;
207
+ }
208
+
209
+ const accesses = UserService.getUserAccess(
210
+ fileId,
211
+ res.locals.user_id,
212
+ isAdmin(res.locals.user_id),
213
+ );
214
+
215
+ res.json(accesses);
216
+ });
217
+
218
+ app.post('/access', (req, res) => {
219
+ const userAccess = req.body || {};
220
+ const session = validateSession(req, res);
221
+
222
+ if (!session) return;
223
+
224
+ const { granted } = UserService.checkFilePermission(
225
+ userAccess.fileId,
226
+ session.user_id,
227
+ ) || {
228
+ granted: 0,
229
+ };
230
+
231
+ if (granted === 0 && !isAdmin(session.user_id)) {
232
+ res.status(400).send({
233
+ status: 'error',
234
+ reason: 'file-denied',
235
+ details: "You don't have permissions over this file",
236
+ });
237
+ return;
238
+ }
239
+
240
+ const fileIdInDb = UserService.getFileById(userAccess.fileId);
241
+ if (!fileIdInDb) {
242
+ res.status(404).send({
243
+ status: 'error',
244
+ reason: 'invalid-file-id',
245
+ details: 'File not found at server',
246
+ });
247
+ return;
248
+ }
249
+
250
+ if (!userAccess.userId) {
251
+ res.status(400).send({
252
+ status: 'error',
253
+ reason: 'user-cant-be-empty',
254
+ details: 'User cannot be empty',
255
+ });
256
+ return;
257
+ }
258
+
259
+ if (UserService.countUserAccess(userAccess.fileId, userAccess.userId) > 0) {
260
+ res.status(400).send({
261
+ status: 'error',
262
+ reason: 'user-already-have-access',
263
+ details: 'User already have access',
264
+ });
265
+ return;
266
+ }
267
+
268
+ UserService.addUserAccess(userAccess.userId, userAccess.fileId);
269
+
270
+ res.status(200).send({ status: 'ok', data: {} });
271
+ });
272
+
273
+ app.delete('/access', (req, res) => {
274
+ const fileId = req.query.fileId;
275
+ const session = validateSession(req, res);
276
+ if (!session) return;
277
+
278
+ const { granted } = UserService.checkFilePermission(
279
+ fileId,
280
+ session.user_id,
281
+ ) || {
282
+ granted: 0,
283
+ };
284
+
285
+ if (granted === 0 && !isAdmin(session.user_id)) {
286
+ res.status(400).send({
287
+ status: 'error',
288
+ reason: 'file-denied',
289
+ details: "You don't have permissions over this file",
290
+ });
291
+ return;
292
+ }
293
+
294
+ const fileIdInDb = UserService.getFileById(fileId);
295
+ if (!fileIdInDb) {
296
+ res.status(404).send({
297
+ status: 'error',
298
+ reason: 'invalid-file-id',
299
+ details: 'File not found at server',
300
+ });
301
+ return;
302
+ }
303
+
304
+ const ids = req.body.ids;
305
+ const totalDeleted = UserService.deleteUserAccessByFileId(ids, fileId);
306
+
307
+ if (ids.length === totalDeleted) {
308
+ res
309
+ .status(200)
310
+ .send({ status: 'ok', data: { someDeletionsFailed: false } });
311
+ } else {
312
+ res.status(400).send({
313
+ status: 'error',
314
+ reason: 'not-all-deleted',
315
+ details: '',
316
+ });
317
+ }
318
+ });
319
+
320
+ app.get('/access/users', validateSessionMiddleware, async (req, res) => {
321
+ const fileId = req.query.fileId;
322
+
323
+ const { granted } = UserService.checkFilePermission(
324
+ fileId,
325
+ res.locals.user_id,
326
+ ) || {
327
+ granted: 0,
328
+ };
329
+
330
+ if (granted === 0 && !isAdmin(res.locals.user_id)) {
331
+ res.status(400).send({
332
+ status: 'error',
333
+ reason: 'file-denied',
334
+ details: "You don't have permissions over this file",
335
+ });
336
+ return;
337
+ }
338
+
339
+ const fileIdInDb = UserService.getFileById(fileId);
340
+ if (!fileIdInDb) {
341
+ res.status(404).send({
342
+ status: 'error',
343
+ reason: 'invalid-file-id',
344
+ details: 'File not found at server',
345
+ });
346
+ return;
347
+ }
348
+
349
+ const users = UserService.getAllUserAccess(fileId);
350
+ res.json(users);
351
+ });
352
+
353
+ app.post(
354
+ '/access/transfer-ownership/',
355
+ validateSessionMiddleware,
356
+ (req, res) => {
357
+ const newUserOwner = req.body || {};
358
+
359
+ const { granted } = UserService.checkFilePermission(
360
+ newUserOwner.fileId,
361
+ res.locals.user_id,
362
+ ) || {
363
+ granted: 0,
364
+ };
365
+
366
+ if (granted === 0 && !isAdmin(res.locals.user_id)) {
367
+ res.status(400).send({
368
+ status: 'error',
369
+ reason: 'file-denied',
370
+ details: "You don't have permissions over this file",
371
+ });
372
+ return;
373
+ }
374
+
375
+ const fileIdInDb = UserService.getFileById(newUserOwner.fileId);
376
+ if (!fileIdInDb) {
377
+ res.status(404).send({
378
+ status: 'error',
379
+ reason: 'invalid-file-id',
380
+ details: 'File not found at server',
381
+ });
382
+ return;
383
+ }
384
+
385
+ if (!newUserOwner.newUserId) {
386
+ res.status(400).send({
387
+ status: 'error',
388
+ reason: 'user-cant-be-empty',
389
+ details: 'Username cannot be empty',
390
+ });
391
+ return;
392
+ }
393
+
394
+ const newUserIdFromDb = UserService.getUserById(newUserOwner.newUserId);
395
+ if (newUserIdFromDb === 0) {
396
+ res.status(400).send({
397
+ status: 'error',
398
+ reason: 'new-user-not-found',
399
+ details: 'New user not found',
400
+ });
401
+ return;
402
+ }
403
+
404
+ UserService.updateFileOwner(newUserOwner.newUserId, newUserOwner.fileId);
405
+
406
+ res.status(200).send({ status: 'ok', data: {} });
407
+ },
408
+ );
409
+
410
+ app.use(errorMiddleware);