@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.
- package/{app.js → build/app.js} +4 -5
- package/build/bin/actual-server.js +101 -0
- package/build/migrations/1694360000000-create-folders.js +21 -0
- package/{migrations → build/migrations}/1694360479680-create-account-db.js +2 -4
- package/{migrations → build/migrations}/1694362247011-create-secret-table.js +2 -4
- package/build/migrations/1702667624000-rename-nordigen-secrets.js +9 -0
- package/{migrations → build/migrations}/1718889148000-openid.js +4 -10
- package/{migrations → build/migrations}/1719409568000-multiuser.js +10 -26
- package/build/src/account-db.js +182 -0
- package/build/src/accounts/openid.js +287 -0
- package/build/src/accounts/password.js +98 -0
- package/build/src/app-account.js +125 -0
- package/build/src/app-admin.js +317 -0
- package/build/src/app-admin.test.js +303 -0
- package/build/src/app-gocardless/app-gocardless.js +193 -0
- package/build/src/app-gocardless/bank-factory.js +84 -0
- package/build/src/app-gocardless/banks/abanca_caglesmm.js +17 -0
- package/build/src/app-gocardless/banks/abnamro_abnanl2a.js +37 -0
- package/build/src/app-gocardless/banks/american_express_aesudef1.js +32 -0
- package/build/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +22 -0
- package/build/src/app-gocardless/banks/bank.interface.js +1 -0
- package/build/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +25 -0
- package/build/src/app-gocardless/banks/bankinter_bkbkesmm.js +18 -0
- package/build/src/app-gocardless/banks/belfius_gkccbebb.js +13 -0
- package/build/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +48 -0
- package/build/src/app-gocardless/banks/bnp_be_gebabebb.js +64 -0
- package/build/src/app-gocardless/banks/boursobank_bousfrppxxx.js +73 -0
- package/build/src/app-gocardless/banks/cbc_cregbebb.js +27 -0
- package/build/src/app-gocardless/banks/commerzbank_cobadeff.js +43 -0
- package/build/src/app-gocardless/banks/danskebank_dabno22.js +26 -0
- package/build/src/app-gocardless/banks/direkt_heladef1822.js +13 -0
- package/build/src/app-gocardless/banks/easybank_bawaatww.js +42 -0
- package/build/src/app-gocardless/banks/entercard_swednokk.js +28 -0
- package/build/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +34 -0
- package/build/src/app-gocardless/banks/hype_hyeeit22.js +63 -0
- package/build/src/app-gocardless/banks/ing_ingbrobu.js +56 -0
- package/build/src/app-gocardless/banks/ing_ingddeff.js +34 -0
- package/build/src/app-gocardless/banks/ing_pl_ingbplpw.js +29 -0
- package/build/src/app-gocardless/banks/integration-bank.js +78 -0
- package/build/src/app-gocardless/banks/isybank_itbbitmm.js +13 -0
- package/build/src/app-gocardless/banks/kbc_kredbebb.js +26 -0
- package/build/src/app-gocardless/banks/lhv-lhvbee22.js +24 -0
- package/build/src/app-gocardless/banks/mbank_retail_brexplpw.js +41 -0
- package/build/src/app-gocardless/banks/nationwide_naiagb21.js +32 -0
- package/build/src/app-gocardless/banks/nbg_ethngraaxxx.js +39 -0
- package/build/src/app-gocardless/banks/norwegian_xx_norwnok1.js +61 -0
- package/build/src/app-gocardless/banks/revolut_revolt21.js +20 -0
- package/build/src/app-gocardless/banks/sandboxfinance_sfin0000.js +21 -0
- package/build/src/app-gocardless/banks/seb_kort_bank_ab.js +63 -0
- package/build/src/app-gocardless/banks/seb_privat.js +19 -0
- package/build/src/app-gocardless/banks/sparnord_spnodk22.js +19 -0
- package/build/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +48 -0
- package/build/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +25 -0
- package/build/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +14 -0
- package/build/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +36 -0
- package/build/src/app-gocardless/banks/swedbank_habalv22.js +30 -0
- package/build/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +17 -0
- package/build/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +45 -0
- package/build/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +41 -0
- package/build/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +16 -0
- package/build/src/app-gocardless/banks/tests/boursobank_bousfrppxxx.spec.js +102 -0
- package/build/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +24 -0
- package/build/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +105 -0
- package/build/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +36 -0
- package/build/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +159 -0
- package/build/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +267 -0
- package/build/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +186 -0
- package/build/src/app-gocardless/banks/tests/integration_bank.spec.js +127 -0
- package/build/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +24 -0
- package/build/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +50 -0
- package/build/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +156 -0
- package/build/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +64 -0
- package/build/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +36 -0
- package/build/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +30 -0
- package/build/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +112 -0
- package/build/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +214 -0
- package/build/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +60 -0
- package/build/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +45 -0
- package/build/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +36 -0
- package/{src → build/src}/app-gocardless/banks/util/escape-regexp.js +1 -1
- package/{src → build/src}/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js +11 -16
- package/build/src/app-gocardless/banks/virgin_nrnbgb22.js +31 -0
- package/build/src/app-gocardless/errors.js +67 -0
- package/build/src/app-gocardless/gocardless-node.types.js +1 -0
- package/build/src/app-gocardless/gocardless.types.js +1 -0
- package/build/src/app-gocardless/services/gocardless-service.js +504 -0
- package/build/src/app-gocardless/services/tests/fixtures.js +165 -0
- package/build/src/app-gocardless/services/tests/gocardless-service.spec.js +387 -0
- package/build/src/app-gocardless/tests/bank-factory.spec.js +13 -0
- package/build/src/app-gocardless/tests/utils.spec.js +158 -0
- package/build/src/app-gocardless/util/handle-error.js +15 -0
- package/build/src/app-gocardless/utils.js +41 -0
- package/build/src/app-openid.js +83 -0
- package/build/src/app-pluggyai/app-pluggyai.js +164 -0
- package/build/src/app-pluggyai/pluggyai-service.js +97 -0
- package/build/src/app-secrets.js +48 -0
- package/build/src/app-simplefin/app-simplefin.js +335 -0
- package/build/src/app-sync/errors.js +12 -0
- package/build/src/app-sync/services/files-service.js +158 -0
- package/build/src/app-sync/tests/services/files-service.test.js +192 -0
- package/build/src/app-sync/validation.js +65 -0
- package/build/src/app-sync.js +302 -0
- package/build/src/app-sync.test.js +655 -0
- package/build/src/app.js +138 -0
- package/build/src/config-types.js +1 -0
- package/build/src/db.js +50 -0
- package/build/src/load-config.js +274 -0
- package/build/src/migrations.js +23 -0
- package/build/src/scripts/disable-openid.js +31 -0
- package/build/src/scripts/enable-openid.js +36 -0
- package/build/src/scripts/health-check.js +16 -0
- package/build/src/scripts/reset-password.js +40 -0
- package/build/src/scripts/run-migrations.js +6 -0
- package/build/src/secrets.test.js +68 -0
- package/build/src/services/secrets-service.js +79 -0
- package/build/src/services/user-service.js +201 -0
- package/build/src/sync-simple.js +68 -0
- package/{src → build/src}/util/hash.js +1 -2
- package/build/src/util/middlewares.js +49 -0
- package/{src → build/src}/util/paths.js +3 -6
- package/build/src/util/payee-name.js +37 -0
- package/build/src/util/prompt.js +70 -0
- package/build/src/util/title/index.js +43 -0
- package/build/src/util/title/lower-case.js +90 -0
- package/build/src/util/title/specials.js +21 -0
- package/build/src/util/validate-user.js +55 -0
- package/package.json +32 -36
- package/bin/actual-server.js +0 -117
- package/migrations/1694360000000-create-folders.js +0 -25
- package/migrations/1702667624000-rename-nordigen-secrets.js +0 -19
- package/src/account-db.js +0 -239
- package/src/accounts/openid.js +0 -368
- package/src/accounts/password.js +0 -149
- package/src/app-account.js +0 -155
- package/src/app-admin.js +0 -410
- package/src/app-admin.test.js +0 -381
- package/src/app-gocardless/app-gocardless.js +0 -274
- package/src/app-gocardless/bank-factory.js +0 -91
- package/src/app-gocardless/banks/abanca_caglesmm.js +0 -22
- package/src/app-gocardless/banks/abnamro_abnanl2a.js +0 -57
- package/src/app-gocardless/banks/american_express_aesudef1.js +0 -40
- package/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +0 -31
- package/src/app-gocardless/banks/bank.interface.ts +0 -51
- package/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +0 -39
- package/src/app-gocardless/banks/bankinter_bkbkesmm.js +0 -24
- package/src/app-gocardless/banks/belfius_gkccbebb.js +0 -17
- package/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +0 -61
- package/src/app-gocardless/banks/bnp_be_gebabebb.js +0 -73
- package/src/app-gocardless/banks/cbc_cregbebb.js +0 -34
- package/src/app-gocardless/banks/commerzbank_cobadeff.js +0 -54
- package/src/app-gocardless/banks/danskebank_dabno22.js +0 -39
- package/src/app-gocardless/banks/direkt_heladef1822.js +0 -18
- package/src/app-gocardless/banks/easybank_bawaatww.js +0 -50
- package/src/app-gocardless/banks/entercard_swednokk.js +0 -40
- package/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +0 -46
- package/src/app-gocardless/banks/hype_hyeeit22.js +0 -74
- package/src/app-gocardless/banks/ing_ingbrobu.js +0 -70
- package/src/app-gocardless/banks/ing_ingddeff.js +0 -47
- package/src/app-gocardless/banks/ing_pl_ingbplpw.js +0 -46
- package/src/app-gocardless/banks/integration-bank.js +0 -115
- package/src/app-gocardless/banks/isybank_itbbitmm.js +0 -18
- package/src/app-gocardless/banks/kbc_kredbebb.js +0 -33
- package/src/app-gocardless/banks/lhv-lhvbee22.js +0 -36
- package/src/app-gocardless/banks/mbank_retail_brexplpw.js +0 -56
- package/src/app-gocardless/banks/nationwide_naiagb21.js +0 -46
- package/src/app-gocardless/banks/nbg_ethngraaxxx.js +0 -51
- package/src/app-gocardless/banks/norwegian_xx_norwnok1.js +0 -74
- package/src/app-gocardless/banks/revolut_revolt21.js +0 -37
- package/src/app-gocardless/banks/sandboxfinance_sfin0000.js +0 -28
- package/src/app-gocardless/banks/seb_kort_bank_ab.js +0 -59
- package/src/app-gocardless/banks/seb_privat.js +0 -29
- package/src/app-gocardless/banks/sparnord_spnodk22.js +0 -24
- package/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +0 -61
- package/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +0 -30
- package/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +0 -19
- package/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +0 -50
- package/src/app-gocardless/banks/swedbank_habalv22.js +0 -47
- package/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +0 -21
- package/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +0 -61
- package/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +0 -53
- package/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +0 -22
- package/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +0 -34
- package/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +0 -133
- package/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +0 -54
- package/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +0 -206
- package/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +0 -302
- package/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +0 -202
- package/src/app-gocardless/banks/tests/integration_bank.spec.js +0 -156
- package/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +0 -38
- package/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +0 -68
- package/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +0 -171
- package/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +0 -105
- package/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +0 -48
- package/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +0 -42
- package/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +0 -133
- package/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +0 -255
- package/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +0 -100
- package/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +0 -57
- package/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +0 -54
- package/src/app-gocardless/banks/virgin_nrnbgb22.js +0 -39
- package/src/app-gocardless/errors.js +0 -84
- package/src/app-gocardless/gocardless-node.types.ts +0 -497
- package/src/app-gocardless/gocardless.types.ts +0 -93
- package/src/app-gocardless/link.html +0 -18
- package/src/app-gocardless/services/gocardless-service.js +0 -620
- package/src/app-gocardless/services/tests/fixtures.js +0 -181
- package/src/app-gocardless/services/tests/gocardless-service.spec.js +0 -537
- package/src/app-gocardless/tests/bank-factory.spec.js +0 -20
- package/src/app-gocardless/tests/utils.spec.js +0 -162
- package/src/app-gocardless/util/handle-error.js +0 -16
- package/src/app-gocardless/utils.js +0 -45
- package/src/app-openid.js +0 -108
- package/src/app-pluggyai/app-pluggyai.js +0 -215
- package/src/app-pluggyai/pluggyai-service.js +0 -120
- package/src/app-secrets.js +0 -61
- package/src/app-simplefin/app-simplefin.js +0 -405
- package/src/app-sync/errors.js +0 -13
- package/src/app-sync/services/files-service.js +0 -243
- package/src/app-sync/tests/services/files-service.test.js +0 -247
- package/src/app-sync/validation.js +0 -77
- package/src/app-sync.js +0 -391
- package/src/app-sync.test.js +0 -877
- package/src/app.js +0 -149
- package/src/config-types.ts +0 -44
- package/src/db.js +0 -58
- package/src/load-config.js +0 -307
- package/src/migrations.js +0 -36
- package/src/run-migrations.js +0 -8
- package/src/scripts/disable-openid.js +0 -44
- package/src/scripts/enable-openid.js +0 -53
- package/src/scripts/health-check.js +0 -23
- package/src/scripts/reset-password.js +0 -51
- package/src/secrets.test.js +0 -83
- package/src/services/secrets-service.js +0 -94
- package/src/services/user-service.js +0 -272
- package/src/sync-simple.js +0 -95
- package/src/util/middlewares.js +0 -62
- package/src/util/payee-name.js +0 -45
- package/src/util/prompt.js +0 -88
- package/src/util/title/index.js +0 -59
- package/src/util/title/lower-case.js +0 -93
- package/src/util/title/specials.js +0 -21
- package/src/util/validate-user.js +0 -68
- /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
|
+
});
|