From ffb34bb6bc84617077007417ca79633b2ff11d74 Mon Sep 17 00:00:00 2001 From: ibizaman Date: Mon, 29 Dec 2025 22:52:15 +0100 Subject: [PATCH] firefly-iii: init --- README.md | 1 + docs/redirects.json | 294 +++++++++++++ docs/services.md | 8 + flake.nix | 4 + modules/services/firefly-iii.nix | 419 +++++++++++++++++++ modules/services/firefly-iii/docs/default.md | 152 +++++++ test/services/firefly-iii.nix | 262 ++++++++++++ 7 files changed, 1140 insertions(+) create mode 100644 modules/services/firefly-iii.nix create mode 100644 modules/services/firefly-iii/docs/default.md create mode 100644 test/services/firefly-iii.nix diff --git a/README.md b/README.md index cb43a06..05e854d 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ Also, the stack fits together nicely thanks to [contracts](#contracts). - Nextcloud - Audiobookshelf - Deluge + *arr stack +- Firefly-iii - Forgejo - Grocy - Hledger diff --git a/docs/redirects.json b/docs/redirects.json index 8212693..596e28a 100644 --- a/docs/redirects.json +++ b/docs/redirects.json @@ -2711,12 +2711,306 @@ "services-category-documents": [ "services.html#services-category-documents" ], + "services-category-finance": [ + "services.html#services-category-finance" + ], "services-category-media": [ "services.html#services-category-media" ], "services-category-passwords": [ "services.html#services-category-passwords" ], + "services-firefly-iii": [ + "services-firefly-iii.html#services-firefly-iii" + ], + "services-firefly-iii-certs": [ + "services-firefly-iii.html#services-firefly-iii-certs" + ], + "services-firefly-iii-database-inspection": [ + "services-firefly-iii.html#services-firefly-iii-database-inspection" + ], + "services-firefly-iii-declarative-ldap": [ + "services-firefly-iii.html#services-firefly-iii-declarative-ldap" + ], + "services-firefly-iii-impermanence": [ + "services-firefly-iii.html#services-firefly-iii-impermanence" + ], + "services-firefly-iii-mobile": [ + "services-firefly-iii.html#services-firefly-iii-mobile" + ], + "services-firefly-iii-options": [ + "services-firefly-iii.html#services-firefly-iii-options" + ], + "services-firefly-iii-options-shb.firefly-iii.appKey": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.appKey" + ], + "services-firefly-iii-options-shb.firefly-iii.appKey.request": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.appKey.request" + ], + "services-firefly-iii-options-shb.firefly-iii.appKey.request.group": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.appKey.request.group" + ], + "services-firefly-iii-options-shb.firefly-iii.appKey.request.mode": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.appKey.request.mode" + ], + "services-firefly-iii-options-shb.firefly-iii.appKey.request.owner": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.appKey.request.owner" + ], + "services-firefly-iii-options-shb.firefly-iii.appKey.request.restartUnits": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.appKey.request.restartUnits" + ], + "services-firefly-iii-options-shb.firefly-iii.appKey.result": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.appKey.result" + ], + "services-firefly-iii-options-shb.firefly-iii.appKey.result.path": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.appKey.result.path" + ], + "services-firefly-iii-options-shb.firefly-iii.backup": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup" + ], + "services-firefly-iii-options-shb.firefly-iii.backup.request": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup.request" + ], + "services-firefly-iii-options-shb.firefly-iii.backup.request.excludePatterns": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup.request.excludePatterns" + ], + "services-firefly-iii-options-shb.firefly-iii.backup.request.hooks": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup.request.hooks" + ], + "services-firefly-iii-options-shb.firefly-iii.backup.request.hooks.afterBackup": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup.request.hooks.afterBackup" + ], + "services-firefly-iii-options-shb.firefly-iii.backup.request.hooks.beforeBackup": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup.request.hooks.beforeBackup" + ], + "services-firefly-iii-options-shb.firefly-iii.backup.request.sourceDirectories": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup.request.sourceDirectories" + ], + "services-firefly-iii-options-shb.firefly-iii.backup.request.user": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup.request.user" + ], + "services-firefly-iii-options-shb.firefly-iii.backup.result": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup.result" + ], + "services-firefly-iii-options-shb.firefly-iii.backup.result.backupService": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup.result.backupService" + ], + "services-firefly-iii-options-shb.firefly-iii.backup.result.restoreScript": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup.result.restoreScript" + ], + "services-firefly-iii-options-shb.firefly-iii.dbPassword": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dbPassword" + ], + "services-firefly-iii-options-shb.firefly-iii.dbPassword.request": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dbPassword.request" + ], + "services-firefly-iii-options-shb.firefly-iii.dbPassword.request.group": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dbPassword.request.group" + ], + "services-firefly-iii-options-shb.firefly-iii.dbPassword.request.mode": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dbPassword.request.mode" + ], + "services-firefly-iii-options-shb.firefly-iii.dbPassword.request.owner": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dbPassword.request.owner" + ], + "services-firefly-iii-options-shb.firefly-iii.dbPassword.request.restartUnits": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dbPassword.request.restartUnits" + ], + "services-firefly-iii-options-shb.firefly-iii.dbPassword.result": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dbPassword.result" + ], + "services-firefly-iii-options-shb.firefly-iii.dbPassword.result.path": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dbPassword.result.path" + ], + "services-firefly-iii-options-shb.firefly-iii.debug": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.debug" + ], + "services-firefly-iii-options-shb.firefly-iii.domain": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.domain" + ], + "services-firefly-iii-options-shb.firefly-iii.enable": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.enable" + ], + "services-firefly-iii-options-shb.firefly-iii.impermanence": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.impermanence" + ], + "services-firefly-iii-options-shb.firefly-iii.importer": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.importer" + ], + "services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken" + ], + "services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.request": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.request" + ], + "services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.request.group": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.request.group" + ], + "services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.request.mode": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.request.mode" + ], + "services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.request.owner": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.request.owner" + ], + "services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.request.restartUnits": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.request.restartUnits" + ], + "services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.result": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.result" + ], + "services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.result.path": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.importer.firefly-iii-accessToken.result.path" + ], + "services-firefly-iii-options-shb.firefly-iii.importer.subdomain": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.importer.subdomain" + ], + "services-firefly-iii-options-shb.firefly-iii.ldap": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.ldap" + ], + "services-firefly-iii-options-shb.firefly-iii.ldap.adminGroup": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.ldap.adminGroup" + ], + "services-firefly-iii-options-shb.firefly-iii.ldap.userGroup": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.ldap.userGroup" + ], + "services-firefly-iii-options-shb.firefly-iii.siteOwnerEmail": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.siteOwnerEmail" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp.from_address": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp.from_address" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp.host": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp.host" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp.password": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp.password" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp.password.request": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp.password.request" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp.password.request.group": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp.password.request.group" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp.password.request.mode": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp.password.request.mode" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp.password.request.owner": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp.password.request.owner" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp.password.request.restartUnits": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp.password.request.restartUnits" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp.password.result": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp.password.result" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp.password.result.path": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp.password.result.path" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp.port": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp.port" + ], + "services-firefly-iii-options-shb.firefly-iii.smtp.username": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.smtp.username" + ], + "services-firefly-iii-options-shb.firefly-iii.ssl": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.ssl" + ], + "services-firefly-iii-options-shb.firefly-iii.ssl.paths": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.ssl.paths" + ], + "services-firefly-iii-options-shb.firefly-iii.ssl.paths.cert": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.ssl.paths.cert" + ], + "services-firefly-iii-options-shb.firefly-iii.ssl.paths.key": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.ssl.paths.key" + ], + "services-firefly-iii-options-shb.firefly-iii.ssl.systemdService": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.ssl.systemdService" + ], + "services-firefly-iii-options-shb.firefly-iii.sso": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.adminGroup": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.adminGroup" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.authEndpoint": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.authEndpoint" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.authorization_policy": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.authorization_policy" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.clientID": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.clientID" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.enable": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.enable" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.port": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.port" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.provider": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.provider" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secret": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secret" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secret.request": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secret.request" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secret.request.group": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secret.request.group" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secret.request.mode": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secret.request.mode" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secret.request.owner": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secret.request.owner" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secret.request.restartUnits": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secret.request.restartUnits" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secret.result": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secret.result" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secret.result.path": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secret.result.path" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.request": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.request" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.request.group": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.request.group" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.request.mode": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.request.mode" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.request.owner": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.request.owner" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.request.restartUnits": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.request.restartUnits" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.result": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.result" + ], + "services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.result.path": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.sso.secretForAuthelia.result.path" + ], + "services-firefly-iii-options-shb.firefly-iii.subdomain": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.subdomain" + ], + "services-firefly-iii-usage": [ + "services-firefly-iii.html#services-firefly-iii-usage" + ], + "services-firefly-iii-usage-backup": [ + "services-firefly-iii.html#services-firefly-iii-usage-backup" + ], "services-forgejo": [ "services-forgejo.html#services-forgejo" ], diff --git a/docs/services.md b/docs/services.md index afffa65..582b68e 100644 --- a/docs/services.md +++ b/docs/services.md @@ -16,6 +16,7 @@ information is provided in the respective manual sections. | Service | Backup | Reverse Proxy | SSO | LDAP | Monitoring | Profiling | |----------------------|--------|---------------|-----|-------|------------|-----------| | [*Arr][] | Y (1) | Y | Y | Y (4) | Y (2) | N | +| [Firefly-iii][] | Y (1) | Y | Y | Y | Y (2) | N | | [Forgejo][] | Y (1) | Y | Y | Y | Y (2) | N | | [Home-Assistant][] | Y (1) | Y | N | Y | Y (2) | N | | [Jellyfin][] | Y (1) | Y | Y | Y | Y (2) | N | @@ -35,6 +36,7 @@ Legend: **N**: no but WIP; **P**: partial; **Y**: yes 4. Uses LDAP indirectly through forward auth. [*Arr]: services-arr.html +[Firefly-iii]: services-firefly-iii.html [Forgejo]: services-forgejo.html [Home-Assistant]: services-home-assistant.html [Jellyfin]: services-jellyfin.html @@ -91,3 +93,9 @@ modules/services/jellyfin/docs/default.md ```{=include=} chapters html:into-file=//services-pinchflat.html modules/services/pinchflat/docs/default.md ``` + +## Finance {#services-category-finance} + +```{=include=} chapters html:into-file=//services-firefly-iii.html +modules/services/firefly-iii/docs/default.md +``` diff --git a/flake.nix b/flake.nix index 7e3da84..f075e47 100644 --- a/flake.nix +++ b/flake.nix @@ -104,6 +104,7 @@ "blocks/restic" = ./modules/blocks/restic.nix; "blocks/sops" = ./modules/blocks/sops.nix; "services/arr" = ./modules/services/arr.nix; + "services/firefly-iii" = ./modules/services/firefly-iii.nix; "services/forgejo" = [ ./modules/services/forgejo.nix (pkgs.path + "/nixos/modules/services/misc/forgejo.nix") @@ -280,6 +281,7 @@ // (vm_test "arr" ./test/services/arr.nix) // (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix) // (vm_test "deluge" ./test/services/deluge.nix) + // (vm_test "firefly-iii" ./test/services/firefly-iii.nix) // (vm_test "forgejo" ./test/services/forgejo.nix) // (vm_test "grocy" ./test/services/grocy.nix) // (vm_test "hledger" ./test/services/hledger.nix) @@ -388,6 +390,7 @@ self.nixosModules.arr self.nixosModules.audiobookshelf self.nixosModules.deluge + self.nixosModules.firefly-iii self.nixosModules.forgejo self.nixosModules.grocy self.nixosModules.hledger @@ -424,6 +427,7 @@ nixosModules.arr = modules/services/arr.nix; nixosModules.audiobookshelf = modules/services/audiobookshelf.nix; nixosModules.deluge = modules/services/deluge.nix; + nixosModules.firefly-iii = modules/services/firefly-iii.nix; nixosModules.forgejo = modules/services/forgejo.nix; nixosModules.grocy = modules/services/grocy.nix; nixosModules.hledger = modules/services/hledger.nix; diff --git a/modules/services/firefly-iii.nix b/modules/services/firefly-iii.nix new file mode 100644 index 0000000..6388e95 --- /dev/null +++ b/modules/services/firefly-iii.nix @@ -0,0 +1,419 @@ +{ + config, + lib, + shb, + ... +}: + +let + cfg = config.shb.firefly-iii; +in +{ + imports = [ + ../../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. + + ``` + . + ``` + ''; + example = "firefly-iii"; + }; + + domain = lib.mkOption { + description = '' + Domain under which firefly-iii is served. + + ``` + .[:] + ``` + ''; + 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 = { + 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; + }; + }; + }; + }; + }; + + 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; + } + ]; + } + { + 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}" ]; + } + { + domain = "${cfg.importer.subdomain}.${cfg.domain}"; + policy = cfg.sso.authorization_policy; + subject = [ "group:${cfg.ldap.adminGroup}" ]; + } + ]; + } + ]; + }) + ] + ); +} diff --git a/modules/services/firefly-iii/docs/default.md b/modules/services/firefly-iii/docs/default.md new file mode 100644 index 0000000..93197b3 --- /dev/null +++ b/modules/services/firefly-iii/docs/default.md @@ -0,0 +1,152 @@ +# Firefly-iii Service {#services-firefly-iii} + +Defined in [`/modules/services/firefly-iii.nix`](@REPO@/modules/services/firefly-iii.nix). + +This NixOS module is a service that sets up a [Firefly-iii](https://www.firefly-iii.org/) instance. + +Compared to the stock module from nixpkgs, +this one sets up, in a fully declarative manner, +LDAP and SSO integration +and has a nicer option for secrets. + +## Usage {#services-firefly-iii-usage} + +The following snippet assumes a few blocks have been setup already: + +- the [secrets block](usage.html#usage-secrets) with SOPS, +- the [`shb.ssl` block](blocks-ssl.html#usage), +- the [`shb.lldap` block](blocks-lldap.html#blocks-lldap-global-setup). +- the [`shb.authelia` block](blocks-authelia.html#blocks-sso-global-setup). + +```nix +shb.firefly-iii = { + enable = true; + debug = false; + + appKey.result = config.shb.sops.secret."firefly-iii/appKey".result; + dbPassword.result = config.shb.sops.secret."firefly-iii/dbPassword".result; + + domain = "example.com"; + subdomain = "firefly-iii"; + siteOwnerEmail = "mail@example.com"; + ssl = config.shb.certs.certs.letsencrypt.${domain}; + + smtp = { + host = "smtp.eu.mailgun.org"; + port = 587; + username = "postmaster@mg.example.com"; + from_address = "firefly-iii@example.com"; + password.result = config.shb.sops.secrets."firefly-iii/smtpPassword".result; + }; + + sso = { + enable = true; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + + importer = { + # See note hereunder. + # firefly-iii-accessToken.result = config.shb.sops.secret."firefly-iii/importerAccessToken".result; + }; +}; +shb.sops.secret."firefly-iii/appKey".request = config.shb.firefly-iii.appKey.request; +shb.sops.secret."firefly-iii/dbPassword".request = config.shb.firefly-iii.dbPassword.request; +shb.sops.secret."firefly-iii/smtpPassword".request = config.shb.firefly-iii.smtp.password.request; +# See not hereunder. +# shb.sops.secret."firefly-iii/importerAccessToken".request = config.shb.firefly-iii.importer.firefly-iii-accessToken.request; +``` + +Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`. +Note that for `appKey`, the secret length must be exactly 32 characters. + +The [user](#services-firefly-iii-options-shb.firefly-iii.ldap.userGroup) +and [admin](#services-firefly-iii-options-shb.firefly-iii.ldap.adminGroup) +LDAP groups are created automatically. +Only admin users have access to the Firefly-iii data importer. +On the Firefly-iii web UI, the first user to login will be the admin. +We cannot yet create multiple admins in the Firefly-iii web UI. + +On first start, leave the `shb.firefly-iii.importer.firefly-iii-accessToken` option empty. +To fill it out and connect the data importer to the Firefly-iii instance, +you must first create a personal access token then fill that option and redeploy. + +## Backup {#services-firefly-iii-usage-backup} + +Backing up Firefly-iii using the [Restic block](blocks-restic.html) is done like so: + +```nix +shb.restic.instances."firefly-iii" = { + request = config.shb.firefly-iii.backup; + settings = { + enable = true; + }; +}; +``` + +The name `"firefly-iii"` in the `instances` can be anything. +The `config.shb.firefly-iii.backup` option provides what directories to backup. +You can define any number of Restic instances to backup Firefly-iii multiple times. + +Linking the Firefly-iii data importer to the Firefly-iii instance must still be done manually +by following the instructions appearing on the web UI. + +## Certificates {#services-firefly-iii-certs} + +For Let's Encrypt certificates, add: + +```nix +{ + shb.certs.certs.letsencrypt.${domain}.extraDomains = [ + "${config.shb.firefly-iii.subdomain}.${config.shb.firefly-iii.domain}" + "${config.shb.firefly-iii.importer.subdomain}.${config.shb.firefly-iii.domain}" + ]; +} +``` + +## Impermanence {#services-firefly-iii-impermanence} + +To save the data folder in an impermanence setup, add: + +```nix +{ + shb.zfs.datasets."safe/firefly-iii".path = config.shb.firefly-iii.impermanence; +} +``` + +## Declarative LDAP {#services-firefly-iii-declarative-ldap} + +To add a user `USERNAME` to the user and admin groups for Firefly-iii, add: + +```nix +shb.lldap.ensureUsers.USERNAME.groups = [ + config.shb.firefly-iii.ldap.userGroup + config.shb.firefly-iii.ldap.adminGroup +]; +``` + +## Database Inspection {#services-firefly-iii-database-inspection} + +Access the database with: + +```nix +sudo -u firefly-iii psql +``` + +Dump the database with: + +```nix +sudo -u firefly-iii pg_dump --data-only --inserts firefly-iii > dump +``` + +## Mobile Apps {#services-firefly-iii-mobile} + +This module was tested with the [Abacus iOS](https://github.com/victorbalssa/abacus) mobile app +using a Personal Account Token. + +## Options Reference {#services-firefly-iii-options} + +```{=include=} options +id-prefix: services-firefly-iii-options- +list-id: selfhostblocks-service-firefly-iii-options +source: @OPTIONS_JSON@ +``` diff --git a/test/services/firefly-iii.nix b/test/services/firefly-iii.nix new file mode 100644 index 0000000..3a271f6 --- /dev/null +++ b/test/services/firefly-iii.nix @@ -0,0 +1,262 @@ +{ pkgs, shb, ... }: +let + commonTestScript = shb.test.mkScripts { + hasSSL = { node, ... }: !(isNull node.config.shb.firefly-iii.ssl); + waitForServices = + { ... }: + [ + "phpfpm-firefly-iii.service" + "nginx.service" + ]; + waitForPorts = + { node, ... }: + [ + # node.config.shb.firefly-iii.port + ]; + }; + + basic = + { config, ... }: + { + imports = [ + shb.test.baseModule + ../../modules/blocks/hardcodedsecret.nix + ../../modules/services/firefly-iii.nix + ]; + + test = { + subdomain = "f"; + }; + + shb.firefly-iii = { + enable = true; + debug = true; + inherit (config.test) subdomain domain; + siteOwnerEmail = "mail@example.com"; + appKey.result = config.shb.hardcodedsecret.appKey.result; + dbPassword.result = config.shb.hardcodedsecret.dbPassword.result; + }; + + # systemd.tmpfiles.rules = [ + # "d '/src/firefly-iii' 0750 pinchflat pinchflat - -" + # ]; + + # Needed for gitea-runner-local to be able to ping firefly-iii. + # networking.hosts = { + # "127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ]; + # }; + + shb.hardcodedsecret.appKey = { + request = config.shb.firefly-iii.appKey.request; + # Firefly-iir requires this to be exactly 32 characters. + settings.content = pkgs.lib.strings.replicate 32 "Z"; + }; + shb.hardcodedsecret.dbPassword = { + request = config.shb.firefly-iii.dbPassword.request; + settings.content = pkgs.lib.strings.replicate 64 "Y"; + }; + }; + + clientLogin = + { config, ... }: + { + imports = [ + shb.test.baseModule + shb.test.clientLoginModule + ]; + test = { + subdomain = "f"; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}"; + # There is no login without SSO integration. + testLoginWith = [ + { + username = null; + password = null; + nextPageExpect = [ + "expect(page.get_by_text('Register a new account')).to_be_visible()" + ]; + } + ]; + }; + }; + + https = + { config, ... }: + { + shb.firefly-iii = { + ssl = config.shb.certs.certs.selfsigned.n; + }; + }; + + ldap = + { config, ... }: + { + shb.firefly-iii = { + ldap = { + userGroup = "user_group"; + }; + }; + }; + + clientLoginSso = + { config, ... }: + { + imports = [ + shb.test.baseModule + shb.test.clientLoginModule + ]; + test = { + subdomain = "f"; + }; + + test.login = { + startUrl = "https://${config.test.fqdn}"; + usernameFieldLabelRegex = "Username"; + passwordFieldLabelRegex = "Password"; + loginButtonNameRegex = "[sS]ign [iI]n"; + testLoginWith = [ + { + username = "alice"; + password = "NotAlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" + ]; + } + { + username = "alice"; + password = "AlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page.get_by_text('Dashboard')).to_be_visible(timeout=10000)" + ]; + } + { + username = "bob"; + password = "NotBobPassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" + ]; + } + { + username = "bob"; + password = "BobPassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page.get_by_text('Dashboard')).to_be_visible(timeout=10000)" + ]; + } + { + username = "charlie"; + password = "NotCharliePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" + ]; + } + { + username = "charlie"; + password = "CharliePassword"; + nextPageExpect = [ + "expect(page).to_have_url(re.compile('.*/authenticated'))" + ]; + } + ]; + }; + }; + + sso = + { config, ... }: + { + shb.firefly-iii = { + sso = { + enable = true; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + }; + }; +in +{ + basic = shb.test.runNixOSTest { + name = "firefly-iii_basic"; + + nodes.client = { + imports = [ + clientLogin + ]; + }; + nodes.server = { + imports = [ + basic + ]; + }; + + testScript = commonTestScript.access; + }; + + backup = shb.test.runNixOSTest { + name = "firefly-iii_backup"; + + nodes.server = + { config, ... }: + { + imports = [ + basic + (shb.test.backup config.shb.firefly-iii.backup) + ]; + }; + + nodes.client = { }; + + testScript = commonTestScript.backup; + }; + + https = shb.test.runNixOSTest { + name = "firefly-iii_https"; + + nodes.client = { + imports = [ + clientLogin + ]; + }; + nodes.server = { + imports = [ + basic + shb.test.certs + https + ]; + }; + + testScript = commonTestScript.access; + }; + + sso = shb.test.runNixOSTest { + name = "firefly-iii_sso"; + + nodes.client = { + imports = [ + clientLoginSso + ]; + }; + nodes.server = + { config, pkgs, ... }: + { + imports = [ + basic + shb.test.certs + https + shb.test.ldap + ldap + (shb.test.sso config.shb.certs.certs.selfsigned.n) + sso + ]; + }; + + testScript = commonTestScript.access.override { + redirectSSO = true; + }; + }; +}