nixos/wealthfolio: init

This commit is contained in:
Romain Delhommais
2026-06-25 19:41:56 +02:00
parent dc4c196c35
commit cdb61286b3
2 changed files with 185 additions and 0 deletions
+1
View File
@@ -1805,6 +1805,7 @@
./services/web-apps/umami.nix
./services/web-apps/vikunja.nix
./services/web-apps/wakapi.nix
./services/web-apps/wealthfolio.nix
./services/web-apps/weblate.nix
./services/web-apps/websurfx.nix
./services/web-apps/whitebophir.nix
@@ -0,0 +1,184 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.wealthfolio;
in
{
options.services.wealthfolio = {
enable = lib.mkEnableOption "Wealthfolio personal investment tracker";
package = lib.mkPackageOption pkgs "wealthfolio-server" { };
address = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "The IP address the Wealthfolio server binds to.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8088;
description = "The port the Wealthfolio server listens on.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to automatically open the specified port in the system firewall.";
};
secretKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = lib.literalExpression "config.age.secrets.wealthfolio-key.path";
description = ''
Path to a file containing the 32-byte secret key used for encrypting sensitive data
at rest (broker credentials, API keys) and signing JWT tokens.
Generate with: `openssl rand -base64 32`.
Note: Losing this key means losing access to all stored encrypted secrets.
There is no recovery.
'';
};
authPasswordHashFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = lib.literalExpression "config.age.secrets.wealthfolio-hash.path";
description = ''
Path to a file containing the Argon2id PHC string defining the login password.
Required for web access unless `authRequired` is false.
Generate with: `printf 'your-password' | argon2 yoursalt16chars! -id -e`
'';
};
authRequired = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to require internal authentication.
Security Note: The server panics at startup if the listener is bound to a
non-loopback address and authentication is disabled. Set this to `false`
only if a reverse proxy handles authentication for you.
'';
};
corsAllowOrigins = lib.mkOption {
type = lib.types.str;
default = "*";
example = "https://wealthfolio.example.com";
description = ''
Comma-separated list of allowed CORS origins.
Security Note: The server panics at startup if `*` is used while authentication
is enabled, as this is a CSRF vector. Set explicit origins matching your
deployment URL (scheme + host + port).
'';
};
authTokenTtlMinutes = lib.mkOption {
type = lib.types.ints.positive;
default = 60;
description = "JWT access token lifetime in minutes. (e.g., 1440 for 24h, 10080 for 7d).";
};
cookieSecure = lib.mkOption {
type = lib.types.enum [
"auto"
"true"
"false"
];
default = "auto";
description = ''
Controls the Secure attribute on the authentication session cookie.
- auto: Sets Secure based on HTTPS protocol.
- true: Always sets Secure (Use behind a reverse proxy that terminates HTTPS).
- false: Never sets Secure (Not recommended).
'';
};
requestTimeoutMs = lib.mkOption {
type = lib.types.ints.positive;
default = 300000;
description = "HTTP request timeout in milliseconds. Default (5m) accommodates large broker syncs.";
};
logFormat = lib.mkOption {
type = lib.types.enum [
"text"
"json"
];
default = "text";
description = "Log output format. `json` is recommended if shipping to log aggregators.";
};
};
config = lib.mkIf cfg.enable {
networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.port;
assertions = [
{
assertion = cfg.secretKeyFile != null;
message = "services.wealthfolio: secretKeyFile must be provided.";
}
{
assertion = cfg.authRequired -> cfg.authPasswordHashFile != null;
message = "services.wealthfolio: authPasswordHashFile must be provided when authRequired is true.";
}
{
assertion = cfg.authRequired -> cfg.corsAllowOrigins != "*";
message = "services.wealthfolio: corsAllowOrigins cannot be '*' when authRequired is true. Provide an explicit domain.";
}
];
systemd.services.wealthfolio = {
description = "Wealthfolio server service daemon.";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
WF_LISTEN_ADDR = "${cfg.address}:${toString cfg.port}";
WF_DB_PATH = "/var/lib/wealthfolio/wealthfolio.db";
WF_AUTH_REQUIRED = lib.boolToString cfg.authRequired;
WF_CORS_ALLOW_ORIGINS = cfg.corsAllowOrigins;
WF_AUTH_TOKEN_TTL_MINUTES = toString cfg.authTokenTtlMinutes;
WF_COOKIE_SECURE = cfg.cookieSecure;
WF_REQUEST_TIMEOUT_MS = toString cfg.requestTimeoutMs;
WF_LOG_FORMAT = cfg.logFormat;
};
script = ''
${lib.optionalString (
cfg.secretKeyFile != null
) "export WF_SECRET_KEY=$(<\"$CREDENTIALS_DIRECTORY/secret_key\")"}
${lib.optionalString (
cfg.authPasswordHashFile != null
) "export WF_AUTH_PASSWORD_HASH=$(<\"$CREDENTIALS_DIRECTORY/auth_hash\")"}
exec ${lib.getExe cfg.package}
'';
serviceConfig = {
LoadCredential =
lib.optional (cfg.secretKeyFile != null) "secret_key:${cfg.secretKeyFile}"
++ lib.optional (cfg.authPasswordHashFile != null) "auth_hash:${cfg.authPasswordHashFile}";
DynamicUser = true;
StateDirectory = "wealthfolio";
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
};
};
};
meta = {
maintainers = with lib.maintainers; [ luuumine ];
};
}