mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 17:47:26 -05:00
Refactoring load-config.js (#4440)
This commit is contained in:
@@ -15,11 +15,11 @@ async function ensureExists(path) {
|
||||
}
|
||||
|
||||
export const up = async function () {
|
||||
await ensureExists(config.serverFiles);
|
||||
await ensureExists(config.userFiles);
|
||||
await ensureExists(config.get('serverFiles'));
|
||||
await ensureExists(config.get('userFiles'));
|
||||
};
|
||||
|
||||
export const down = async function () {
|
||||
await fs.rm(config.serverFiles, { recursive: true, force: true });
|
||||
await fs.rm(config.userFiles, { recursive: true, force: true });
|
||||
await fs.rm(config.get('serverFiles'), { recursive: true, force: true });
|
||||
await fs.rm(config.get('userFiles'), { recursive: true, force: true });
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"@babel/preset-typescript": "^7.20.2",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/convict": "^6",
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/express-actuator": "^1.8.3",
|
||||
@@ -56,6 +57,7 @@
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"convict": "^6.2.4",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
|
||||
@@ -11,7 +11,7 @@ let _accountDb;
|
||||
|
||||
export function getAccountDb() {
|
||||
if (_accountDb === undefined) {
|
||||
const dbPath = join(config.serverFiles, 'account.sqlite');
|
||||
const dbPath = join(config.get('serverFiles'), 'account.sqlite');
|
||||
_accountDb = openDatabase(dbPath);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ export function listLoginMethods() {
|
||||
const rows = accountDb.all('SELECT method, display_name, active FROM auth');
|
||||
return rows
|
||||
.filter(f =>
|
||||
rows.length > 1 && config.enforceOpenId ? f.method === 'openid' : true,
|
||||
rows.length > 1 && config.get('enforceOpenId')
|
||||
? f.method === 'openid'
|
||||
: true,
|
||||
)
|
||||
.map(r => ({
|
||||
method: r.method,
|
||||
@@ -55,13 +57,13 @@ export function getLoginMethod(req) {
|
||||
if (
|
||||
typeof req !== 'undefined' &&
|
||||
(req.body || { loginMethod: null }).loginMethod &&
|
||||
config.allowedLoginMethods.includes(req.body.loginMethod)
|
||||
config.get('allowedLoginMethods').includes(req.body.loginMethod)
|
||||
) {
|
||||
return req.body.loginMethod;
|
||||
}
|
||||
|
||||
if (config.loginMethod) {
|
||||
return config.loginMethod;
|
||||
if (config.get('loginMethod')) {
|
||||
return config.get('loginMethod');
|
||||
}
|
||||
|
||||
const activeMethod = getActiveLoginMethod();
|
||||
|
||||
@@ -2,29 +2,29 @@ import { generators, Issuer } from 'openid-client';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { clearExpiredSessions, getAccountDb } from '../account-db.js';
|
||||
import { config as finalConfig } from '../load-config.js';
|
||||
import { config } from '../load-config.js';
|
||||
import {
|
||||
getUserByUsername,
|
||||
transferAllFilesFromUser,
|
||||
} from '../services/user-service.js';
|
||||
import { TOKEN_EXPIRATION_NEVER } from '../util/validate-user.js';
|
||||
|
||||
export async function bootstrapOpenId(config) {
|
||||
if (!('issuer' in config)) {
|
||||
return { error: 'missing-issuer' };
|
||||
export async function bootstrapOpenId(configParameter) {
|
||||
if (!('issuer' in configParameter) || !('discoveryURL' in configParameter)) {
|
||||
return { error: 'missing-issuer-or-discoveryURL' };
|
||||
}
|
||||
if (!('client_id' in config)) {
|
||||
if (!('client_id' in configParameter)) {
|
||||
return { error: 'missing-client-id' };
|
||||
}
|
||||
if (!('client_secret' in config)) {
|
||||
if (!('client_secret' in configParameter)) {
|
||||
return { error: 'missing-client-secret' };
|
||||
}
|
||||
if (!('server_hostname' in config)) {
|
||||
if (!('server_hostname' in configParameter)) {
|
||||
return { error: 'missing-server-hostname' };
|
||||
}
|
||||
|
||||
try {
|
||||
await setupOpenIdClient(config);
|
||||
await setupOpenIdClient(configParameter);
|
||||
} catch (err) {
|
||||
console.error('Error setting up OpenID client:', err);
|
||||
return { error: 'configuration-error' };
|
||||
@@ -37,7 +37,7 @@ export async function bootstrapOpenId(config) {
|
||||
accountDb.mutate('UPDATE auth SET active = 0');
|
||||
accountDb.mutate(
|
||||
"INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)",
|
||||
[JSON.stringify(config)],
|
||||
[JSON.stringify(configParameter)],
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -48,23 +48,22 @@ export async function bootstrapOpenId(config) {
|
||||
return {};
|
||||
}
|
||||
|
||||
async function setupOpenIdClient(config) {
|
||||
const issuer =
|
||||
typeof config.issuer === 'string'
|
||||
? await Issuer.discover(config.issuer)
|
||||
: new Issuer({
|
||||
issuer: config.issuer.name,
|
||||
authorization_endpoint: config.issuer.authorization_endpoint,
|
||||
token_endpoint: config.issuer.token_endpoint,
|
||||
userinfo_endpoint: config.issuer.userinfo_endpoint,
|
||||
});
|
||||
async function setupOpenIdClient(configParameter) {
|
||||
const issuer = configParameter.discoveryURL
|
||||
? await Issuer.discover(configParameter.discoveryURL)
|
||||
: new Issuer({
|
||||
issuer: configParameter.issuer.name,
|
||||
authorization_endpoint: configParameter.issuer.authorization_endpoint,
|
||||
token_endpoint: configParameter.issuer.token_endpoint,
|
||||
userinfo_endpoint: configParameter.issuer.userinfo_endpoint,
|
||||
});
|
||||
|
||||
const client = new issuer.Client({
|
||||
client_id: config.client_id,
|
||||
client_secret: config.client_secret,
|
||||
client_id: configParameter.client_id,
|
||||
client_secret: configParameter.client_secret,
|
||||
redirect_uri: new URL(
|
||||
'/openid/callback',
|
||||
config.server_hostname,
|
||||
configParameter.server_hostname,
|
||||
).toString(),
|
||||
validate_id_token: true,
|
||||
});
|
||||
@@ -139,21 +138,21 @@ export async function loginWithOpenIdFinalize(body) {
|
||||
}
|
||||
|
||||
const accountDb = getAccountDb();
|
||||
let config = accountDb.first(
|
||||
let configFromDb = accountDb.first(
|
||||
"SELECT extra_data FROM auth WHERE method = 'openid' AND active = 1",
|
||||
);
|
||||
if (!config) {
|
||||
if (!configFromDb) {
|
||||
return { error: 'openid-not-configured' };
|
||||
}
|
||||
try {
|
||||
config = JSON.parse(config['extra_data']);
|
||||
configFromDb = JSON.parse(configFromDb['extra_data']);
|
||||
} catch (err) {
|
||||
console.error('Error parsing OpenID configuration:', err);
|
||||
return { error: 'openid-setup-failed' };
|
||||
}
|
||||
let client;
|
||||
try {
|
||||
client = await setupOpenIdClient(config);
|
||||
client = await setupOpenIdClient(configFromDb);
|
||||
} catch (err) {
|
||||
console.error('Error setting up OpenID client:', err);
|
||||
return { error: 'openid-setup-failed' };
|
||||
@@ -173,7 +172,7 @@ export async function loginWithOpenIdFinalize(body) {
|
||||
try {
|
||||
let tokenSet = null;
|
||||
|
||||
if (!config.authMethod || config.authMethod === 'openid') {
|
||||
if (!configFromDb.authMethod || configFromDb.authMethod === 'openid') {
|
||||
const params = { code: body.code, state: body.state };
|
||||
tokenSet = await client.callback(client.redirect_uris[0], params, {
|
||||
code_verifier,
|
||||
@@ -264,13 +263,13 @@ export async function loginWithOpenIdFinalize(body) {
|
||||
const token = uuidv4();
|
||||
|
||||
let expiration;
|
||||
if (finalConfig.token_expiration === 'openid-provider') {
|
||||
if (config.get('token_expiration') === 'openid-provider') {
|
||||
expiration = tokenSet.expires_at ?? TOKEN_EXPIRATION_NEVER;
|
||||
} else if (finalConfig.token_expiration === 'never') {
|
||||
} else if (config.get('token_expiration') === 'never') {
|
||||
expiration = TOKEN_EXPIRATION_NEVER;
|
||||
} else if (typeof finalConfig.token_expiration === 'number') {
|
||||
} else if (typeof config.get('token_expiration') === 'number') {
|
||||
expiration =
|
||||
Math.floor(Date.now() / 1000) + finalConfig.token_expiration * 60;
|
||||
Math.floor(Date.now() / 1000) + config.get('token_expiration') * 60;
|
||||
} else {
|
||||
expiration = Math.floor(Date.now() / 1000) + 10 * 60;
|
||||
}
|
||||
|
||||
@@ -85,11 +85,12 @@ export function loginWithPassword(password) {
|
||||
|
||||
let expiration = TOKEN_EXPIRATION_NEVER;
|
||||
if (
|
||||
config.token_expiration !== 'never' &&
|
||||
config.token_expiration !== 'openid-provider' &&
|
||||
typeof config.token_expiration === 'number'
|
||||
config.get('token_expiration') !== 'never' &&
|
||||
config.get('token_expiration') !== 'openid-provider' &&
|
||||
typeof config.get('token_expiration') === 'number'
|
||||
) {
|
||||
expiration = Math.floor(Date.now() / 1000) + config.token_expiration * 60;
|
||||
expiration =
|
||||
Math.floor(Date.now() / 1000) + config.get('token_expiration') * 60;
|
||||
}
|
||||
|
||||
if (!sessionRow) {
|
||||
|
||||
@@ -24,7 +24,7 @@ process.on('unhandledRejection', reason => {
|
||||
|
||||
app.disable('x-powered-by');
|
||||
app.use(cors());
|
||||
app.set('trust proxy', config.trustedProxies);
|
||||
app.set('trust proxy', config.get('trustedProxies'));
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
app.use(
|
||||
rateLimit({
|
||||
@@ -36,17 +36,19 @@ if (process.env.NODE_ENV !== 'development') {
|
||||
);
|
||||
}
|
||||
|
||||
app.use(bodyParser.json({ limit: `${config.upload.fileSizeLimitMB}mb` }));
|
||||
app.use(
|
||||
bodyParser.json({ limit: `${config.get('upload.fileSizeLimitMB')}mb` }),
|
||||
);
|
||||
app.use(
|
||||
bodyParser.raw({
|
||||
type: 'application/actual-sync',
|
||||
limit: `${config.upload.fileSizeSyncLimitMB}mb`,
|
||||
limit: `${config.get('upload.fileSizeSyncLimitMB')}mb`,
|
||||
}),
|
||||
);
|
||||
app.use(
|
||||
bodyParser.raw({
|
||||
type: 'application/encrypted-file',
|
||||
limit: `${config.upload.syncEncryptedFileSizeLimitMB}mb`,
|
||||
limit: `${config.get('upload.syncEncryptedFileSizeLimitMB')}mb`,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -61,7 +63,7 @@ app.use('/admin', adminApp.handlers);
|
||||
app.use('/openid', openidApp.handlers);
|
||||
|
||||
app.get('/mode', (req, res) => {
|
||||
res.send(config.mode);
|
||||
res.send(config.get('mode'));
|
||||
});
|
||||
|
||||
app.use(actuator()); // Provides /health, /metrics, /info
|
||||
@@ -91,8 +93,10 @@ if (process.env.NODE_ENV === 'development') {
|
||||
} else {
|
||||
console.log('Running in production mode - Serving static React app');
|
||||
|
||||
app.use(express.static(config.webRoot, { index: false }));
|
||||
app.get('/*', (req, res) => res.sendFile(config.webRoot + '/index.html'));
|
||||
app.use(express.static(config.get('webRoot'), { index: false }));
|
||||
app.get('/*', (req, res) =>
|
||||
res.sendFile(config.get('webRoot') + '/index.html'),
|
||||
);
|
||||
}
|
||||
|
||||
function parseHTTPSConfig(value) {
|
||||
@@ -103,17 +107,21 @@ function parseHTTPSConfig(value) {
|
||||
}
|
||||
|
||||
export async function run() {
|
||||
if (config.https) {
|
||||
if (config.get('https.key') && config.get('https.cert')) {
|
||||
const https = await import('node:https');
|
||||
const httpsOptions = {
|
||||
...config.https,
|
||||
key: parseHTTPSConfig(config.https.key),
|
||||
cert: parseHTTPSConfig(config.https.cert),
|
||||
key: parseHTTPSConfig(config.get('https.key')),
|
||||
cert: parseHTTPSConfig(config.get('https.cert')),
|
||||
};
|
||||
https.createServer(httpsOptions, app).listen(config.port, config.hostname);
|
||||
https
|
||||
.createServer(httpsOptions, app)
|
||||
.listen(config.get('port'), config.get('hostname'));
|
||||
} else {
|
||||
app.listen(config.port, config.hostname);
|
||||
app.listen(config.get('port'), config.get('hostname'));
|
||||
}
|
||||
|
||||
console.log('Listening on ' + config.hostname + ':' + config.port + '...');
|
||||
console.log(
|
||||
'Listening on ' + config.get('hostname') + ':' + config.get('port') + '...',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ export interface Config {
|
||||
server_hostname: string;
|
||||
authMethod?: 'openid' | 'oauth2';
|
||||
};
|
||||
multiuser: boolean;
|
||||
token_expiration?: 'never' | 'openid-provider' | number;
|
||||
enforceOpenId: boolean;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import convict from 'convict';
|
||||
import createDebug from 'debug';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
@@ -10,257 +11,295 @@ const debug = createDebug('actual:config');
|
||||
const debugSensitive = createDebug('actual-sensitive:config');
|
||||
|
||||
const projectRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||||
debug(`project root: '${projectRoot}'`);
|
||||
const defaultDataDir = fs.existsSync('./data') ? './data' : projectRoot;
|
||||
|
||||
debug(`Project root: '${projectRoot}'`);
|
||||
|
||||
export const sqlDir = path.join(projectRoot, 'src', 'sql');
|
||||
|
||||
let defaultDataDir = fs.existsSync('/data') ? '/data' : projectRoot;
|
||||
const actualAppWebBuildPath = path.join(
|
||||
path.dirname(require.resolve('@actual-app/web/package.json')),
|
||||
'build',
|
||||
);
|
||||
debug(`Actual web build path: '${actualAppWebBuildPath}'`);
|
||||
|
||||
if (process.env.ACTUAL_DATA_DIR) {
|
||||
defaultDataDir = process.env.ACTUAL_DATA_DIR;
|
||||
}
|
||||
// Custom formats
|
||||
convict.addFormat({
|
||||
name: 'tokenExpiration',
|
||||
validate(val) {
|
||||
if (val === 'never' || val === 'openid-provider') return;
|
||||
if (typeof val === 'number' && Number.isFinite(val) && val >= 0) return;
|
||||
throw new Error(`Invalid token_expiration value: ${val}`);
|
||||
},
|
||||
});
|
||||
|
||||
debug(`default data directory: '${defaultDataDir}'`);
|
||||
// Main config schema
|
||||
const configSchema = convict({
|
||||
env: {
|
||||
doc: 'The application environment.',
|
||||
format: ['production', 'development', 'test'],
|
||||
default: 'development',
|
||||
env: 'NODE_ENV',
|
||||
},
|
||||
mode: {
|
||||
doc: 'Application mode.',
|
||||
format: ['test', 'development'],
|
||||
default: process.env.NODE_ENV === 'test' ? 'test' : 'development',
|
||||
},
|
||||
projectRoot: {
|
||||
doc: 'Project root directory.',
|
||||
format: String,
|
||||
default: projectRoot,
|
||||
},
|
||||
dataDir: {
|
||||
doc: 'Default data directory.',
|
||||
format: String,
|
||||
default: defaultDataDir,
|
||||
env: 'ACTUAL_DATA_DIR',
|
||||
},
|
||||
port: {
|
||||
doc: 'Port to run the server on.',
|
||||
format: 'port',
|
||||
default: 5006,
|
||||
env: ['ACTUAL_PORT', 'PORT'],
|
||||
},
|
||||
hostname: {
|
||||
doc: 'Server hostname.',
|
||||
format: String,
|
||||
default: '::',
|
||||
env: 'ACTUAL_HOSTNAME',
|
||||
},
|
||||
serverFiles: {
|
||||
doc: 'Path to server files.',
|
||||
format: String,
|
||||
default:
|
||||
process.env.NODE_ENV === 'test'
|
||||
? path.join(projectRoot, 'test-server-files')
|
||||
: path.join(projectRoot, 'server-files'),
|
||||
env: 'ACTUAL_SERVER_FILES',
|
||||
},
|
||||
userFiles: {
|
||||
doc: 'Path to user files.',
|
||||
format: String,
|
||||
default:
|
||||
process.env.NODE_ENV === 'test'
|
||||
? path.join(projectRoot, 'test-user-files')
|
||||
: path.join(projectRoot, 'user-files'),
|
||||
env: 'ACTUAL_USER_FILES',
|
||||
},
|
||||
webRoot: {
|
||||
doc: 'Web root directory.',
|
||||
format: String,
|
||||
default: actualAppWebBuildPath,
|
||||
env: 'ACTUAL_WEB_ROOT',
|
||||
},
|
||||
loginMethod: {
|
||||
doc: 'Authentication method.',
|
||||
format: ['password', 'header', 'openid'],
|
||||
default: 'password',
|
||||
env: 'ACTUAL_LOGIN_METHOD',
|
||||
},
|
||||
allowedLoginMethods: {
|
||||
doc: 'Allowed authentication methods.',
|
||||
format: Array,
|
||||
default: ['password', 'header', 'openid'],
|
||||
env: 'ACTUAL_ALLOWED_LOGIN_METHODS',
|
||||
},
|
||||
trustedProxies: {
|
||||
doc: 'List of trusted proxies.',
|
||||
format: Array,
|
||||
default: [
|
||||
'10.0.0.0/8',
|
||||
'172.16.0.0/12',
|
||||
'192.168.0.0/16',
|
||||
'fc00::/7',
|
||||
'::1/128',
|
||||
],
|
||||
env: 'ACTUAL_TRUSTED_PROXIES',
|
||||
},
|
||||
trustedAuthProxies: {
|
||||
doc: 'List of trusted auth proxies.',
|
||||
format: Array,
|
||||
default: [],
|
||||
env: 'ACTUAL_TRUSTED_AUTH_PROXIES',
|
||||
},
|
||||
|
||||
function parseJSON(path, allowMissing = false) {
|
||||
let text;
|
||||
try {
|
||||
text = fs.readFileSync(path, 'utf8');
|
||||
} catch (e) {
|
||||
if (allowMissing) {
|
||||
debug(`config file '${path}' not found, ignoring.`);
|
||||
return {};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return JSON.parse(text);
|
||||
}
|
||||
https: {
|
||||
doc: 'HTTPS configuration.',
|
||||
format: Object,
|
||||
default: {
|
||||
key: '',
|
||||
cert: '',
|
||||
},
|
||||
|
||||
key: {
|
||||
doc: 'HTTPS Certificate key',
|
||||
format: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
cert: {
|
||||
doc: 'HTTPS Certificate',
|
||||
format: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
upload: {
|
||||
doc: 'Upload configuration.',
|
||||
format: Object,
|
||||
default: {
|
||||
fileSizeSyncLimitMB: 20,
|
||||
syncEncryptedFileSizeLimitMB: 50,
|
||||
fileSizeLimitMB: 20,
|
||||
},
|
||||
|
||||
fileSizeSyncLimitMB: {
|
||||
doc: 'Sync file size limit (in MB)',
|
||||
format: 'nat',
|
||||
default: 20,
|
||||
env: 'ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB',
|
||||
},
|
||||
|
||||
syncEncryptedFileSizeLimitMB: {
|
||||
doc: 'Encrypted Sync file size limit (in MB)',
|
||||
format: 'nat',
|
||||
default: 50,
|
||||
env: 'ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB',
|
||||
},
|
||||
|
||||
fileSizeLimitMB: {
|
||||
doc: 'General file size limit (in MB)',
|
||||
format: 'nat',
|
||||
default: 20,
|
||||
env: 'ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB',
|
||||
},
|
||||
},
|
||||
|
||||
openId: {
|
||||
doc: 'OpenID authentication settings.',
|
||||
|
||||
discoveryURL: {
|
||||
doc: 'OpenID Provider discovery URL.',
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'ACTUAL_OPENID_DISCOVERY_URL',
|
||||
},
|
||||
issuer: {
|
||||
doc: 'OpenID issuer',
|
||||
format: Object,
|
||||
default: {},
|
||||
name: {
|
||||
doc: 'Name of the provider',
|
||||
default: '',
|
||||
format: String,
|
||||
env: 'ACTUAL_OPENID_PROVIDER_NAME',
|
||||
},
|
||||
authorization_endpoint: {
|
||||
doc: 'Authorization endpoint',
|
||||
default: '',
|
||||
format: String,
|
||||
env: 'ACTUAL_OPENID_AUTHORIZATION_ENDPOINT',
|
||||
},
|
||||
token_endpoint: {
|
||||
doc: 'Token endpoint',
|
||||
default: '',
|
||||
format: String,
|
||||
env: 'ACTUAL_OPENID_TOKEN_ENDPOINT',
|
||||
},
|
||||
userinfo_endpoint: {
|
||||
doc: 'Userinfo endpoint',
|
||||
default: '',
|
||||
format: String,
|
||||
env: 'ACTUAL_OPENID_USERINFO_ENDPOINT',
|
||||
},
|
||||
},
|
||||
client_id: {
|
||||
doc: 'OpenID client ID.',
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'ACTUAL_OPENID_CLIENT_ID',
|
||||
},
|
||||
client_secret: {
|
||||
doc: 'OpenID client secret.',
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'ACTUAL_OPENID_CLIENT_SECRET',
|
||||
},
|
||||
server_hostname: {
|
||||
doc: 'OpenID server hostname.',
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'ACTUAL_OPENID_SERVER_HOSTNAME',
|
||||
},
|
||||
authMethod: {
|
||||
doc: 'OpenID authentication method.',
|
||||
format: ['openid', 'oauth2'],
|
||||
default: 'openid',
|
||||
env: 'ACTUAL_OPENID_AUTH_METHOD',
|
||||
},
|
||||
},
|
||||
|
||||
token_expiration: {
|
||||
doc: 'Token expiration time.',
|
||||
format: 'tokenExpiration',
|
||||
default: 'never',
|
||||
env: 'ACTUAL_TOKEN_EXPIRATION',
|
||||
},
|
||||
|
||||
enforceOpenId: {
|
||||
doc: 'Enforce OpenID authentication.',
|
||||
format: Boolean,
|
||||
default: false,
|
||||
env: 'ACTUAL_OPENID_ENFORCE',
|
||||
},
|
||||
});
|
||||
|
||||
let configPath = null;
|
||||
|
||||
let userConfig;
|
||||
if (process.env.ACTUAL_CONFIG_PATH) {
|
||||
debug(
|
||||
`loading config from ACTUAL_CONFIG_PATH: '${process.env.ACTUAL_CONFIG_PATH}'`,
|
||||
);
|
||||
userConfig = parseJSON(process.env.ACTUAL_CONFIG_PATH);
|
||||
|
||||
defaultDataDir = userConfig.dataDir ?? defaultDataDir;
|
||||
configPath = process.env.ACTUAL_CONFIG_PATH;
|
||||
} else {
|
||||
let configFile = path.join(projectRoot, 'config.json');
|
||||
configPath = path.join(projectRoot, 'config.json');
|
||||
|
||||
if (!fs.existsSync(configFile)) {
|
||||
configFile = path.join(defaultDataDir, 'config.json');
|
||||
if (!fs.existsSync(configPath)) {
|
||||
configPath = path.join(defaultDataDir, 'config.json');
|
||||
}
|
||||
|
||||
debug(`loading config from default path: '${configFile}'`);
|
||||
userConfig = parseJSON(configFile, true);
|
||||
debug(`loading config from default path: '${configPath}'`);
|
||||
}
|
||||
|
||||
const actualAppWebBuildPath = path.join(
|
||||
// require.resolve is used to recursively search up the workspace to find the node_modules directory
|
||||
path.dirname(require.resolve('@actual-app/web/package.json')),
|
||||
'build',
|
||||
);
|
||||
|
||||
debug(`Actual web build path: '${actualAppWebBuildPath}'`);
|
||||
|
||||
/** @type {Omit<import('./config-types.js').Config, 'mode' | 'dataDir' | 'serverFiles' | 'userFiles'>} */
|
||||
const defaultConfig = {
|
||||
loginMethod: 'password',
|
||||
allowedLoginMethods: ['password', 'header', 'openid'],
|
||||
// assume local networks are trusted
|
||||
trustedProxies: [
|
||||
'10.0.0.0/8',
|
||||
'172.16.0.0/12',
|
||||
'192.168.0.0/16',
|
||||
'fc00::/7',
|
||||
'::1/128',
|
||||
],
|
||||
// fallback to trustedProxies, but in the future trustedProxies will only be used for express trust
|
||||
// and trustedAuthProxies will just be for header auth
|
||||
trustedAuthProxies: null,
|
||||
port: 5006,
|
||||
hostname: '::',
|
||||
webRoot: actualAppWebBuildPath,
|
||||
upload: {
|
||||
fileSizeSyncLimitMB: 20,
|
||||
syncEncryptedFileSizeLimitMB: 50,
|
||||
fileSizeLimitMB: 20,
|
||||
},
|
||||
projectRoot,
|
||||
multiuser: false,
|
||||
token_expiration: 'never',
|
||||
enforceOpenId: false,
|
||||
};
|
||||
|
||||
/** @type {import('./config-types.js').Config} */
|
||||
let config;
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
config = {
|
||||
mode: 'test',
|
||||
dataDir: projectRoot,
|
||||
serverFiles: path.join(projectRoot, 'test-server-files'),
|
||||
userFiles: path.join(projectRoot, 'test-user-files'),
|
||||
...defaultConfig,
|
||||
};
|
||||
} else {
|
||||
config = {
|
||||
mode: 'development',
|
||||
...defaultConfig,
|
||||
dataDir: defaultDataDir,
|
||||
serverFiles: path.join(defaultDataDir, 'server-files'),
|
||||
userFiles: path.join(defaultDataDir, 'user-files'),
|
||||
...(userConfig || {}),
|
||||
};
|
||||
if (fs.existsSync(configPath)) {
|
||||
configSchema.loadFile(configPath);
|
||||
debug(`Config loaded`);
|
||||
}
|
||||
|
||||
const finalConfig = {
|
||||
...config,
|
||||
loginMethod: process.env.ACTUAL_LOGIN_METHOD
|
||||
? process.env.ACTUAL_LOGIN_METHOD.toLowerCase()
|
||||
: config.loginMethod,
|
||||
multiuser: process.env.ACTUAL_MULTIUSER
|
||||
? (() => {
|
||||
const value = process.env.ACTUAL_MULTIUSER.toLowerCase();
|
||||
if (!['true', 'false'].includes(value)) {
|
||||
throw new Error('ACTUAL_MULTIUSER must be either "true" or "false"');
|
||||
}
|
||||
return value === 'true';
|
||||
})()
|
||||
: config.multiuser,
|
||||
allowedLoginMethods: process.env.ACTUAL_ALLOWED_LOGIN_METHODS
|
||||
? process.env.ACTUAL_ALLOWED_LOGIN_METHODS.split(',')
|
||||
.map(q => q.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
: config.allowedLoginMethods,
|
||||
trustedProxies: process.env.ACTUAL_TRUSTED_PROXIES
|
||||
? process.env.ACTUAL_TRUSTED_PROXIES.split(',')
|
||||
.map(q => q.trim())
|
||||
.filter(Boolean)
|
||||
: config.trustedProxies,
|
||||
trustedAuthProxies: process.env.ACTUAL_TRUSTED_AUTH_PROXIES
|
||||
? process.env.ACTUAL_TRUSTED_AUTH_PROXIES.split(',')
|
||||
.map(q => q.trim())
|
||||
.filter(Boolean)
|
||||
: config.trustedAuthProxies,
|
||||
port: +process.env.ACTUAL_PORT || +process.env.PORT || config.port,
|
||||
hostname: process.env.ACTUAL_HOSTNAME || config.hostname,
|
||||
serverFiles: process.env.ACTUAL_SERVER_FILES || config.serverFiles,
|
||||
userFiles: process.env.ACTUAL_USER_FILES || config.userFiles,
|
||||
webRoot: process.env.ACTUAL_WEB_ROOT || config.webRoot,
|
||||
https:
|
||||
process.env.ACTUAL_HTTPS_KEY && process.env.ACTUAL_HTTPS_CERT
|
||||
? {
|
||||
key: process.env.ACTUAL_HTTPS_KEY.replace(/\\n/g, '\n'),
|
||||
cert: process.env.ACTUAL_HTTPS_CERT.replace(/\\n/g, '\n'),
|
||||
...(config.https || {}),
|
||||
}
|
||||
: config.https,
|
||||
upload:
|
||||
process.env.ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB ||
|
||||
process.env.ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB ||
|
||||
process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB
|
||||
? {
|
||||
fileSizeSyncLimitMB:
|
||||
+process.env.ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB ||
|
||||
+process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB ||
|
||||
config.upload.fileSizeSyncLimitMB,
|
||||
syncEncryptedFileSizeLimitMB:
|
||||
+process.env.ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB ||
|
||||
+process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB ||
|
||||
config.upload.syncEncryptedFileSizeLimitMB,
|
||||
fileSizeLimitMB:
|
||||
+process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB ||
|
||||
config.upload.fileSizeLimitMB,
|
||||
}
|
||||
: config.upload,
|
||||
openId: (() => {
|
||||
if (
|
||||
!process.env.ACTUAL_OPENID_DISCOVERY_URL &&
|
||||
!process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT
|
||||
) {
|
||||
return config.openId;
|
||||
}
|
||||
const baseConfig = process.env.ACTUAL_OPENID_DISCOVERY_URL
|
||||
? { issuer: process.env.ACTUAL_OPENID_DISCOVERY_URL }
|
||||
: {
|
||||
...(() => {
|
||||
const required = {
|
||||
authorization_endpoint:
|
||||
process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT,
|
||||
token_endpoint: process.env.ACTUAL_OPENID_TOKEN_ENDPOINT,
|
||||
userinfo_endpoint: process.env.ACTUAL_OPENID_USERINFO_ENDPOINT,
|
||||
};
|
||||
const missing = Object.entries(required)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.filter(([_, value]) => !value)
|
||||
.map(([key]) => key);
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
`Missing required OpenID configuration: ${missing.join(', ')}`,
|
||||
);
|
||||
}
|
||||
return {};
|
||||
})(),
|
||||
issuer: {
|
||||
name: process.env.ACTUAL_OPENID_PROVIDER_NAME,
|
||||
authorization_endpoint:
|
||||
process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT,
|
||||
token_endpoint: process.env.ACTUAL_OPENID_TOKEN_ENDPOINT,
|
||||
userinfo_endpoint: process.env.ACTUAL_OPENID_USERINFO_ENDPOINT,
|
||||
},
|
||||
};
|
||||
return {
|
||||
...baseConfig,
|
||||
client_id:
|
||||
process.env.ACTUAL_OPENID_CLIENT_ID ?? config.openId?.client_id,
|
||||
client_secret:
|
||||
process.env.ACTUAL_OPENID_CLIENT_SECRET ?? config.openId?.client_secret,
|
||||
server_hostname:
|
||||
process.env.ACTUAL_OPENID_SERVER_HOSTNAME ??
|
||||
config.openId?.server_hostname,
|
||||
};
|
||||
})(),
|
||||
token_expiration: process.env.ACTUAL_TOKEN_EXPIRATION
|
||||
? process.env.ACTUAL_TOKEN_EXPIRATION
|
||||
: config.token_expiration,
|
||||
enforceOpenId: process.env.ACTUAL_OPENID_ENFORCE
|
||||
? (() => {
|
||||
const value = process.env.ACTUAL_OPENID_ENFORCE.toLowerCase();
|
||||
if (!['true', 'false'].includes(value)) {
|
||||
throw new Error(
|
||||
'ACTUAL_OPENID_ENFORCE must be either "true" or "false"',
|
||||
);
|
||||
}
|
||||
return value === 'true';
|
||||
})()
|
||||
: config.enforceOpenId,
|
||||
};
|
||||
debug(`using port ${finalConfig.port}`);
|
||||
debug(`using hostname ${finalConfig.hostname}`);
|
||||
debug(`using data directory ${finalConfig.dataDir}`);
|
||||
debug(`using server files directory ${finalConfig.serverFiles}`);
|
||||
debug(`using user files directory ${finalConfig.userFiles}`);
|
||||
debug(`using web root directory ${finalConfig.webRoot}`);
|
||||
debug(`using login method ${finalConfig.loginMethod}`);
|
||||
debug(`using trusted proxies ${finalConfig.trustedProxies.join(', ')}`);
|
||||
debug(
|
||||
`using trusted auth proxies ${
|
||||
finalConfig.trustedAuthProxies?.join(', ') ?? 'same as trusted proxies'
|
||||
}`,
|
||||
);
|
||||
debug(`Validating config`);
|
||||
configSchema.validate({ allowed: 'strict' });
|
||||
|
||||
if (finalConfig.https) {
|
||||
debug(`using https key: ${'*'.repeat(finalConfig.https.key.length)}`);
|
||||
debugSensitive(`using https key ${finalConfig.https.key}`);
|
||||
debug(`using https cert: ${'*'.repeat(finalConfig.https.cert.length)}`);
|
||||
debugSensitive(`using https cert ${finalConfig.https.cert}`);
|
||||
debug(`Project root: ${configSchema.get('projectRoot')}`);
|
||||
debug(`Port: ${configSchema.get('port')}`);
|
||||
debug(`Hostname: ${configSchema.get('hostname')}`);
|
||||
debug(`Data directory: ${configSchema.get('dataDir')}`);
|
||||
debug(`Server files: ${configSchema.get('serverFiles')}`);
|
||||
debug(`User files: ${configSchema.get('userFiles')}`);
|
||||
debug(`Web root: ${configSchema.get('webRoot')}`);
|
||||
debug(`Login method: ${configSchema.get('loginMethod')}`);
|
||||
debug(`Allowed methods: ${configSchema.get('allowedLoginMethods').join(', ')}`);
|
||||
|
||||
const httpsKey = configSchema.get('https.key');
|
||||
if (httpsKey) {
|
||||
debug(`HTTPS Key: ${'*'.repeat(httpsKey.length)}`);
|
||||
debugSensitive(`HTTPS Key: ${httpsKey}`);
|
||||
}
|
||||
|
||||
if (finalConfig.upload) {
|
||||
debug(`using file sync limit ${finalConfig.upload.fileSizeSyncLimitMB}mb`);
|
||||
debug(
|
||||
`using sync encrypted file limit ${finalConfig.upload.syncEncryptedFileSizeLimitMB}mb`,
|
||||
);
|
||||
debug(`using file limit ${finalConfig.upload.fileSizeLimitMB}mb`);
|
||||
const httpsCert = configSchema.get('https.cert');
|
||||
if (httpsCert) {
|
||||
debug(`HTTPS Cert: ${'*'.repeat(httpsCert.length)}`);
|
||||
debugSensitive(`HTTPS Cert: ${httpsCert}`);
|
||||
}
|
||||
|
||||
export { finalConfig as config };
|
||||
export { configSchema as config };
|
||||
|
||||
@@ -12,10 +12,10 @@ export function run(direction = 'up') {
|
||||
return new Promise(resolve =>
|
||||
migrate.load(
|
||||
{
|
||||
stateStore: `${path.join(config.dataDir, '.migrate')}${
|
||||
config.mode === 'test' ? '-test' : ''
|
||||
stateStore: `${path.join(config.get('dataDir'), '.migrate')}${
|
||||
config.get('mode') === 'test' ? '-test' : ''
|
||||
}`,
|
||||
migrationsDirectory: `${path.join(config.projectRoot, 'migrations')}`,
|
||||
migrationsDirectory: `${path.join(config.get('projectRoot'), 'migrations')}`,
|
||||
},
|
||||
(err, set) => {
|
||||
if (err) {
|
||||
|
||||
@@ -21,7 +21,7 @@ if (needsBootstrap()) {
|
||||
console.log('OpenID already enabled.');
|
||||
process.exit(0);
|
||||
}
|
||||
const { error } = (await enableOpenID(config)) || {};
|
||||
const { error } = (await enableOpenID(config.getProperties())) || {};
|
||||
|
||||
if (error) {
|
||||
console.log('Error enabling openid:', error);
|
||||
|
||||
@@ -2,10 +2,12 @@ import fetch from 'node-fetch';
|
||||
|
||||
import { config } from '../load-config.js';
|
||||
|
||||
const protocol = config.https ? 'https' : 'http';
|
||||
const hostname = config.hostname === '::' ? 'localhost' : config.hostname;
|
||||
const protocol =
|
||||
config.get('https.key') && config.get('https.cert') ? 'https' : 'http';
|
||||
const hostname =
|
||||
config.get('hostname') === '::' ? 'localhost' : config.get('hostname');
|
||||
|
||||
fetch(`${protocol}://${hostname}:${config.port}/health`)
|
||||
fetch(`${protocol}://${hostname}:${config.get('port')}/health`)
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if (res.status !== 'UP') {
|
||||
|
||||
@@ -4,10 +4,10 @@ import { config } from '../load-config.js';
|
||||
|
||||
/** @param {string} fileId */
|
||||
export function getPathForUserFile(fileId) {
|
||||
return join(config.userFiles, `file-${fileId}.blob`);
|
||||
return join(config.get('userFiles'), `file-${fileId}.blob`);
|
||||
}
|
||||
|
||||
/** @param {string} groupId */
|
||||
export function getPathForGroupFile(groupId) {
|
||||
return join(config.userFiles, `group-${groupId}.sqlite`);
|
||||
return join(config.get('userFiles'), `group-${groupId}.sqlite`);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ export function validateSession(req, res) {
|
||||
|
||||
export function validateAuthHeader(req) {
|
||||
// fallback to trustedProxies when trustedAuthProxies not set
|
||||
const trustedAuthProxies = config.trustedAuthProxies ?? config.trustedProxies;
|
||||
const trustedAuthProxies =
|
||||
config.get('trustedAuthProxies') ?? config.get('trustedProxies');
|
||||
// ensure the first hop from our server is trusted
|
||||
const peer = req.socket.remoteAddress;
|
||||
const peerIp = ipaddr.process(peer);
|
||||
|
||||
6
upcoming-release-notes/4440.md
Normal file
6
upcoming-release-notes/4440.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [lelemm]
|
||||
---
|
||||
|
||||
Refactoring Sync Server's configuration file and Environmental Variables
|
||||
30
yarn.lock
30
yarn.lock
@@ -88,6 +88,7 @@ __metadata:
|
||||
"@babel/preset-typescript": "npm:^7.20.2"
|
||||
"@types/bcrypt": "npm:^5.0.2"
|
||||
"@types/better-sqlite3": "npm:^7.6.12"
|
||||
"@types/convict": "npm:^6"
|
||||
"@types/cors": "npm:^2.8.13"
|
||||
"@types/express": "npm:^5.0.0"
|
||||
"@types/express-actuator": "npm:^1.8.3"
|
||||
@@ -100,6 +101,7 @@ __metadata:
|
||||
bcrypt: "npm:^5.1.1"
|
||||
better-sqlite3: "npm:^11.7.0"
|
||||
body-parser: "npm:^1.20.3"
|
||||
convict: "npm:^6.2.4"
|
||||
cors: "npm:^2.8.5"
|
||||
date-fns: "npm:^2.30.0"
|
||||
debug: "npm:^4.3.4"
|
||||
@@ -6112,6 +6114,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/convict@npm:^6":
|
||||
version: 6.1.6
|
||||
resolution: "@types/convict@npm:6.1.6"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
checksum: 10/680e6ec527545d4bf3a4a7368c71510b67a09f131a058c2c4616a3a445e9f2be58b42a21535a8078739439b98b1326130f270fff80940465dc40b83b9bf22f4c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/cookiejar@npm:^2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@types/cookiejar@npm:2.1.5"
|
||||
@@ -9376,6 +9387,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"convict@npm:^6.2.4":
|
||||
version: 6.2.4
|
||||
resolution: "convict@npm:6.2.4"
|
||||
dependencies:
|
||||
lodash.clonedeep: "npm:^4.5.0"
|
||||
yargs-parser: "npm:^20.2.7"
|
||||
checksum: 10/d4b9c50dcddf4b5da7a80c1d99d1cfae8a47d78d291f0cc11637ab25b6b4515f5f0e9029abd45bcc30cc3e33032aa8814ead22142b4563c4e4959d2e56bdf1ae
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie-signature@npm:1.0.6":
|
||||
version: 1.0.6
|
||||
resolution: "cookie-signature@npm:1.0.6"
|
||||
@@ -15990,6 +16011,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.clonedeep@npm:^4.5.0":
|
||||
version: 4.5.0
|
||||
resolution: "lodash.clonedeep@npm:4.5.0"
|
||||
checksum: 10/957ed243f84ba6791d4992d5c222ffffca339a3b79dbe81d2eaf0c90504160b500641c5a0f56e27630030b18b8e971ea10b44f928a977d5ced3c8948841b555f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.debounce@npm:^4.0.8":
|
||||
version: 4.0.8
|
||||
resolution: "lodash.debounce@npm:4.0.8"
|
||||
@@ -23569,7 +23597,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yargs-parser@npm:^20.2.2":
|
||||
"yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.7":
|
||||
version: 20.2.9
|
||||
resolution: "yargs-parser@npm:20.2.9"
|
||||
checksum: 10/0188f430a0f496551d09df6719a9132a3469e47fe2747208b1dd0ab2bb0c512a95d0b081628bbca5400fb20dbf2fabe63d22badb346cecadffdd948b049f3fcc
|
||||
|
||||
Reference in New Issue
Block a user