@actual-app/sync-server 25.5.0 → 25.6.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 (244) hide show
  1. package/{app.js → build/app.js} +4 -5
  2. package/build/bin/actual-server.js +101 -0
  3. package/build/migrations/1694360000000-create-folders.js +21 -0
  4. package/{migrations → build/migrations}/1694360479680-create-account-db.js +2 -4
  5. package/{migrations → build/migrations}/1694362247011-create-secret-table.js +2 -4
  6. package/build/migrations/1702667624000-rename-nordigen-secrets.js +9 -0
  7. package/{migrations → build/migrations}/1718889148000-openid.js +4 -10
  8. package/{migrations → build/migrations}/1719409568000-multiuser.js +10 -26
  9. package/build/src/account-db.js +182 -0
  10. package/build/src/accounts/openid.js +287 -0
  11. package/build/src/accounts/password.js +98 -0
  12. package/build/src/app-account.js +125 -0
  13. package/build/src/app-admin.js +317 -0
  14. package/build/src/app-admin.test.js +303 -0
  15. package/build/src/app-gocardless/app-gocardless.js +193 -0
  16. package/build/src/app-gocardless/bank-factory.js +84 -0
  17. package/build/src/app-gocardless/banks/abanca_caglesmm.js +17 -0
  18. package/build/src/app-gocardless/banks/abnamro_abnanl2a.js +37 -0
  19. package/build/src/app-gocardless/banks/american_express_aesudef1.js +32 -0
  20. package/build/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +22 -0
  21. package/build/src/app-gocardless/banks/bank.interface.js +1 -0
  22. package/build/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +25 -0
  23. package/build/src/app-gocardless/banks/bankinter_bkbkesmm.js +18 -0
  24. package/build/src/app-gocardless/banks/belfius_gkccbebb.js +13 -0
  25. package/build/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +48 -0
  26. package/build/src/app-gocardless/banks/bnp_be_gebabebb.js +64 -0
  27. package/build/src/app-gocardless/banks/boursobank_bousfrppxxx.js +73 -0
  28. package/build/src/app-gocardless/banks/cbc_cregbebb.js +27 -0
  29. package/build/src/app-gocardless/banks/commerzbank_cobadeff.js +43 -0
  30. package/build/src/app-gocardless/banks/danskebank_dabno22.js +26 -0
  31. package/build/src/app-gocardless/banks/direkt_heladef1822.js +13 -0
  32. package/build/src/app-gocardless/banks/easybank_bawaatww.js +42 -0
  33. package/build/src/app-gocardless/banks/entercard_swednokk.js +28 -0
  34. package/build/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +34 -0
  35. package/build/src/app-gocardless/banks/hype_hyeeit22.js +63 -0
  36. package/build/src/app-gocardless/banks/ing_ingbrobu.js +56 -0
  37. package/build/src/app-gocardless/banks/ing_ingddeff.js +34 -0
  38. package/build/src/app-gocardless/banks/ing_pl_ingbplpw.js +29 -0
  39. package/build/src/app-gocardless/banks/integration-bank.js +78 -0
  40. package/build/src/app-gocardless/banks/isybank_itbbitmm.js +13 -0
  41. package/build/src/app-gocardless/banks/kbc_kredbebb.js +26 -0
  42. package/build/src/app-gocardless/banks/lhv-lhvbee22.js +24 -0
  43. package/build/src/app-gocardless/banks/mbank_retail_brexplpw.js +41 -0
  44. package/build/src/app-gocardless/banks/nationwide_naiagb21.js +32 -0
  45. package/build/src/app-gocardless/banks/nbg_ethngraaxxx.js +39 -0
  46. package/build/src/app-gocardless/banks/norwegian_xx_norwnok1.js +61 -0
  47. package/build/src/app-gocardless/banks/revolut_revolt21.js +20 -0
  48. package/build/src/app-gocardless/banks/sandboxfinance_sfin0000.js +21 -0
  49. package/build/src/app-gocardless/banks/seb_kort_bank_ab.js +63 -0
  50. package/build/src/app-gocardless/banks/seb_privat.js +19 -0
  51. package/build/src/app-gocardless/banks/sparnord_spnodk22.js +19 -0
  52. package/build/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +48 -0
  53. package/build/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +25 -0
  54. package/build/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +14 -0
  55. package/build/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +36 -0
  56. package/build/src/app-gocardless/banks/swedbank_habalv22.js +30 -0
  57. package/build/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +17 -0
  58. package/build/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +45 -0
  59. package/build/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +41 -0
  60. package/build/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +16 -0
  61. package/build/src/app-gocardless/banks/tests/boursobank_bousfrppxxx.spec.js +102 -0
  62. package/build/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +24 -0
  63. package/build/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +105 -0
  64. package/build/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +36 -0
  65. package/build/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +159 -0
  66. package/build/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +267 -0
  67. package/build/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +186 -0
  68. package/build/src/app-gocardless/banks/tests/integration_bank.spec.js +127 -0
  69. package/build/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +24 -0
  70. package/build/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +50 -0
  71. package/build/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +156 -0
  72. package/build/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +64 -0
  73. package/build/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +36 -0
  74. package/build/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +30 -0
  75. package/build/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +112 -0
  76. package/build/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +214 -0
  77. package/build/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +60 -0
  78. package/build/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +45 -0
  79. package/build/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +36 -0
  80. package/{src → build/src}/app-gocardless/banks/util/escape-regexp.js +1 -1
  81. package/{src → build/src}/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js +11 -16
  82. package/build/src/app-gocardless/banks/virgin_nrnbgb22.js +31 -0
  83. package/build/src/app-gocardless/errors.js +67 -0
  84. package/build/src/app-gocardless/gocardless-node.types.js +1 -0
  85. package/build/src/app-gocardless/gocardless.types.js +1 -0
  86. package/build/src/app-gocardless/services/gocardless-service.js +504 -0
  87. package/build/src/app-gocardless/services/tests/fixtures.js +165 -0
  88. package/build/src/app-gocardless/services/tests/gocardless-service.spec.js +387 -0
  89. package/build/src/app-gocardless/tests/bank-factory.spec.js +13 -0
  90. package/build/src/app-gocardless/tests/utils.spec.js +158 -0
  91. package/build/src/app-gocardless/util/handle-error.js +15 -0
  92. package/build/src/app-gocardless/utils.js +41 -0
  93. package/build/src/app-openid.js +83 -0
  94. package/build/src/app-pluggyai/app-pluggyai.js +164 -0
  95. package/build/src/app-pluggyai/pluggyai-service.js +97 -0
  96. package/build/src/app-secrets.js +48 -0
  97. package/build/src/app-simplefin/app-simplefin.js +335 -0
  98. package/build/src/app-sync/errors.js +12 -0
  99. package/build/src/app-sync/services/files-service.js +158 -0
  100. package/build/src/app-sync/tests/services/files-service.test.js +192 -0
  101. package/build/src/app-sync/validation.js +65 -0
  102. package/build/src/app-sync.js +302 -0
  103. package/build/src/app-sync.test.js +655 -0
  104. package/build/src/app.js +138 -0
  105. package/build/src/config-types.js +1 -0
  106. package/build/src/db.js +50 -0
  107. package/build/src/load-config.js +274 -0
  108. package/build/src/migrations.js +23 -0
  109. package/build/src/scripts/disable-openid.js +31 -0
  110. package/build/src/scripts/enable-openid.js +36 -0
  111. package/build/src/scripts/health-check.js +16 -0
  112. package/build/src/scripts/reset-password.js +40 -0
  113. package/build/src/scripts/run-migrations.js +6 -0
  114. package/build/src/secrets.test.js +68 -0
  115. package/build/src/services/secrets-service.js +79 -0
  116. package/build/src/services/user-service.js +201 -0
  117. package/build/src/sync-simple.js +68 -0
  118. package/{src → build/src}/util/hash.js +1 -2
  119. package/build/src/util/middlewares.js +49 -0
  120. package/{src → build/src}/util/paths.js +3 -6
  121. package/build/src/util/payee-name.js +37 -0
  122. package/build/src/util/prompt.js +70 -0
  123. package/build/src/util/title/index.js +43 -0
  124. package/build/src/util/title/lower-case.js +90 -0
  125. package/build/src/util/title/specials.js +21 -0
  126. package/build/src/util/validate-user.js +55 -0
  127. package/package.json +32 -36
  128. package/bin/actual-server.js +0 -117
  129. package/migrations/1694360000000-create-folders.js +0 -25
  130. package/migrations/1702667624000-rename-nordigen-secrets.js +0 -19
  131. package/src/account-db.js +0 -239
  132. package/src/accounts/openid.js +0 -368
  133. package/src/accounts/password.js +0 -149
  134. package/src/app-account.js +0 -155
  135. package/src/app-admin.js +0 -410
  136. package/src/app-admin.test.js +0 -381
  137. package/src/app-gocardless/app-gocardless.js +0 -274
  138. package/src/app-gocardless/bank-factory.js +0 -91
  139. package/src/app-gocardless/banks/abanca_caglesmm.js +0 -22
  140. package/src/app-gocardless/banks/abnamro_abnanl2a.js +0 -57
  141. package/src/app-gocardless/banks/american_express_aesudef1.js +0 -40
  142. package/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +0 -31
  143. package/src/app-gocardless/banks/bank.interface.ts +0 -51
  144. package/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +0 -39
  145. package/src/app-gocardless/banks/bankinter_bkbkesmm.js +0 -24
  146. package/src/app-gocardless/banks/belfius_gkccbebb.js +0 -17
  147. package/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +0 -61
  148. package/src/app-gocardless/banks/bnp_be_gebabebb.js +0 -73
  149. package/src/app-gocardless/banks/cbc_cregbebb.js +0 -34
  150. package/src/app-gocardless/banks/commerzbank_cobadeff.js +0 -54
  151. package/src/app-gocardless/banks/danskebank_dabno22.js +0 -39
  152. package/src/app-gocardless/banks/direkt_heladef1822.js +0 -18
  153. package/src/app-gocardless/banks/easybank_bawaatww.js +0 -50
  154. package/src/app-gocardless/banks/entercard_swednokk.js +0 -40
  155. package/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +0 -46
  156. package/src/app-gocardless/banks/hype_hyeeit22.js +0 -74
  157. package/src/app-gocardless/banks/ing_ingbrobu.js +0 -70
  158. package/src/app-gocardless/banks/ing_ingddeff.js +0 -47
  159. package/src/app-gocardless/banks/ing_pl_ingbplpw.js +0 -46
  160. package/src/app-gocardless/banks/integration-bank.js +0 -115
  161. package/src/app-gocardless/banks/isybank_itbbitmm.js +0 -18
  162. package/src/app-gocardless/banks/kbc_kredbebb.js +0 -33
  163. package/src/app-gocardless/banks/lhv-lhvbee22.js +0 -36
  164. package/src/app-gocardless/banks/mbank_retail_brexplpw.js +0 -56
  165. package/src/app-gocardless/banks/nationwide_naiagb21.js +0 -46
  166. package/src/app-gocardless/banks/nbg_ethngraaxxx.js +0 -51
  167. package/src/app-gocardless/banks/norwegian_xx_norwnok1.js +0 -74
  168. package/src/app-gocardless/banks/revolut_revolt21.js +0 -37
  169. package/src/app-gocardless/banks/sandboxfinance_sfin0000.js +0 -28
  170. package/src/app-gocardless/banks/seb_kort_bank_ab.js +0 -59
  171. package/src/app-gocardless/banks/seb_privat.js +0 -29
  172. package/src/app-gocardless/banks/sparnord_spnodk22.js +0 -24
  173. package/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +0 -61
  174. package/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +0 -30
  175. package/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +0 -19
  176. package/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +0 -50
  177. package/src/app-gocardless/banks/swedbank_habalv22.js +0 -47
  178. package/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +0 -21
  179. package/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +0 -61
  180. package/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +0 -53
  181. package/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +0 -22
  182. package/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +0 -34
  183. package/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +0 -133
  184. package/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +0 -54
  185. package/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +0 -206
  186. package/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +0 -302
  187. package/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +0 -202
  188. package/src/app-gocardless/banks/tests/integration_bank.spec.js +0 -156
  189. package/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +0 -38
  190. package/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +0 -68
  191. package/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +0 -171
  192. package/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +0 -105
  193. package/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +0 -48
  194. package/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +0 -42
  195. package/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +0 -133
  196. package/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +0 -255
  197. package/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +0 -100
  198. package/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +0 -57
  199. package/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +0 -54
  200. package/src/app-gocardless/banks/virgin_nrnbgb22.js +0 -39
  201. package/src/app-gocardless/errors.js +0 -84
  202. package/src/app-gocardless/gocardless-node.types.ts +0 -497
  203. package/src/app-gocardless/gocardless.types.ts +0 -93
  204. package/src/app-gocardless/link.html +0 -18
  205. package/src/app-gocardless/services/gocardless-service.js +0 -620
  206. package/src/app-gocardless/services/tests/fixtures.js +0 -181
  207. package/src/app-gocardless/services/tests/gocardless-service.spec.js +0 -537
  208. package/src/app-gocardless/tests/bank-factory.spec.js +0 -20
  209. package/src/app-gocardless/tests/utils.spec.js +0 -162
  210. package/src/app-gocardless/util/handle-error.js +0 -16
  211. package/src/app-gocardless/utils.js +0 -45
  212. package/src/app-openid.js +0 -108
  213. package/src/app-pluggyai/app-pluggyai.js +0 -215
  214. package/src/app-pluggyai/pluggyai-service.js +0 -120
  215. package/src/app-secrets.js +0 -61
  216. package/src/app-simplefin/app-simplefin.js +0 -405
  217. package/src/app-sync/errors.js +0 -13
  218. package/src/app-sync/services/files-service.js +0 -243
  219. package/src/app-sync/tests/services/files-service.test.js +0 -247
  220. package/src/app-sync/validation.js +0 -77
  221. package/src/app-sync.js +0 -391
  222. package/src/app-sync.test.js +0 -877
  223. package/src/app.js +0 -149
  224. package/src/config-types.ts +0 -44
  225. package/src/db.js +0 -58
  226. package/src/load-config.js +0 -307
  227. package/src/migrations.js +0 -36
  228. package/src/run-migrations.js +0 -8
  229. package/src/scripts/disable-openid.js +0 -44
  230. package/src/scripts/enable-openid.js +0 -53
  231. package/src/scripts/health-check.js +0 -23
  232. package/src/scripts/reset-password.js +0 -51
  233. package/src/secrets.test.js +0 -83
  234. package/src/services/secrets-service.js +0 -94
  235. package/src/services/user-service.js +0 -272
  236. package/src/sync-simple.js +0 -95
  237. package/src/util/middlewares.js +0 -62
  238. package/src/util/payee-name.js +0 -45
  239. package/src/util/prompt.js +0 -88
  240. package/src/util/title/index.js +0 -59
  241. package/src/util/title/lower-case.js +0 -93
  242. package/src/util/title/specials.js +0 -21
  243. package/src/util/validate-user.js +0 -68
  244. /package/{src → build/src}/sql/messages.sql +0 -0
@@ -0,0 +1,287 @@
1
+ import { custom, generators, Issuer } from 'openid-client';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { clearExpiredSessions, getAccountDb, listLoginMethods, } from '../account-db.js';
4
+ import { config } from '../load-config.js';
5
+ import { getUserByUsername, transferAllFilesFromUser, } from '../services/user-service.js';
6
+ import { TOKEN_EXPIRATION_NEVER } from '../util/validate-user.js';
7
+ import { checkPassword } from './password.js';
8
+ export async function bootstrapOpenId(configParameter) {
9
+ if (!('issuer' in configParameter) && !('discoveryURL' in configParameter)) {
10
+ return { error: 'missing-issuer-or-discoveryURL' };
11
+ }
12
+ if (!('client_id' in configParameter)) {
13
+ return { error: 'missing-client-id' };
14
+ }
15
+ if (!('client_secret' in configParameter)) {
16
+ return { error: 'missing-client-secret' };
17
+ }
18
+ if (!('server_hostname' in configParameter)) {
19
+ return { error: 'missing-server-hostname' };
20
+ }
21
+ custom.setHttpOptionsDefaults({
22
+ timeout: 20 * 1000, // 20 seconds
23
+ });
24
+ try {
25
+ //FOR BACKWARD COMPATIBLITY:
26
+ //If we don't put discoverURL into the issuer, it will break already enabled openid instances
27
+ if (configParameter.discoveryURL) {
28
+ configParameter.issuer = configParameter.discoveryURL;
29
+ delete configParameter.discoveryURL;
30
+ }
31
+ await setupOpenIdClient(configParameter);
32
+ }
33
+ catch (err) {
34
+ console.error('Error setting up OpenID client:', err);
35
+ return { error: 'configuration-error' };
36
+ }
37
+ const accountDb = getAccountDb();
38
+ try {
39
+ accountDb.transaction(() => {
40
+ accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']);
41
+ accountDb.mutate('UPDATE auth SET active = 0');
42
+ accountDb.mutate("INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", [JSON.stringify(configParameter)]);
43
+ });
44
+ }
45
+ catch (err) {
46
+ console.error('Error updating auth table:', err);
47
+ return { error: 'database-error' };
48
+ }
49
+ return {};
50
+ }
51
+ async function setupOpenIdClient(configParameter) {
52
+ const issuer = typeof configParameter.issuer === 'string'
53
+ ? await Issuer.discover(configParameter.issuer)
54
+ : new Issuer({
55
+ issuer: configParameter.issuer.name,
56
+ authorization_endpoint: configParameter.issuer.authorization_endpoint,
57
+ token_endpoint: configParameter.issuer.token_endpoint,
58
+ userinfo_endpoint: configParameter.issuer.userinfo_endpoint,
59
+ });
60
+ const client = new issuer.Client({
61
+ client_id: configParameter.client_id,
62
+ client_secret: configParameter.client_secret,
63
+ redirect_uri: new URL('/openid/callback', configParameter.server_hostname).toString(),
64
+ validate_id_token: true,
65
+ });
66
+ return client;
67
+ }
68
+ export async function loginWithOpenIdSetup(returnUrl, firstTimeLoginPassword = '') {
69
+ if (!returnUrl) {
70
+ return { error: 'return-url-missing' };
71
+ }
72
+ if (!isValidRedirectUrl(returnUrl)) {
73
+ return { error: 'invalid-return-url' };
74
+ }
75
+ const accountDb = getAccountDb();
76
+ const { countUsersWithUserName } = accountDb.first('SELECT count(*) as countUsersWithUserName FROM users WHERE user_name <> ?', ['']);
77
+ if (countUsersWithUserName === 0) {
78
+ const methods = listLoginMethods();
79
+ if (methods.some(authMethod => authMethod.method === 'password')) {
80
+ const valid = checkPassword(firstTimeLoginPassword);
81
+ if (!valid) {
82
+ return { error: 'invalid-password' };
83
+ }
84
+ }
85
+ }
86
+ let config = accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [
87
+ 'openid',
88
+ ]);
89
+ if (!config) {
90
+ return { error: 'openid-not-configured' };
91
+ }
92
+ try {
93
+ config = JSON.parse(config['extra_data']);
94
+ }
95
+ catch (err) {
96
+ console.error('Error parsing OpenID configuration:', err);
97
+ return { error: 'openid-setup-failed' };
98
+ }
99
+ let client;
100
+ try {
101
+ client = await setupOpenIdClient(config);
102
+ }
103
+ catch (err) {
104
+ console.error('Error setting up OpenID client:', err);
105
+ return { error: 'openid-setup-failed' };
106
+ }
107
+ const state = generators.state();
108
+ const code_verifier = generators.codeVerifier();
109
+ const code_challenge = generators.codeChallenge(code_verifier);
110
+ const now_time = Date.now();
111
+ const expiry_time = now_time + 300 * 1000;
112
+ accountDb.mutate('DELETE FROM pending_openid_requests WHERE expiry_time < ?', [now_time]);
113
+ accountDb.mutate('INSERT INTO pending_openid_requests (state, code_verifier, return_url, expiry_time) VALUES (?, ?, ?, ?)', [state, code_verifier, returnUrl, expiry_time]);
114
+ const url = client.authorizationUrl({
115
+ response_type: 'code',
116
+ scope: 'openid email profile',
117
+ state,
118
+ code_challenge,
119
+ code_challenge_method: 'S256',
120
+ });
121
+ return { url };
122
+ }
123
+ export async function loginWithOpenIdFinalize(body) {
124
+ if (!body.code) {
125
+ return { error: 'missing-authorization-code' };
126
+ }
127
+ if (!body.state) {
128
+ return { error: 'missing-state' };
129
+ }
130
+ const accountDb = getAccountDb();
131
+ let configFromDb = accountDb.first("SELECT extra_data FROM auth WHERE method = 'openid' AND active = 1");
132
+ if (!configFromDb) {
133
+ return { error: 'openid-not-configured' };
134
+ }
135
+ try {
136
+ configFromDb = JSON.parse(configFromDb['extra_data']);
137
+ }
138
+ catch (err) {
139
+ console.error('Error parsing OpenID configuration:', err);
140
+ return { error: 'openid-setup-failed' };
141
+ }
142
+ let client;
143
+ try {
144
+ client = await setupOpenIdClient(configFromDb);
145
+ }
146
+ catch (err) {
147
+ console.error('Error setting up OpenID client:', err);
148
+ return { error: 'openid-setup-failed' };
149
+ }
150
+ const pendingRequest = accountDb.first('SELECT code_verifier, return_url FROM pending_openid_requests WHERE state = ? AND expiry_time > ?', [body.state, Date.now()]);
151
+ if (!pendingRequest) {
152
+ return { error: 'invalid-or-expired-state' };
153
+ }
154
+ const { code_verifier, return_url } = pendingRequest;
155
+ try {
156
+ let tokenSet = null;
157
+ if (!configFromDb.authMethod || configFromDb.authMethod === 'openid') {
158
+ const params = { code: body.code, state: body.state, iss: body.iss };
159
+ tokenSet = await client.callback(client.redirect_uris[0], params, {
160
+ code_verifier,
161
+ state: body.state,
162
+ });
163
+ }
164
+ else {
165
+ tokenSet = await client.grant({
166
+ grant_type: 'authorization_code',
167
+ code: body.code,
168
+ redirect_uri: client.redirect_uris[0],
169
+ code_verifier,
170
+ });
171
+ }
172
+ const userInfo = await client.userinfo(tokenSet.access_token);
173
+ const identity = userInfo.preferred_username ??
174
+ userInfo.login ??
175
+ userInfo.email ??
176
+ userInfo.id ??
177
+ userInfo.sub;
178
+ if (identity == null) {
179
+ return { error: 'openid-grant-failed: no identification was found' };
180
+ }
181
+ let userId = null;
182
+ try {
183
+ accountDb.transaction(() => {
184
+ const { countUsersWithUserName } = accountDb.first('SELECT count(*) as countUsersWithUserName FROM users WHERE user_name <> ?', ['']);
185
+ // Check if user was created by another transaction
186
+ const existingUser = accountDb.first('SELECT id FROM users WHERE user_name = ?', [identity]);
187
+ if (!existingUser &&
188
+ (countUsersWithUserName === 0 ||
189
+ config.get('userCreationMode') === 'login')) {
190
+ userId = uuidv4();
191
+ accountDb.mutate('INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, 1, ?, ?)', [
192
+ userId,
193
+ identity,
194
+ userInfo.name ?? userInfo.email ?? identity,
195
+ countUsersWithUserName === 0 ? '1' : '0',
196
+ countUsersWithUserName === 0 ? 'ADMIN' : 'BASIC',
197
+ ]);
198
+ if (countUsersWithUserName === 0) {
199
+ const userFromPasswordMethod = getUserByUsername('');
200
+ if (userFromPasswordMethod) {
201
+ transferAllFilesFromUser(userId, userFromPasswordMethod.user_id);
202
+ }
203
+ }
204
+ }
205
+ else {
206
+ const { id: userIdFromDb, display_name: displayName } = accountDb.first('SELECT id, display_name FROM users WHERE user_name = ? and enabled = 1', [identity]) || {};
207
+ if (userIdFromDb == null) {
208
+ throw new Error('openid-grant-failed');
209
+ }
210
+ if (!displayName && userInfo.name) {
211
+ accountDb.mutate('UPDATE users set display_name = ? WHERE id = ?', [
212
+ userInfo.name,
213
+ userIdFromDb,
214
+ ]);
215
+ }
216
+ userId = userIdFromDb;
217
+ }
218
+ });
219
+ }
220
+ catch (error) {
221
+ if (error.message === 'user-already-exists') {
222
+ return { error: 'user-already-exists' };
223
+ }
224
+ else if (error.message === 'openid-grant-failed') {
225
+ return { error: 'openid-grant-failed' };
226
+ }
227
+ else {
228
+ throw error; // Re-throw other unexpected errors
229
+ }
230
+ }
231
+ const token = uuidv4();
232
+ let expiration;
233
+ if (config.get('token_expiration') === 'openid-provider') {
234
+ expiration = tokenSet.expires_at ?? TOKEN_EXPIRATION_NEVER;
235
+ }
236
+ else if (config.get('token_expiration') === 'never') {
237
+ expiration = TOKEN_EXPIRATION_NEVER;
238
+ }
239
+ else if (typeof config.get('token_expiration') === 'number') {
240
+ expiration =
241
+ Math.floor(Date.now() / 1000) + config.get('token_expiration') * 60;
242
+ }
243
+ else {
244
+ expiration = Math.floor(Date.now() / 1000) + 10 * 60;
245
+ }
246
+ accountDb.mutate('INSERT INTO sessions (token, expires_at, user_id, auth_method) VALUES (?, ?, ?, ?)', [token, expiration, userId, 'openid']);
247
+ clearExpiredSessions();
248
+ return { url: `${return_url}/openid-cb?token=${token}` };
249
+ }
250
+ catch (err) {
251
+ console.error('OpenID grant failed:', err);
252
+ return { error: 'openid-grant-failed' };
253
+ }
254
+ }
255
+ export function getServerHostname() {
256
+ const auth = getAccountDb().first('select * from auth WHERE method = ? and active = 1', ['openid']);
257
+ if (auth && auth.extra_data) {
258
+ try {
259
+ const openIdConfig = JSON.parse(auth.extra_data);
260
+ return openIdConfig.server_hostname;
261
+ }
262
+ catch (error) {
263
+ console.error('Error parsing OpenID configuration:', error);
264
+ }
265
+ }
266
+ return null;
267
+ }
268
+ export function isValidRedirectUrl(url) {
269
+ const serverHostname = getServerHostname();
270
+ if (!serverHostname) {
271
+ return false;
272
+ }
273
+ try {
274
+ const redirectUrl = new URL(url);
275
+ const serverUrl = new URL(serverHostname);
276
+ if (redirectUrl.hostname === serverUrl.hostname ||
277
+ redirectUrl.hostname === 'localhost') {
278
+ return true;
279
+ }
280
+ else {
281
+ return false;
282
+ }
283
+ }
284
+ catch (err) {
285
+ return false;
286
+ }
287
+ }
@@ -0,0 +1,98 @@
1
+ import * as bcrypt from 'bcrypt';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { clearExpiredSessions, getAccountDb } from '../account-db.js';
4
+ import { config } from '../load-config.js';
5
+ import { TOKEN_EXPIRATION_NEVER } from '../util/validate-user.js';
6
+ function isValidPassword(password) {
7
+ return password != null && password !== '';
8
+ }
9
+ function hashPassword(password) {
10
+ return bcrypt.hashSync(password, 12);
11
+ }
12
+ export function bootstrapPassword(password) {
13
+ if (!isValidPassword(password)) {
14
+ return { error: 'invalid-password' };
15
+ }
16
+ const hashed = hashPassword(password);
17
+ const accountDb = getAccountDb();
18
+ accountDb.transaction(() => {
19
+ accountDb.mutate('DELETE FROM auth WHERE method = ?', ['password']);
20
+ accountDb.mutate('UPDATE auth SET active = 0');
21
+ accountDb.mutate("INSERT INTO auth (method, display_name, extra_data, active) VALUES ('password', 'Password', ?, 1)", [hashed]);
22
+ });
23
+ return {};
24
+ }
25
+ export function loginWithPassword(password) {
26
+ if (!isValidPassword(password)) {
27
+ return { error: 'invalid-password' };
28
+ }
29
+ const accountDb = getAccountDb();
30
+ const { extra_data: passwordHash } = accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [
31
+ 'password',
32
+ ]) || {};
33
+ if (!passwordHash) {
34
+ return { error: 'invalid-password' };
35
+ }
36
+ const confirmed = bcrypt.compareSync(password, passwordHash);
37
+ if (!confirmed) {
38
+ return { error: 'invalid-password' };
39
+ }
40
+ const sessionRow = accountDb.first('SELECT * FROM sessions WHERE auth_method = ?', ['password']);
41
+ const token = sessionRow ? sessionRow.token : uuidv4();
42
+ const { totalOfUsers } = accountDb.first('SELECT count(*) as totalOfUsers FROM users');
43
+ let userId = null;
44
+ if (totalOfUsers === 0) {
45
+ userId = uuidv4();
46
+ accountDb.mutate('INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, 1, 1, ?)', [userId, '', '', 'ADMIN']);
47
+ }
48
+ else {
49
+ const { id: userIdFromDb } = accountDb.first('SELECT id FROM users WHERE user_name = ?', ['']);
50
+ userId = userIdFromDb;
51
+ if (!userId) {
52
+ return { error: 'user-not-found' };
53
+ }
54
+ }
55
+ let expiration = TOKEN_EXPIRATION_NEVER;
56
+ if (config.get('token_expiration') !== 'never' &&
57
+ config.get('token_expiration') !== 'openid-provider' &&
58
+ typeof config.get('token_expiration') === 'number') {
59
+ expiration =
60
+ Math.floor(Date.now() / 1000) + config.get('token_expiration') * 60;
61
+ }
62
+ if (!sessionRow) {
63
+ accountDb.mutate('INSERT INTO sessions (token, expires_at, user_id, auth_method) VALUES (?, ?, ?, ?)', [token, expiration, userId, 'password']);
64
+ }
65
+ else {
66
+ accountDb.mutate('UPDATE sessions SET user_id = ?, expires_at = ? WHERE token = ?', [userId, expiration, token]);
67
+ }
68
+ clearExpiredSessions();
69
+ return { token };
70
+ }
71
+ export function changePassword(newPassword) {
72
+ const accountDb = getAccountDb();
73
+ if (!isValidPassword(newPassword)) {
74
+ return { error: 'invalid-password' };
75
+ }
76
+ const hashed = hashPassword(newPassword);
77
+ accountDb.mutate("UPDATE auth SET extra_data = ? WHERE method = 'password'", [
78
+ hashed,
79
+ ]);
80
+ return {};
81
+ }
82
+ export function checkPassword(password) {
83
+ if (!isValidPassword(password)) {
84
+ return false;
85
+ }
86
+ const accountDb = getAccountDb();
87
+ const { extra_data: passwordHash } = accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [
88
+ 'password',
89
+ ]) || {};
90
+ if (!passwordHash) {
91
+ return false;
92
+ }
93
+ const confirmed = bcrypt.compareSync(password, passwordHash);
94
+ if (!confirmed) {
95
+ return false;
96
+ }
97
+ return true;
98
+ }
@@ -0,0 +1,125 @@
1
+ import express from 'express';
2
+ import { bootstrap, needsBootstrap, getLoginMethod, listLoginMethods, getUserInfo, getActiveLoginMethod, } from './account-db.js';
3
+ import { isValidRedirectUrl, loginWithOpenIdSetup } from './accounts/openid.js';
4
+ import { changePassword, loginWithPassword } from './accounts/password.js';
5
+ import { errorMiddleware, requestLoggerMiddleware, } from './util/middlewares.js';
6
+ import { validateAuthHeader, validateSession } from './util/validate-user.js';
7
+ const app = express();
8
+ app.use(express.json());
9
+ app.use(express.urlencoded({ extended: true }));
10
+ app.use(errorMiddleware);
11
+ app.use(requestLoggerMiddleware);
12
+ export { app as handlers };
13
+ // Non-authenticated endpoints:
14
+ //
15
+ // /needs-bootstrap
16
+ // /boostrap (special endpoint for setting up the instance, cant call again)
17
+ // /login
18
+ app.get('/needs-bootstrap', (req, res) => {
19
+ const availableLoginMethods = listLoginMethods();
20
+ res.send({
21
+ status: 'ok',
22
+ data: {
23
+ bootstrapped: !needsBootstrap(),
24
+ loginMethod: availableLoginMethods.length === 1
25
+ ? availableLoginMethods[0].method
26
+ : getLoginMethod(),
27
+ availableLoginMethods,
28
+ multiuser: getActiveLoginMethod() === 'openid',
29
+ },
30
+ });
31
+ });
32
+ app.post('/bootstrap', async (req, res) => {
33
+ const boot = await bootstrap(req.body);
34
+ if (boot?.error) {
35
+ res.status(400).send({ status: 'error', reason: boot?.error });
36
+ return;
37
+ }
38
+ res.send({ status: 'ok', data: boot });
39
+ });
40
+ app.get('/login-methods', (req, res) => {
41
+ const methods = listLoginMethods();
42
+ res.send({ status: 'ok', methods });
43
+ });
44
+ app.post('/login', async (req, res) => {
45
+ const loginMethod = getLoginMethod(req);
46
+ console.log('Logging in via ' + loginMethod);
47
+ let tokenRes = null;
48
+ switch (loginMethod) {
49
+ case 'header': {
50
+ const headerVal = req.get('x-actual-password') || '';
51
+ const obfuscated = '*'.repeat(headerVal.length) || 'No password provided.';
52
+ console.debug('HEADER VALUE: ' + obfuscated);
53
+ if (headerVal === '') {
54
+ res.send({ status: 'error', reason: 'invalid-header' });
55
+ return;
56
+ }
57
+ else {
58
+ if (validateAuthHeader(req)) {
59
+ tokenRes = loginWithPassword(headerVal);
60
+ }
61
+ else {
62
+ res.send({ status: 'error', reason: 'proxy-not-trusted' });
63
+ return;
64
+ }
65
+ }
66
+ break;
67
+ }
68
+ case 'openid': {
69
+ if (!isValidRedirectUrl(req.body.returnUrl)) {
70
+ res
71
+ .status(400)
72
+ .send({ status: 'error', reason: 'Invalid redirect URL' });
73
+ return;
74
+ }
75
+ const { error, url } = await loginWithOpenIdSetup(req.body.returnUrl, req.body.password);
76
+ if (error) {
77
+ res.status(400).send({ status: 'error', reason: error });
78
+ return;
79
+ }
80
+ res.send({ status: 'ok', data: { returnUrl: url } });
81
+ return;
82
+ }
83
+ default:
84
+ tokenRes = loginWithPassword(req.body.password);
85
+ break;
86
+ }
87
+ const { error, token } = tokenRes;
88
+ if (error) {
89
+ res.status(400).send({ status: 'error', reason: error });
90
+ return;
91
+ }
92
+ res.send({ status: 'ok', data: { token } });
93
+ });
94
+ app.post('/change-password', (req, res) => {
95
+ const session = validateSession(req, res);
96
+ if (!session)
97
+ return;
98
+ const { error } = changePassword(req.body.password);
99
+ if (error) {
100
+ res.status(400).send({ status: 'error', reason: error });
101
+ return;
102
+ }
103
+ res.send({ status: 'ok', data: {} });
104
+ });
105
+ app.get('/validate', (req, res) => {
106
+ const session = validateSession(req, res);
107
+ if (session) {
108
+ const user = getUserInfo(session.user_id);
109
+ if (!user) {
110
+ res.status(400).send({ status: 'error', reason: 'User not found' });
111
+ return;
112
+ }
113
+ res.send({
114
+ status: 'ok',
115
+ data: {
116
+ validated: true,
117
+ userName: user?.user_name,
118
+ permission: user?.role,
119
+ userId: session?.user_id,
120
+ displayName: user?.display_name,
121
+ loginMethod: session?.auth_method,
122
+ },
123
+ });
124
+ }
125
+ });