selfhostblocks/modules/services/firefly-iii.nix
2026-02-24 20:00:36 +01:00

457 lines
14 KiB
Nix

{
config,
lib,
shb,
...
}:
let
cfg = config.shb.firefly-iii;
in
{
imports = [
../blocks/nginx.nix
../blocks/lldap.nix
../../lib/module.nix
];
options.shb.firefly-iii = {
enable = lib.mkEnableOption "SHB's firefly-iii module";
subdomain = lib.mkOption {
type = lib.types.str;
description = ''
Subdomain under which firefly-iii will be served.
```
<subdomain>.<domain>
```
'';
example = "firefly-iii";
};
domain = lib.mkOption {
description = ''
Domain under which firefly-iii is served.
```
<subdomain>.<domain>[:<port>]
```
'';
type = lib.types.str;
example = "domain.com";
};
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr shb.contracts.ssl.certs;
default = null;
};
siteOwnerEmail = lib.mkOption {
description = "Email of the site owner.";
type = lib.types.str;
example = "mail@example.com";
};
impermanence = lib.mkOption {
description = ''
Path to save when using impermanence setup.
'';
type = lib.types.str;
default = config.services.firefly-iii.dataDir;
defaultText = "services.firefly-iii.dataDir";
};
backup = lib.mkOption {
description = ''
Backup configuration.
'';
default = { };
type = lib.types.submodule {
options = shb.contracts.backup.mkRequester {
user = config.services.firefly-iii.user;
userText = "services.firefly-iii.user";
sourceDirectories = [
config.services.firefly-iii.dataDir
];
sourceDirectoriesText = ''
[
config.services.firefly-iii.dataDir
]
'';
};
};
};
appKey = lib.mkOption {
description = "Encryption key used for sessions. Must be 32 characters long exactly.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = config.services.firefly-iii.user;
ownerText = "services.firefly-iii.user";
restartUnits = [ "firefly-iii-setup.service" ];
};
};
};
dbPassword = lib.mkOption {
description = "DB password.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0440";
owner = config.services.firefly-iii.user;
ownerText = "services.firefly-iii.user";
group = "postgres";
restartUnits = [
"postgresql.service"
"firefly-iii-setup.service"
];
};
};
};
ldap = lib.mkOption {
description = ''
LDAP Integration
'';
default = { };
type = lib.types.submodule {
options = {
userGroup = lib.mkOption {
type = lib.types.str;
description = "Group users must belong to to be able to login to Firefly-iii.";
default = "firefly-iii_user";
};
adminGroup = lib.mkOption {
type = lib.types.str;
description = "Group users must belong to to be able to import data user the Firefly-iii data importer.";
default = "firefly-iii_admin";
};
};
};
};
sso = lib.mkOption {
description = ''
SSO Integration
'';
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "SSO integration.";
authEndpoint = lib.mkOption {
type = lib.types.str;
description = "OIDC endpoint for SSO.";
example = "https://authelia.example.com";
};
port = lib.mkOption {
description = "If given, adds a port to the endpoint.";
type = lib.types.nullOr lib.types.port;
default = null;
};
provider = lib.mkOption {
type = lib.types.enum [ "Authelia" ];
description = "OIDC provider name, used for display.";
default = "Authelia";
};
clientID = lib.mkOption {
type = lib.types.str;
description = "Client ID for the OIDC endpoint.";
default = "firefly-iii";
};
authorization_policy = lib.mkOption {
type = lib.types.enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
adminGroup = lib.mkOption {
type = lib.types.str;
description = "Group admins must belong to to be able to login to Firefly-iii.";
default = "firefly-iii_admin";
};
secret = lib.mkOption {
description = "OIDC shared secret.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = "firefly-iii";
restartUnits = [ "firefly-iii-setup.service" ];
};
};
};
secretForAuthelia = lib.mkOption {
description = "OIDC shared secret. Content must be the same as `secretFile` option.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = "authelia";
};
};
};
};
};
};
smtp = lib.mkOption {
description = ''
If set, send notifications through smtp.
https://docs.firefly-iii.org/how-to/firefly-iii/advanced/notifications/
'';
default = null;
type = lib.types.nullOr (
lib.types.submodule {
options = {
from_address = lib.mkOption {
type = lib.types.str;
description = "SMTP address from which the emails originate.";
example = "authelia@mydomain.com";
};
host = lib.mkOption {
type = lib.types.str;
description = "SMTP host to send the emails to.";
};
port = lib.mkOption {
type = lib.types.port;
description = "SMTP port to send the emails to.";
default = 25;
};
username = lib.mkOption {
type = lib.types.str;
description = "Username to connect to the SMTP host.";
};
password = lib.mkOption {
description = "File containing the password to connect to the SMTP host.";
type = lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = config.services.firefly-iii.user;
ownerText = "services.firefly-iii.user";
restartUnits = [ "firefly-iii-setup.service" ];
};
};
};
};
}
);
};
debug = lib.mkOption {
type = lib.types.bool;
description = "Enable more verbose logging.";
default = false;
example = true;
};
importer = lib.mkOption {
description = ''
Configuration for Firefly-iii data importer.
'';
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "Firefly-iii Data Importer." // {
default = true;
};
subdomain = lib.mkOption {
type = lib.types.str;
description = ''
Subdomain under which the firefly-iii data importer will be served.
'';
default = "${cfg.subdomain}-importer";
defaultText = lib.literalExpression "\${shb.firefly-iii.subdomain}-importer";
};
firefly-iii-accessToken = lib.mkOption {
type = lib.types.nullOr (
lib.types.submodule {
options = shb.contracts.secret.mkRequester {
mode = "0400";
owner = config.services.firefly-iii-data-importer.user;
ownerText = "services.firefly-iii-data-importer.user";
restartUnits = [ "firefly-iii-data-importer-setup.service" ];
};
}
);
description = ''
Create a Personal Access Token then set then token in this option.
'';
default = null;
};
};
};
};
dashboard = lib.mkOption {
description = ''
Dashboard contract consumer
'';
default = { };
type = lib.types.submodule {
options = shb.contracts.dashboard.mkRequester {
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
externalUrlText = "https://\${config.shb.firefly-iii.subdomain}.\${config.shb.firefly-iii.domain}";
# This works thanks to the Personal Access Token.
internalUrl = "https://${cfg.subdomain}.${cfg.domain}";
internalUrlText = "https://\${config.shb.firefly-iii.subdomain}.\${config.shb.firefly-iii.domain}";
};
};
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
services.firefly-iii = {
enable = true;
group = "nginx";
virtualHost = "${cfg.subdomain}.${cfg.domain}";
# https://github.com/firefly-iii/firefly-iii/blob/main/.env.example
settings = {
APP_ENV = "production";
APP_URL = "https://${cfg.subdomain}.${cfg.domain}";
APP_DEBUG = cfg.debug;
APP_LOG_LEVEL = if cfg.debug then "debug" else "notice";
LOG_CHANNEL = "stdout";
APP_KEY_FILE = cfg.appKey.result.path;
SITE_OWNER = cfg.siteOwnerEmail;
DB_CONNECTION = "pgsql";
DB_HOST = "localhost";
DB_PORT = config.services.postgresql.settings.port;
DB_DATABASE = "firefly-iii";
DB_USERNAME = "firefly-iii";
DB_PASSWORD_FILE = cfg.dbPassword.result.path;
# MAP_DEFAULT_LAT = "51.983333";
# MAP_DEFAULT_LONG = "5.916667";
# MAP_DEFAULT_ZOOM = "6";
};
};
shb.postgresql.enableTCPIP = true;
shb.postgresql.ensures = [
{
username = "firefly-iii";
database = "firefly-iii";
passwordFile = cfg.dbPassword.result.path;
}
];
# This should be using a contract instead of setting the option directly.
shb.lldap = lib.mkIf config.shb.lldap.enable {
ensureGroups = {
${cfg.ldap.userGroup} = { };
${cfg.ldap.adminGroup} = { };
};
};
# We enable the firefly-iii nginx integration and merge it with SHB's nginx configuration.
services.firefly-iii.enableNginx = true;
shb.nginx.vhosts = [
{
inherit (cfg) subdomain domain ssl;
}
];
}
(lib.mkIf cfg.importer.enable {
services.firefly-iii-data-importer = {
enable = true;
virtualHost = "${cfg.importer.subdomain}.${cfg.domain}";
settings = {
FIREFLY_III_URL = "https://${config.services.firefly-iii.virtualHost}";
}
// lib.optionalAttrs (cfg.importer.firefly-iii-accessToken != null) {
FIREFLY_III_ACCESS_TOKEN_FILE = cfg.importer.firefly-iii-accessToken.result.path;
};
};
# We enable the firefly-iii-data-importer nginx integration and merge it with SHB's nginx configuration.
services.firefly-iii-data-importer.enableNginx = true;
shb.nginx.vhosts = [
{
inherit (cfg) domain ssl;
subdomain = cfg.importer.subdomain;
}
];
})
(lib.mkIf (cfg.smtp != null) {
services.firefly-iii.settings = {
MAIL_MAILER = "smtp";
MAIL_HOST = cfg.smtp.host;
MAIL_PORT = cfg.smtp.port;
MAIL_FROM = cfg.smtp.from_address;
MAIL_USERNAME = cfg.smtp.username;
MAIL_PASSWORD_FILE = cfg.smtp.password.result.path;
MAIL_ENCRYPTION = "tls";
};
})
(lib.mkIf cfg.sso.enable {
services.firefly-iii.settings = {
AUTHENTICATION_GUARD = "remote_user_guard";
AUTHENTICATION_GUARD_HEADER = "HTTP_X_FORWARDED_USER";
};
shb.nginx.vhosts = [
{
inherit (cfg) subdomain domain ssl;
inherit (cfg.sso) authEndpoint;
phpForwardAuth = true;
autheliaRules = [
{
domain = "${cfg.subdomain}.${cfg.domain}";
policy = "bypass";
resources = [ "^/api" ];
}
{
domain = "${cfg.subdomain}.${cfg.domain}";
policy = cfg.sso.authorization_policy;
subject = [
"group:${cfg.ldap.userGroup}"
"group:${cfg.ldap.adminGroup}"
];
}
];
}
];
})
(lib.mkIf (cfg.sso.enable && cfg.importer.enable) {
shb.nginx.vhosts = [
{
inherit (cfg.importer) subdomain;
inherit (cfg) domain ssl;
inherit (cfg.sso) authEndpoint;
autheliaRules = [
{
domain = "${cfg.importer.subdomain}.${cfg.domain}";
policy = cfg.sso.authorization_policy;
subject = [ "group:${cfg.ldap.adminGroup}" ];
}
];
}
];
})
]
);
}