@actual-app/sync-server 25.4.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +12 -0
- package/README.md +19 -0
- package/app.js +11 -0
- package/babel.config.json +3 -0
- package/bin/@actual-app/sync-server +55 -0
- package/docker/alpine.Dockerfile +62 -0
- package/docker/ubuntu.Dockerfile +63 -0
- package/docker-compose.yml +29 -0
- package/jest.config.json +19 -0
- package/jest.global-setup.js +101 -0
- package/jest.global-teardown.js +6 -0
- package/migrations/1694360000000-create-folders.js +25 -0
- package/migrations/1694360479680-create-account-db.js +30 -0
- package/migrations/1694362247011-create-secret-table.js +16 -0
- package/migrations/1702667624000-rename-nordigen-secrets.js +19 -0
- package/migrations/1718889148000-openid.js +41 -0
- package/migrations/1719409568000-multiuser.js +116 -0
- package/package.json +64 -0
- package/src/account-db.js +239 -0
- package/src/accounts/openid.js +361 -0
- package/src/accounts/password.js +149 -0
- package/src/app-account.js +155 -0
- package/src/app-admin.js +410 -0
- package/src/app-admin.test.js +381 -0
- package/src/app-gocardless/README.md +198 -0
- package/src/app-gocardless/app-gocardless.js +274 -0
- package/src/app-gocardless/bank-factory.js +91 -0
- package/src/app-gocardless/banks/abanca_caglesmm.js +22 -0
- package/src/app-gocardless/banks/abnamro_abnanl2a.js +57 -0
- package/src/app-gocardless/banks/american_express_aesudef1.js +40 -0
- package/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +31 -0
- package/src/app-gocardless/banks/bank.interface.ts +51 -0
- package/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +39 -0
- package/src/app-gocardless/banks/bankinter_bkbkesmm.js +24 -0
- package/src/app-gocardless/banks/belfius_gkccbebb.js +17 -0
- package/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +61 -0
- package/src/app-gocardless/banks/bnp_be_gebabebb.js +73 -0
- package/src/app-gocardless/banks/cbc_cregbebb.js +34 -0
- package/src/app-gocardless/banks/commerzbank_cobadeff.js +51 -0
- package/src/app-gocardless/banks/danskebank_dabno22.js +39 -0
- package/src/app-gocardless/banks/direkt_heladef1822.js +18 -0
- package/src/app-gocardless/banks/easybank_bawaatww.js +50 -0
- package/src/app-gocardless/banks/entercard_swednokk.js +40 -0
- package/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +46 -0
- package/src/app-gocardless/banks/hype_hyeeit22.js +74 -0
- package/src/app-gocardless/banks/ing_ingbrobu.js +70 -0
- package/src/app-gocardless/banks/ing_ingddeff.js +47 -0
- package/src/app-gocardless/banks/ing_pl_ingbplpw.js +46 -0
- package/src/app-gocardless/banks/integration-bank.js +115 -0
- package/src/app-gocardless/banks/isybank_itbbitmm.js +18 -0
- package/src/app-gocardless/banks/kbc_kredbebb.js +33 -0
- package/src/app-gocardless/banks/lhv-lhvbee22.js +36 -0
- package/src/app-gocardless/banks/mbank_retail_brexplpw.js +56 -0
- package/src/app-gocardless/banks/nationwide_naiagb21.js +46 -0
- package/src/app-gocardless/banks/nbg_ethngraaxxx.js +51 -0
- package/src/app-gocardless/banks/norwegian_xx_norwnok1.js +74 -0
- package/src/app-gocardless/banks/revolut_revolt21.js +37 -0
- package/src/app-gocardless/banks/sandboxfinance_sfin0000.js +28 -0
- package/src/app-gocardless/banks/seb_kort_bank_ab.js +58 -0
- package/src/app-gocardless/banks/seb_privat.js +29 -0
- package/src/app-gocardless/banks/sparnord_spnodk22.js +24 -0
- package/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +61 -0
- package/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +30 -0
- package/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +19 -0
- package/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +50 -0
- package/src/app-gocardless/banks/swedbank_habalv22.js +47 -0
- package/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +21 -0
- package/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +61 -0
- package/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +53 -0
- package/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +22 -0
- package/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +34 -0
- package/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +110 -0
- package/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +54 -0
- package/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +206 -0
- package/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +302 -0
- package/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +202 -0
- package/src/app-gocardless/banks/tests/integration_bank.spec.js +158 -0
- package/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +38 -0
- package/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +68 -0
- package/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +171 -0
- package/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +105 -0
- package/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +48 -0
- package/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +42 -0
- package/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +133 -0
- package/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +256 -0
- package/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +102 -0
- package/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +57 -0
- package/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +54 -0
- package/src/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js +36 -0
- package/src/app-gocardless/banks/virgin_nrnbgb22.js +39 -0
- package/src/app-gocardless/errors.js +84 -0
- package/src/app-gocardless/gocardless-node.types.ts +497 -0
- package/src/app-gocardless/gocardless.types.ts +93 -0
- package/src/app-gocardless/link.html +18 -0
- package/src/app-gocardless/services/gocardless-service.js +620 -0
- package/src/app-gocardless/services/tests/fixtures.js +181 -0
- package/src/app-gocardless/services/tests/gocardless-service.spec.js +537 -0
- package/src/app-gocardless/tests/bank-factory.spec.js +20 -0
- package/src/app-gocardless/tests/utils.spec.js +162 -0
- package/src/app-gocardless/util/handle-error.js +16 -0
- package/src/app-gocardless/utils.js +45 -0
- package/src/app-openid.js +108 -0
- package/src/app-pluggyai/app-pluggyai.js +215 -0
- package/src/app-pluggyai/pluggyai-service.js +120 -0
- package/src/app-secrets.js +61 -0
- package/src/app-simplefin/app-simplefin.js +418 -0
- package/src/app-sync/errors.js +13 -0
- package/src/app-sync/services/files-service.js +243 -0
- package/src/app-sync/tests/services/files-service.test.js +250 -0
- package/src/app-sync/validation.js +77 -0
- package/src/app-sync.js +391 -0
- package/src/app-sync.test.js +877 -0
- package/src/app.js +145 -0
- package/src/config-types.ts +44 -0
- package/src/db.js +58 -0
- package/src/load-config.js +307 -0
- package/src/migrations.js +36 -0
- package/src/run-migrations.js +8 -0
- package/src/scripts/disable-openid.js +44 -0
- package/src/scripts/enable-openid.js +53 -0
- package/src/scripts/health-check.js +23 -0
- package/src/scripts/reset-password.js +51 -0
- package/src/secrets.test.js +83 -0
- package/src/services/secrets-service.js +94 -0
- package/src/services/user-service.js +272 -0
- package/src/sql/messages.sql +9 -0
- package/src/sync-simple.js +95 -0
- package/src/util/hash.js +5 -0
- package/src/util/middlewares.js +62 -0
- package/src/util/paths.js +13 -0
- package/src/util/payee-name.js +45 -0
- package/src/util/prompt.js +88 -0
- package/src/util/title/index.js +59 -0
- package/src/util/title/lower-case.js +93 -0
- package/src/util/title/specials.js +21 -0
- package/src/util/validate-user.js +68 -0
- package/tsconfig.json +21 -0
package/src/app.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import bodyParser from 'body-parser';
|
|
4
|
+
import cors from 'cors';
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import actuator from 'express-actuator';
|
|
7
|
+
import rateLimit from 'express-rate-limit';
|
|
8
|
+
|
|
9
|
+
import { bootstrap } from './account-db.js';
|
|
10
|
+
import * as accountApp from './app-account.js';
|
|
11
|
+
import * as adminApp from './app-admin.js';
|
|
12
|
+
import * as goCardlessApp from './app-gocardless/app-gocardless.js';
|
|
13
|
+
import * as openidApp from './app-openid.js';
|
|
14
|
+
import * as pluggai from './app-pluggyai/app-pluggyai.js';
|
|
15
|
+
import * as secretApp from './app-secrets.js';
|
|
16
|
+
import * as simpleFinApp from './app-simplefin/app-simplefin.js';
|
|
17
|
+
import * as syncApp from './app-sync.js';
|
|
18
|
+
import { config } from './load-config.js';
|
|
19
|
+
|
|
20
|
+
const app = express();
|
|
21
|
+
|
|
22
|
+
process.on('unhandledRejection', reason => {
|
|
23
|
+
console.log('Rejection:', reason);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.disable('x-powered-by');
|
|
27
|
+
app.use(cors());
|
|
28
|
+
app.set('trust proxy', config.get('trustedProxies'));
|
|
29
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
30
|
+
app.use(
|
|
31
|
+
rateLimit({
|
|
32
|
+
windowMs: 60 * 1000,
|
|
33
|
+
max: 500,
|
|
34
|
+
legacyHeaders: false,
|
|
35
|
+
standardHeaders: true,
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
app.use(
|
|
41
|
+
bodyParser.json({ limit: `${config.get('upload.fileSizeLimitMB')}mb` }),
|
|
42
|
+
);
|
|
43
|
+
app.use(
|
|
44
|
+
bodyParser.raw({
|
|
45
|
+
type: 'application/actual-sync',
|
|
46
|
+
limit: `${config.get('upload.fileSizeSyncLimitMB')}mb`,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
app.use(
|
|
50
|
+
bodyParser.raw({
|
|
51
|
+
type: 'application/encrypted-file',
|
|
52
|
+
limit: `${config.get('upload.syncEncryptedFileSizeLimitMB')}mb`,
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
app.use('/sync', syncApp.handlers);
|
|
57
|
+
app.use('/account', accountApp.handlers);
|
|
58
|
+
app.use('/gocardless', goCardlessApp.handlers);
|
|
59
|
+
app.use('/simplefin', simpleFinApp.handlers);
|
|
60
|
+
app.use('/pluggyai', pluggai.handlers);
|
|
61
|
+
app.use('/secret', secretApp.handlers);
|
|
62
|
+
|
|
63
|
+
app.use('/admin', adminApp.handlers);
|
|
64
|
+
app.use('/openid', openidApp.handlers);
|
|
65
|
+
|
|
66
|
+
app.get('/mode', (req, res) => {
|
|
67
|
+
res.send(config.get('mode'));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
app.use(actuator()); // Provides /health, /metrics, /info
|
|
71
|
+
|
|
72
|
+
// The web frontend
|
|
73
|
+
app.use((req, res, next) => {
|
|
74
|
+
res.set('Cross-Origin-Opener-Policy', 'same-origin');
|
|
75
|
+
res.set('Cross-Origin-Embedder-Policy', 'require-corp');
|
|
76
|
+
next();
|
|
77
|
+
});
|
|
78
|
+
if (process.env.NODE_ENV === 'development') {
|
|
79
|
+
console.log(
|
|
80
|
+
'Running in development mode - Proxying frontend routes to React Dev Server',
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Imported within Dev block to allow dev dependency in package.json (reduces package size in production)
|
|
84
|
+
const httpProxyMiddleware = await import('http-proxy-middleware');
|
|
85
|
+
|
|
86
|
+
app.use(
|
|
87
|
+
httpProxyMiddleware.createProxyMiddleware({
|
|
88
|
+
target: 'http://localhost:3001',
|
|
89
|
+
changeOrigin: true,
|
|
90
|
+
ws: true,
|
|
91
|
+
logLevel: 'debug',
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
console.log('Running in production mode - Serving static React app');
|
|
96
|
+
|
|
97
|
+
app.use(express.static(config.get('webRoot'), { index: false }));
|
|
98
|
+
app.get('/*', (req, res) =>
|
|
99
|
+
res.sendFile(config.get('webRoot') + '/index.html'),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseHTTPSConfig(value) {
|
|
104
|
+
if (value.startsWith('-----BEGIN')) {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
return fs.readFileSync(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function run() {
|
|
111
|
+
if (config.openId) {
|
|
112
|
+
console.log('OpenID configuration found. Preparing server to use it');
|
|
113
|
+
try {
|
|
114
|
+
const { error } = await bootstrap({ openId: config.openId }, true);
|
|
115
|
+
if (error) {
|
|
116
|
+
console.log(error);
|
|
117
|
+
} else {
|
|
118
|
+
console.log('OpenID configured!');
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (config.get('https.key') && config.get('https.cert')) {
|
|
126
|
+
const https = await import('node:https');
|
|
127
|
+
const httpsOptions = {
|
|
128
|
+
...config.https,
|
|
129
|
+
key: parseHTTPSConfig(config.get('https.key')),
|
|
130
|
+
cert: parseHTTPSConfig(config.get('https.cert')),
|
|
131
|
+
};
|
|
132
|
+
https
|
|
133
|
+
.createServer(httpsOptions, app)
|
|
134
|
+
.listen(config.get('port'), config.get('hostname'));
|
|
135
|
+
} else {
|
|
136
|
+
app.listen(config.get('port'), config.get('hostname'));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Signify to any parent process that the server has started. Used in electron desktop app
|
|
140
|
+
process.parentPort?.postMessage({ type: 'server-started' });
|
|
141
|
+
|
|
142
|
+
console.log(
|
|
143
|
+
'Listening on ' + config.get('hostname') + ':' + config.get('port') + '...',
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ServerOptions } from 'https';
|
|
2
|
+
|
|
3
|
+
type LoginMethod = 'password' | 'header' | 'openid';
|
|
4
|
+
|
|
5
|
+
export interface Config {
|
|
6
|
+
mode: 'test' | 'development';
|
|
7
|
+
loginMethod?: LoginMethod;
|
|
8
|
+
allowedLoginMethods: LoginMethod[];
|
|
9
|
+
trustedProxies: string[];
|
|
10
|
+
trustedAuthProxies?: string[];
|
|
11
|
+
dataDir: string;
|
|
12
|
+
projectRoot: string;
|
|
13
|
+
port: number;
|
|
14
|
+
hostname: string;
|
|
15
|
+
serverFiles: string;
|
|
16
|
+
userFiles: string;
|
|
17
|
+
webRoot: string;
|
|
18
|
+
https?: {
|
|
19
|
+
key: string;
|
|
20
|
+
cert: string;
|
|
21
|
+
} & ServerOptions;
|
|
22
|
+
upload?: {
|
|
23
|
+
fileSizeSyncLimitMB: number;
|
|
24
|
+
syncEncryptedFileSizeLimitMB: number;
|
|
25
|
+
fileSizeLimitMB: number;
|
|
26
|
+
};
|
|
27
|
+
openId?: {
|
|
28
|
+
issuer:
|
|
29
|
+
| string
|
|
30
|
+
| {
|
|
31
|
+
name: string;
|
|
32
|
+
authorization_endpoint: string;
|
|
33
|
+
token_endpoint: string;
|
|
34
|
+
userinfo_endpoint: string;
|
|
35
|
+
};
|
|
36
|
+
client_id: string;
|
|
37
|
+
client_secret: string;
|
|
38
|
+
server_hostname: string;
|
|
39
|
+
authMethod?: 'openid' | 'oauth2';
|
|
40
|
+
};
|
|
41
|
+
token_expiration?: 'never' | 'openid-provider' | number;
|
|
42
|
+
enforceOpenId: boolean;
|
|
43
|
+
userCreationMode?: 'manual' | 'login';
|
|
44
|
+
}
|
package/src/db.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
|
|
3
|
+
class WrappedDatabase {
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} sql
|
|
10
|
+
* @param {string[]} params
|
|
11
|
+
*/
|
|
12
|
+
all(sql, params = []) {
|
|
13
|
+
const stmt = this.db.prepare(sql);
|
|
14
|
+
return stmt.all(...params);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} sql
|
|
19
|
+
* @param {string[]} params
|
|
20
|
+
*/
|
|
21
|
+
first(sql, params = []) {
|
|
22
|
+
const rows = this.all(sql, params);
|
|
23
|
+
return rows.length === 0 ? null : rows[0];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} sql
|
|
28
|
+
*/
|
|
29
|
+
exec(sql) {
|
|
30
|
+
return this.db.exec(sql);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} sql
|
|
35
|
+
* @param {string[]} params
|
|
36
|
+
*/
|
|
37
|
+
mutate(sql, params = []) {
|
|
38
|
+
const stmt = this.db.prepare(sql);
|
|
39
|
+
const info = stmt.run(...params);
|
|
40
|
+
return { changes: info.changes, insertId: info.lastInsertRowid };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {() => void} fn
|
|
45
|
+
*/
|
|
46
|
+
transaction(fn) {
|
|
47
|
+
return this.db.transaction(fn)();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
close() {
|
|
51
|
+
this.db.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @param {string} filename */
|
|
56
|
+
export function openDatabase(filename) {
|
|
57
|
+
return new WrappedDatabase(new Database(filename));
|
|
58
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import convict from 'convict';
|
|
7
|
+
import createDebug from 'debug';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const debug = createDebug('actual:config');
|
|
11
|
+
const debugSensitive = createDebug('actual-sensitive:config');
|
|
12
|
+
|
|
13
|
+
const projectRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
14
|
+
const defaultDataDir = process.env.ACTUAL_DATA_DIR
|
|
15
|
+
? process.env.ACTUAL_DATA_DIR
|
|
16
|
+
: fs.existsSync('/data')
|
|
17
|
+
? '/data'
|
|
18
|
+
: projectRoot;
|
|
19
|
+
|
|
20
|
+
debug(`Project root: '${projectRoot}'`);
|
|
21
|
+
|
|
22
|
+
export const sqlDir = path.join(projectRoot, 'src', 'sql');
|
|
23
|
+
|
|
24
|
+
const actualAppWebBuildPath = path.join(
|
|
25
|
+
path.dirname(require.resolve('@actual-app/web/package.json')),
|
|
26
|
+
'build',
|
|
27
|
+
);
|
|
28
|
+
debug(`Actual web build path: '${actualAppWebBuildPath}'`);
|
|
29
|
+
|
|
30
|
+
// Custom formats
|
|
31
|
+
convict.addFormat({
|
|
32
|
+
name: 'tokenExpiration',
|
|
33
|
+
validate(val) {
|
|
34
|
+
if (val === 'never' || val === 'openid-provider') return;
|
|
35
|
+
if (typeof val === 'number' && Number.isFinite(val) && val >= 0) return;
|
|
36
|
+
throw new Error(`Invalid token_expiration value: ${val}`);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Main config schema
|
|
41
|
+
const configSchema = convict({
|
|
42
|
+
env: {
|
|
43
|
+
doc: 'The application environment.',
|
|
44
|
+
format: ['production', 'development', 'test'],
|
|
45
|
+
default: 'development',
|
|
46
|
+
env: 'NODE_ENV',
|
|
47
|
+
},
|
|
48
|
+
mode: {
|
|
49
|
+
doc: 'Application mode.',
|
|
50
|
+
format: ['test', 'development'],
|
|
51
|
+
default: process.env.NODE_ENV === 'test' ? 'test' : 'development',
|
|
52
|
+
},
|
|
53
|
+
projectRoot: {
|
|
54
|
+
doc: 'Project root directory.',
|
|
55
|
+
format: String,
|
|
56
|
+
default: projectRoot,
|
|
57
|
+
},
|
|
58
|
+
dataDir: {
|
|
59
|
+
doc: 'Default data directory.',
|
|
60
|
+
format: String,
|
|
61
|
+
default: process.env.NODE_ENV === 'test' ? projectRoot : defaultDataDir,
|
|
62
|
+
env: 'ACTUAL_DATA_DIR',
|
|
63
|
+
},
|
|
64
|
+
port: {
|
|
65
|
+
doc: 'Port to run the server on.',
|
|
66
|
+
format: 'port',
|
|
67
|
+
default: process.env.PORT ? process.env.PORT : 5006,
|
|
68
|
+
env: 'ACTUAL_PORT',
|
|
69
|
+
},
|
|
70
|
+
hostname: {
|
|
71
|
+
doc: 'Server hostname.',
|
|
72
|
+
format: String,
|
|
73
|
+
default: '::',
|
|
74
|
+
env: 'ACTUAL_HOSTNAME',
|
|
75
|
+
},
|
|
76
|
+
serverFiles: {
|
|
77
|
+
doc: 'Path to server files.',
|
|
78
|
+
format: String,
|
|
79
|
+
default:
|
|
80
|
+
process.env.NODE_ENV === 'test'
|
|
81
|
+
? path.join(projectRoot, 'test-server-files')
|
|
82
|
+
: path.join(defaultDataDir, 'server-files'),
|
|
83
|
+
env: 'ACTUAL_SERVER_FILES',
|
|
84
|
+
},
|
|
85
|
+
userFiles: {
|
|
86
|
+
doc: 'Path to user files.',
|
|
87
|
+
format: String,
|
|
88
|
+
default:
|
|
89
|
+
process.env.NODE_ENV === 'test'
|
|
90
|
+
? path.join(projectRoot, 'test-user-files')
|
|
91
|
+
: path.join(defaultDataDir, 'user-files'),
|
|
92
|
+
env: 'ACTUAL_USER_FILES',
|
|
93
|
+
},
|
|
94
|
+
webRoot: {
|
|
95
|
+
doc: 'Web root directory.',
|
|
96
|
+
format: String,
|
|
97
|
+
default: actualAppWebBuildPath,
|
|
98
|
+
env: 'ACTUAL_WEB_ROOT',
|
|
99
|
+
},
|
|
100
|
+
loginMethod: {
|
|
101
|
+
doc: 'Authentication method.',
|
|
102
|
+
format: ['password', 'header', 'openid'],
|
|
103
|
+
default: 'password',
|
|
104
|
+
env: 'ACTUAL_LOGIN_METHOD',
|
|
105
|
+
},
|
|
106
|
+
allowedLoginMethods: {
|
|
107
|
+
doc: 'Allowed authentication methods.',
|
|
108
|
+
format: Array,
|
|
109
|
+
default: ['password', 'header', 'openid'],
|
|
110
|
+
env: 'ACTUAL_ALLOWED_LOGIN_METHODS',
|
|
111
|
+
},
|
|
112
|
+
trustedProxies: {
|
|
113
|
+
doc: 'List of trusted proxies.',
|
|
114
|
+
format: Array,
|
|
115
|
+
default: [
|
|
116
|
+
'10.0.0.0/8',
|
|
117
|
+
'172.16.0.0/12',
|
|
118
|
+
'192.168.0.0/16',
|
|
119
|
+
'fc00::/7',
|
|
120
|
+
'::1/128',
|
|
121
|
+
],
|
|
122
|
+
env: 'ACTUAL_TRUSTED_PROXIES',
|
|
123
|
+
},
|
|
124
|
+
trustedAuthProxies: {
|
|
125
|
+
doc: 'List of trusted auth proxies.',
|
|
126
|
+
format: Array,
|
|
127
|
+
default: [],
|
|
128
|
+
env: 'ACTUAL_TRUSTED_AUTH_PROXIES',
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
https: {
|
|
132
|
+
doc: 'HTTPS configuration.',
|
|
133
|
+
|
|
134
|
+
key: {
|
|
135
|
+
doc: 'HTTPS Certificate key',
|
|
136
|
+
format: String,
|
|
137
|
+
default: '',
|
|
138
|
+
env: 'ACTUAL_HTTPS_KEY',
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
cert: {
|
|
142
|
+
doc: 'HTTPS Certificate',
|
|
143
|
+
format: String,
|
|
144
|
+
default: '',
|
|
145
|
+
env: 'ACTUAL_HTTPS_CERT',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
upload: {
|
|
150
|
+
doc: 'Upload configuration.',
|
|
151
|
+
|
|
152
|
+
fileSizeSyncLimitMB: {
|
|
153
|
+
doc: 'Sync file size limit (in MB)',
|
|
154
|
+
format: 'nat',
|
|
155
|
+
default: 20,
|
|
156
|
+
env: 'ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB',
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
syncEncryptedFileSizeLimitMB: {
|
|
160
|
+
doc: 'Encrypted Sync file size limit (in MB)',
|
|
161
|
+
format: 'nat',
|
|
162
|
+
default: 50,
|
|
163
|
+
env: 'ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB',
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
fileSizeLimitMB: {
|
|
167
|
+
doc: 'General file size limit (in MB)',
|
|
168
|
+
format: 'nat',
|
|
169
|
+
default: 20,
|
|
170
|
+
env: 'ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB',
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
openId: {
|
|
175
|
+
doc: 'OpenID authentication settings.',
|
|
176
|
+
|
|
177
|
+
discoveryURL: {
|
|
178
|
+
doc: 'OpenID Provider discovery URL.',
|
|
179
|
+
format: String,
|
|
180
|
+
default: '',
|
|
181
|
+
env: 'ACTUAL_OPENID_DISCOVERY_URL',
|
|
182
|
+
},
|
|
183
|
+
issuer: {
|
|
184
|
+
doc: 'OpenID issuer',
|
|
185
|
+
format: Object,
|
|
186
|
+
default: {},
|
|
187
|
+
name: {
|
|
188
|
+
doc: 'Name of the provider',
|
|
189
|
+
default: '',
|
|
190
|
+
format: String,
|
|
191
|
+
env: 'ACTUAL_OPENID_PROVIDER_NAME',
|
|
192
|
+
},
|
|
193
|
+
authorization_endpoint: {
|
|
194
|
+
doc: 'Authorization endpoint',
|
|
195
|
+
default: '',
|
|
196
|
+
format: String,
|
|
197
|
+
env: 'ACTUAL_OPENID_AUTHORIZATION_ENDPOINT',
|
|
198
|
+
},
|
|
199
|
+
token_endpoint: {
|
|
200
|
+
doc: 'Token endpoint',
|
|
201
|
+
default: '',
|
|
202
|
+
format: String,
|
|
203
|
+
env: 'ACTUAL_OPENID_TOKEN_ENDPOINT',
|
|
204
|
+
},
|
|
205
|
+
userinfo_endpoint: {
|
|
206
|
+
doc: 'Userinfo endpoint',
|
|
207
|
+
default: '',
|
|
208
|
+
format: String,
|
|
209
|
+
env: 'ACTUAL_OPENID_USERINFO_ENDPOINT',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
client_id: {
|
|
213
|
+
doc: 'OpenID client ID.',
|
|
214
|
+
format: String,
|
|
215
|
+
default: '',
|
|
216
|
+
env: 'ACTUAL_OPENID_CLIENT_ID',
|
|
217
|
+
},
|
|
218
|
+
client_secret: {
|
|
219
|
+
doc: 'OpenID client secret.',
|
|
220
|
+
format: String,
|
|
221
|
+
default: '',
|
|
222
|
+
env: 'ACTUAL_OPENID_CLIENT_SECRET',
|
|
223
|
+
},
|
|
224
|
+
server_hostname: {
|
|
225
|
+
doc: 'OpenID server hostname.',
|
|
226
|
+
format: String,
|
|
227
|
+
default: '',
|
|
228
|
+
env: 'ACTUAL_OPENID_SERVER_HOSTNAME',
|
|
229
|
+
},
|
|
230
|
+
authMethod: {
|
|
231
|
+
doc: 'OpenID authentication method.',
|
|
232
|
+
format: ['openid', 'oauth2'],
|
|
233
|
+
default: 'openid',
|
|
234
|
+
env: 'ACTUAL_OPENID_AUTH_METHOD',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
token_expiration: {
|
|
239
|
+
doc: 'Token expiration time.',
|
|
240
|
+
format: 'tokenExpiration',
|
|
241
|
+
default: 'never',
|
|
242
|
+
env: 'ACTUAL_TOKEN_EXPIRATION',
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
enforceOpenId: {
|
|
246
|
+
doc: 'Enforce OpenID authentication.',
|
|
247
|
+
format: Boolean,
|
|
248
|
+
default: false,
|
|
249
|
+
env: 'ACTUAL_OPENID_ENFORCE',
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
userCreationMode: {
|
|
253
|
+
doc: 'Determines how users can be created.',
|
|
254
|
+
format: ['manual', 'login'],
|
|
255
|
+
default: 'manual',
|
|
256
|
+
env: 'ACTUAL_USER_CREATION_MODE',
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
let configPath = null;
|
|
261
|
+
|
|
262
|
+
if (process.env.ACTUAL_CONFIG_PATH) {
|
|
263
|
+
debug(
|
|
264
|
+
`loading config from ACTUAL_CONFIG_PATH: '${process.env.ACTUAL_CONFIG_PATH}'`,
|
|
265
|
+
);
|
|
266
|
+
configPath = process.env.ACTUAL_CONFIG_PATH;
|
|
267
|
+
} else {
|
|
268
|
+
configPath = path.join(projectRoot, 'config.json');
|
|
269
|
+
|
|
270
|
+
if (!fs.existsSync(configPath)) {
|
|
271
|
+
configPath = path.join(defaultDataDir, 'config.json');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
debug(`loading config from default path: '${configPath}'`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (fs.existsSync(configPath)) {
|
|
278
|
+
configSchema.loadFile(configPath);
|
|
279
|
+
debug(`Config loaded`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
debug(`Validating config`);
|
|
283
|
+
configSchema.validate({ allowed: 'strict' });
|
|
284
|
+
|
|
285
|
+
debug(`Project root: ${configSchema.get('projectRoot')}`);
|
|
286
|
+
debug(`Port: ${configSchema.get('port')}`);
|
|
287
|
+
debug(`Hostname: ${configSchema.get('hostname')}`);
|
|
288
|
+
debug(`Data directory: ${configSchema.get('dataDir')}`);
|
|
289
|
+
debug(`Server files: ${configSchema.get('serverFiles')}`);
|
|
290
|
+
debug(`User files: ${configSchema.get('userFiles')}`);
|
|
291
|
+
debug(`Web root: ${configSchema.get('webRoot')}`);
|
|
292
|
+
debug(`Login method: ${configSchema.get('loginMethod')}`);
|
|
293
|
+
debug(`Allowed methods: ${configSchema.get('allowedLoginMethods').join(', ')}`);
|
|
294
|
+
|
|
295
|
+
const httpsKey = configSchema.get('https.key');
|
|
296
|
+
if (httpsKey) {
|
|
297
|
+
debug(`HTTPS Key: ${'*'.repeat(httpsKey.length)}`);
|
|
298
|
+
debugSensitive(`HTTPS Key: ${httpsKey}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const httpsCert = configSchema.get('https.cert');
|
|
302
|
+
if (httpsCert) {
|
|
303
|
+
debug(`HTTPS Cert: ${'*'.repeat(httpsCert.length)}`);
|
|
304
|
+
debugSensitive(`HTTPS Cert: ${httpsCert}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export { configSchema as config };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import migrate from 'migrate';
|
|
4
|
+
|
|
5
|
+
import { config } from './load-config.js';
|
|
6
|
+
|
|
7
|
+
export function run(direction = 'up') {
|
|
8
|
+
console.log(
|
|
9
|
+
`Checking if there are any migrations to run for direction "${direction}"...`,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
return new Promise(resolve =>
|
|
13
|
+
migrate.load(
|
|
14
|
+
{
|
|
15
|
+
stateStore: `${path.join(config.get('dataDir'), '.migrate')}${
|
|
16
|
+
config.get('mode') === 'test' ? '-test' : ''
|
|
17
|
+
}`,
|
|
18
|
+
migrationsDirectory: `${path.join(config.get('projectRoot'), 'migrations')}`,
|
|
19
|
+
},
|
|
20
|
+
(err, set) => {
|
|
21
|
+
if (err) {
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
set[direction](err => {
|
|
26
|
+
if (err) {
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log('Migrations: DONE');
|
|
31
|
+
resolve();
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
disableOpenID,
|
|
3
|
+
getActiveLoginMethod,
|
|
4
|
+
needsBootstrap,
|
|
5
|
+
} from '../account-db.js';
|
|
6
|
+
import { promptPassword } from '../util/prompt.js';
|
|
7
|
+
|
|
8
|
+
if (needsBootstrap()) {
|
|
9
|
+
console.log('System needs to be bootstrapped first. OpenID is not enabled.');
|
|
10
|
+
|
|
11
|
+
process.exit(1);
|
|
12
|
+
} else {
|
|
13
|
+
console.log('To disable OpenID, you have to enter your server password:');
|
|
14
|
+
try {
|
|
15
|
+
const loginMethod = getActiveLoginMethod();
|
|
16
|
+
console.log(`Current login method: ${loginMethod}`);
|
|
17
|
+
|
|
18
|
+
if (loginMethod === 'password') {
|
|
19
|
+
console.log('OpenID already disabled.');
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const password = await promptPassword();
|
|
24
|
+
const { error } = (await disableOpenID({ password })) || {};
|
|
25
|
+
|
|
26
|
+
if (error) {
|
|
27
|
+
console.log('Error disabling OpenID:', error);
|
|
28
|
+
console.log(
|
|
29
|
+
'Please report this as an issue: https://github.com/actualbudget/actual-server/issues',
|
|
30
|
+
);
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
console.log('OpenID disabled!');
|
|
34
|
+
console.log(
|
|
35
|
+
'Note: you will need to log in with the password on any browsers or devices that are currently logged in.',
|
|
36
|
+
);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.log('Unexpected error:', err);
|
|
39
|
+
console.log(
|
|
40
|
+
'Please report this as an issue: https://github.com/actualbudget/actual-server/issues',
|
|
41
|
+
);
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
enableOpenID,
|
|
3
|
+
getActiveLoginMethod,
|
|
4
|
+
needsBootstrap,
|
|
5
|
+
} from '../account-db.js';
|
|
6
|
+
import { config } from '../load-config.js';
|
|
7
|
+
|
|
8
|
+
if (needsBootstrap()) {
|
|
9
|
+
console.log(
|
|
10
|
+
'It looks like you don’t have a password set yet. Password is the fallback authentication method when using OpenID. Execute the command reset-password before using this command!',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
process.exit(1);
|
|
14
|
+
} else {
|
|
15
|
+
console.log('Enabling openid based on Environment variables or config.json');
|
|
16
|
+
try {
|
|
17
|
+
const loginMethod = getActiveLoginMethod();
|
|
18
|
+
console.log(`Current login method: ${loginMethod}`);
|
|
19
|
+
|
|
20
|
+
if (loginMethod === 'openid') {
|
|
21
|
+
console.log('OpenID already enabled.');
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
const { error } = (await enableOpenID(config.getProperties())) || {};
|
|
25
|
+
|
|
26
|
+
if (error) {
|
|
27
|
+
console.log('Error enabling openid:', error);
|
|
28
|
+
if (error === 'invalid-login-settings') {
|
|
29
|
+
console.log(
|
|
30
|
+
'Error configuring OpenID. Please verify that the configuration file or environment variables are correct.',
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
process.exit(1);
|
|
34
|
+
} else {
|
|
35
|
+
console.log(
|
|
36
|
+
'Please report this as an issue: https://github.com/actualbudget/actual-server/issues',
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
console.log('OpenID enabled!');
|
|
43
|
+
console.log(
|
|
44
|
+
'Note: The first user to login with OpenID will be the owner of the server.',
|
|
45
|
+
);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.log('Unexpected error:', err);
|
|
48
|
+
console.log(
|
|
49
|
+
'Please report this as an issue: https://github.com/actualbudget/actual-server/issues',
|
|
50
|
+
);
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
}
|