Refactoring load-config.js (#4440)

This commit is contained in:
lelemm
2025-03-04 08:15:46 -03:00
committed by GitHub
parent d5e2030d23
commit d815a22f6b
15 changed files with 387 additions and 300 deletions

View File

@@ -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 });
};

View File

@@ -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",

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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') + '...',
);
}

View File

@@ -38,7 +38,6 @@ export interface Config {
server_hostname: string;
authMethod?: 'openid' | 'oauth2';
};
multiuser: boolean;
token_expiration?: 'never' | 'openid-provider' | number;
enforceOpenId: boolean;
}

View File

@@ -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 };

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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') {

View File

@@ -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`);
}

View File

@@ -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);

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [lelemm]
---
Refactoring Sync Server's configuration file and Environmental Variables

View File

@@ -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