diff --git a/docs/contracts.md b/docs/contracts.md index dc7a2e9..dbb4b0f 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -37,13 +37,17 @@ Provided contracts are: - [SSL generator contract](contracts-ssl.html) to generate SSL certificates. Two providers are implemented: self-signed and Let's Encrypt. - [Backup contract][] to backup directories. - One provider is implemented: [Restic][]. + Two providers are implemented: [BorgBackup][] and [Restic][]. - [Database Backup contract](contracts-databasebackup.html) to backup database dumps. - One provider is implemented: [Restic][]. -- [Secret contract](contracts-secret.html) to provide secrets that are deployed outside of the Nix store. + One provider is implemented: [BorgBackup][] and [Restic][]. +- [Contract for Secrets](contracts-secret.html) to provide secrets that are deployed outside of the Nix store. One provider is implemented: [SOPS][]. +- [Dashboard contract](contracts-dashboard.html) to show services in a nice user-facing dashboard. + One provider is implemented: [Homepage][]. [backup contract]: contracts-backup.html +[borgbackup]: blocks-borgbackup.html +[homepage]: services-homepage.html [restic]: blocks-restic.html [sops]: blocks-sops.html @@ -63,6 +67,10 @@ modules/contracts/databasebackup/docs/default.md modules/contracts/secret/docs/default.md ``` +```{=include=} chapters html:into-file=//contracts-dashboard.html +modules/contracts/dashboard/docs/default.md +``` + ## Problem Statement {#contracts-why} Currently in nixpkgs, every module accessing a shared resource diff --git a/docs/recipes/exposeService.md b/docs/recipes/exposeService.md index d4724c1..7f0f6be 100644 --- a/docs/recipes/exposeService.md +++ b/docs/recipes/exposeService.md @@ -128,3 +128,29 @@ shb.sops.secret."awesome" = { request = config.shb.restic.instances.awesome.settings.passphrase.request; }; ``` + +## Impermanence {#recipes-exposeService-impermanence} + +To save the data folder in an impermanence setup, add: + +```nix +{ + shb.zfs.datasets."safe/awesome".path = config.services.awesome.dataDir; +} +``` + +## Application Dashboard {#recipes-exposeService-applicationdashboard} + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.MyServices.services.Awesome = { + sortOrder = 1; + dashboard.request = { + externalUrl = "https://${fqdn}"; + internalUrl = "http://127.0.0.1:${toString listenPort}"; + }; + }; +} +``` diff --git a/docs/redirects.json b/docs/redirects.json index 0e6e87f..10e5030 100644 --- a/docs/redirects.json +++ b/docs/redirects.json @@ -53,9 +53,6 @@ "blocks-authelia-forward-auth": [ "blocks-authelia.html#blocks-authelia-forward-auth" ], - "blocks-authelia-global-setup": [ - "blocks-authelia.html#blocks-authelia-global-setup" - ], "blocks-authelia-oidc": [ "blocks-authelia.html#blocks-authelia-oidc" ], @@ -65,6 +62,21 @@ "blocks-authelia-options-shb.authelia.autheliaUser": [ "blocks-authelia.html#blocks-authelia-options-shb.authelia.autheliaUser" ], + "blocks-authelia-options-shb.authelia.dashboard": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard" + ], + "blocks-authelia-options-shb.authelia.dashboard.request": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard.request" + ], + "blocks-authelia-options-shb.authelia.dashboard.request.externalUrl": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard.request.externalUrl" + ], + "blocks-authelia-options-shb.authelia.dashboard.request.internalUrl": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard.request.internalUrl" + ], + "blocks-authelia-options-shb.authelia.dashboard.result": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard.result" + ], "blocks-authelia-options-shb.authelia.dcdomain": [ "blocks-authelia.html#blocks-authelia-options-shb.authelia.dcdomain" ], @@ -323,6 +335,9 @@ "blocks-authelia-troubleshooting": [ "blocks-authelia.html#blocks-authelia-troubleshooting" ], + "blocks-authelia-usage-configuration": [ + "blocks-authelia.html#blocks-authelia-usage-configuration" + ], "blocks-borgbackup": [ "blocks-borgbackup.html#blocks-borgbackup" ], @@ -578,8 +593,8 @@ "blocks-lldap": [ "blocks-lldap.html#blocks-lldap" ], - "blocks-lldap-global-setup": [ - "blocks-lldap.html#blocks-lldap-global-setup" + "blocks-lldap-features": [ + "blocks-lldap.html#blocks-lldap-features" ], "blocks-lldap-manage-groups": [ "blocks-lldap.html#blocks-lldap-manage-groups" @@ -623,6 +638,21 @@ "blocks-lldap-options-shb.lldap.backup.result.restoreScript": [ "blocks-lldap.html#blocks-lldap-options-shb.lldap.backup.result.restoreScript" ], + "blocks-lldap-options-shb.lldap.dashboard": [ + "blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard" + ], + "blocks-lldap-options-shb.lldap.dashboard.request": [ + "blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard.request" + ], + "blocks-lldap-options-shb.lldap.dashboard.request.externalUrl": [ + "blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard.request.externalUrl" + ], + "blocks-lldap-options-shb.lldap.dashboard.request.internalUrl": [ + "blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard.request.internalUrl" + ], + "blocks-lldap-options-shb.lldap.dashboard.result": [ + "blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard.result" + ], "blocks-lldap-options-shb.lldap.dcdomain": [ "blocks-lldap.html#blocks-lldap-options-shb.lldap.dcdomain" ], @@ -824,18 +854,27 @@ "blocks-lldap-options-shb.lldap.webUIListenPort": [ "blocks-lldap.html#blocks-lldap-options-shb.lldap.webUIListenPort" ], - "blocks-lldap-restrict-access-by-ip": [ - "blocks-lldap.html#blocks-lldap-restrict-access-by-ip" - ], - "blocks-lldap-ssl": [ - "blocks-lldap.html#blocks-lldap-ssl" - ], "blocks-lldap-tests": [ "blocks-lldap.html#blocks-lldap-tests" ], "blocks-lldap-troubleshooting": [ "blocks-lldap.html#blocks-lldap-troubleshooting" ], + "blocks-lldap-usage": [ + "blocks-lldap.html#blocks-lldap-usage" + ], + "blocks-lldap-usage-applicationdashboard": [ + "blocks-lldap.html#blocks-lldap-usage-applicationdashboard" + ], + "blocks-lldap-usage-configuration": [ + "blocks-lldap.html#blocks-lldap-usage-configuration" + ], + "blocks-lldap-usage-restrict-access-by-ip": [ + "blocks-lldap.html#blocks-lldap-usage-restrict-access-by-ip" + ], + "blocks-lldap-usage-ssl": [ + "blocks-lldap.html#blocks-lldap-usage-ssl" + ], "blocks-mitmdump": [ "blocks-mitmdump.html#blocks-mitmdump" ], @@ -953,6 +992,21 @@ "blocks-monitoring-options-shb.monitoring.contactPoints": [ "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.contactPoints" ], + "blocks-monitoring-options-shb.monitoring.dashboard": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard" + ], + "blocks-monitoring-options-shb.monitoring.dashboard.request": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard.request" + ], + "blocks-monitoring-options-shb.monitoring.dashboard.request.externalUrl": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard.request.externalUrl" + ], + "blocks-monitoring-options-shb.monitoring.dashboard.request.internalUrl": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard.request.internalUrl" + ], + "blocks-monitoring-options-shb.monitoring.dashboard.result": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard.result" + ], "blocks-monitoring-options-shb.monitoring.dashboards": [ "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboards" ], @@ -1133,6 +1187,12 @@ "blocks-monitoring-usage": [ "blocks-monitoring.html#blocks-monitoring-usage" ], + "blocks-monitoring-usage-applicationdashboard": [ + "blocks-monitoring.html#blocks-monitoring-usage-applicationdashboard" + ], + "blocks-monitoring-usage-configuration": [ + "blocks-monitoring.html#blocks-monitoring-usage-configuration" + ], "blocks-monitoring-usage-log-optimization": [ "blocks-monitoring.html#blocks-monitoring-usage-log-optimization" ], @@ -1712,6 +1772,15 @@ "contract-backup-usage": [ "contracts-backup.html#contract-backup-usage" ], + "contract-dashboard": [ + "contracts-dashboard.html#contract-dashboard" + ], + "contract-dashboard-options": [ + "contracts-dashboard.html#contract-dashboard-options" + ], + "contract-dashboard-providers": [ + "contracts-dashboard.html#contract-dashboard-providers" + ], "contract-databasebackup": [ "contracts-databasebackup.html#contract-databasebackup" ], @@ -1805,6 +1874,27 @@ "contracts-concept": [ "contracts.html#contracts-concept" ], + "contracts-dashboard-options-shb.contracts.dashboard": [ + "contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard" + ], + "contracts-dashboard-options-shb.contracts.dashboard.request": [ + "contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.request" + ], + "contracts-dashboard-options-shb.contracts.dashboard.request.externalUrl": [ + "contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.request.externalUrl" + ], + "contracts-dashboard-options-shb.contracts.dashboard.request.internalUrl": [ + "contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.request.internalUrl" + ], + "contracts-dashboard-options-shb.contracts.dashboard.result": [ + "contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.result" + ], + "contracts-dashboard-options-shb.contracts.dashboard.settings": [ + "contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.settings" + ], + "contracts-dashboard-usage": [ + "contracts-dashboard.html#contracts-dashboard-usage" + ], "contracts-databasebackup-options-shb.contracts.databasebackup": [ "contracts-databasebackup.html#contracts-databasebackup-options-shb.contracts.databasebackup" ], @@ -2156,12 +2246,18 @@ "recipes-exposeService": [ "recipes-exposeService.html#recipes-exposeService" ], + "recipes-exposeService-applicationdashboard": [ + "recipes-exposeService.html#recipes-exposeService-applicationdashboard" + ], "recipes-exposeService-backup": [ "recipes-exposeService.html#recipes-exposeService-backup" ], "recipes-exposeService-debug": [ "recipes-exposeService.html#recipes-exposeService-debug" ], + "recipes-exposeService-impermanence": [ + "recipes-exposeService.html#recipes-exposeService-impermanence" + ], "recipes-exposeService-ldap": [ "recipes-exposeService.html#recipes-exposeService-ldap" ], @@ -2216,6 +2312,9 @@ "services-arr": [ "services-arr.html#services-arr" ], + "services-arr-features": [ + "services-arr.html#services-arr-features" + ], "services-arr-options": [ "services-arr.html#services-arr-options" ], @@ -2258,6 +2357,21 @@ "services-arr-options-shb.arr.bazarr.backup.result.restoreScript": [ "services-arr.html#services-arr-options-shb.arr.bazarr.backup.result.restoreScript" ], + "services-arr-options-shb.arr.bazarr.dashboard": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.dashboard" + ], + "services-arr-options-shb.arr.bazarr.dashboard.request": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.dashboard.request" + ], + "services-arr-options-shb.arr.bazarr.dashboard.request.externalUrl": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.dashboard.request.externalUrl" + ], + "services-arr-options-shb.arr.bazarr.dashboard.request.internalUrl": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.dashboard.request.internalUrl" + ], + "services-arr-options-shb.arr.bazarr.dashboard.result": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.dashboard.result" + ], "services-arr-options-shb.arr.bazarr.dataDir": [ "services-arr.html#services-arr-options-shb.arr.bazarr.dataDir" ], @@ -2273,6 +2387,15 @@ "services-arr-options-shb.arr.bazarr.settings": [ "services-arr.html#services-arr-options-shb.arr.bazarr.settings" ], + "services-arr-options-shb.arr.bazarr.settings.ApiKey": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.settings.ApiKey" + ], + "services-arr-options-shb.arr.bazarr.settings.ApiKey.source": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.settings.ApiKey.source" + ], + "services-arr-options-shb.arr.bazarr.settings.ApiKey.transform": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.settings.ApiKey.transform" + ], "services-arr-options-shb.arr.bazarr.settings.LogLevel": [ "services-arr.html#services-arr-options-shb.arr.bazarr.settings.LogLevel" ], @@ -2336,6 +2459,21 @@ "services-arr-options-shb.arr.jackett.backup.result.restoreScript": [ "services-arr.html#services-arr-options-shb.arr.jackett.backup.result.restoreScript" ], + "services-arr-options-shb.arr.jackett.dashboard": [ + "services-arr.html#services-arr-options-shb.arr.jackett.dashboard" + ], + "services-arr-options-shb.arr.jackett.dashboard.request": [ + "services-arr.html#services-arr-options-shb.arr.jackett.dashboard.request" + ], + "services-arr-options-shb.arr.jackett.dashboard.request.externalUrl": [ + "services-arr.html#services-arr-options-shb.arr.jackett.dashboard.request.externalUrl" + ], + "services-arr-options-shb.arr.jackett.dashboard.request.internalUrl": [ + "services-arr.html#services-arr-options-shb.arr.jackett.dashboard.request.internalUrl" + ], + "services-arr-options-shb.arr.jackett.dashboard.result": [ + "services-arr.html#services-arr-options-shb.arr.jackett.dashboard.result" + ], "services-arr-options-shb.arr.jackett.dataDir": [ "services-arr.html#services-arr-options-shb.arr.jackett.dataDir" ], @@ -2441,6 +2579,21 @@ "services-arr-options-shb.arr.lidarr.backup.result.restoreScript": [ "services-arr.html#services-arr-options-shb.arr.lidarr.backup.result.restoreScript" ], + "services-arr-options-shb.arr.lidarr.dashboard": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.dashboard" + ], + "services-arr-options-shb.arr.lidarr.dashboard.request": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.dashboard.request" + ], + "services-arr-options-shb.arr.lidarr.dashboard.request.externalUrl": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.dashboard.request.externalUrl" + ], + "services-arr-options-shb.arr.lidarr.dashboard.request.internalUrl": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.dashboard.request.internalUrl" + ], + "services-arr-options-shb.arr.lidarr.dashboard.result": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.dashboard.result" + ], "services-arr-options-shb.arr.lidarr.dataDir": [ "services-arr.html#services-arr-options-shb.arr.lidarr.dataDir" ], @@ -2456,6 +2609,15 @@ "services-arr-options-shb.arr.lidarr.settings": [ "services-arr.html#services-arr-options-shb.arr.lidarr.settings" ], + "services-arr-options-shb.arr.lidarr.settings.ApiKey": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.settings.ApiKey" + ], + "services-arr-options-shb.arr.lidarr.settings.ApiKey.source": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.settings.ApiKey.source" + ], + "services-arr-options-shb.arr.lidarr.settings.ApiKey.transform": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.settings.ApiKey.transform" + ], "services-arr-options-shb.arr.lidarr.settings.LogLevel": [ "services-arr.html#services-arr-options-shb.arr.lidarr.settings.LogLevel" ], @@ -2519,6 +2681,21 @@ "services-arr-options-shb.arr.radarr.backup.result.restoreScript": [ "services-arr.html#services-arr-options-shb.arr.radarr.backup.result.restoreScript" ], + "services-arr-options-shb.arr.radarr.dashboard": [ + "services-arr.html#services-arr-options-shb.arr.radarr.dashboard" + ], + "services-arr-options-shb.arr.radarr.dashboard.request": [ + "services-arr.html#services-arr-options-shb.arr.radarr.dashboard.request" + ], + "services-arr-options-shb.arr.radarr.dashboard.request.externalUrl": [ + "services-arr.html#services-arr-options-shb.arr.radarr.dashboard.request.externalUrl" + ], + "services-arr-options-shb.arr.radarr.dashboard.request.internalUrl": [ + "services-arr.html#services-arr-options-shb.arr.radarr.dashboard.request.internalUrl" + ], + "services-arr-options-shb.arr.radarr.dashboard.result": [ + "services-arr.html#services-arr-options-shb.arr.radarr.dashboard.result" + ], "services-arr-options-shb.arr.radarr.dataDir": [ "services-arr.html#services-arr-options-shb.arr.radarr.dataDir" ], @@ -2609,6 +2786,21 @@ "services-arr-options-shb.arr.readarr.backup.result.restoreScript": [ "services-arr.html#services-arr-options-shb.arr.readarr.backup.result.restoreScript" ], + "services-arr-options-shb.arr.readarr.dashboard": [ + "services-arr.html#services-arr-options-shb.arr.readarr.dashboard" + ], + "services-arr-options-shb.arr.readarr.dashboard.request": [ + "services-arr.html#services-arr-options-shb.arr.readarr.dashboard.request" + ], + "services-arr-options-shb.arr.readarr.dashboard.request.externalUrl": [ + "services-arr.html#services-arr-options-shb.arr.readarr.dashboard.request.externalUrl" + ], + "services-arr-options-shb.arr.readarr.dashboard.request.internalUrl": [ + "services-arr.html#services-arr-options-shb.arr.readarr.dashboard.request.internalUrl" + ], + "services-arr-options-shb.arr.readarr.dashboard.result": [ + "services-arr.html#services-arr-options-shb.arr.readarr.dashboard.result" + ], "services-arr-options-shb.arr.readarr.dataDir": [ "services-arr.html#services-arr-options-shb.arr.readarr.dataDir" ], @@ -2624,6 +2816,15 @@ "services-arr-options-shb.arr.readarr.settings": [ "services-arr.html#services-arr-options-shb.arr.readarr.settings" ], + "services-arr-options-shb.arr.readarr.settings.ApiKey": [ + "services-arr.html#services-arr-options-shb.arr.readarr.settings.ApiKey" + ], + "services-arr-options-shb.arr.readarr.settings.ApiKey.source": [ + "services-arr.html#services-arr-options-shb.arr.readarr.settings.ApiKey.source" + ], + "services-arr-options-shb.arr.readarr.settings.ApiKey.transform": [ + "services-arr.html#services-arr-options-shb.arr.readarr.settings.ApiKey.transform" + ], "services-arr-options-shb.arr.readarr.settings.LogLevel": [ "services-arr.html#services-arr-options-shb.arr.readarr.settings.LogLevel" ], @@ -2687,6 +2888,21 @@ "services-arr-options-shb.arr.sonarr.backup.result.restoreScript": [ "services-arr.html#services-arr-options-shb.arr.sonarr.backup.result.restoreScript" ], + "services-arr-options-shb.arr.sonarr.dashboard": [ + "services-arr.html#services-arr-options-shb.arr.sonarr.dashboard" + ], + "services-arr-options-shb.arr.sonarr.dashboard.request": [ + "services-arr.html#services-arr-options-shb.arr.sonarr.dashboard.request" + ], + "services-arr-options-shb.arr.sonarr.dashboard.request.externalUrl": [ + "services-arr.html#services-arr-options-shb.arr.sonarr.dashboard.request.externalUrl" + ], + "services-arr-options-shb.arr.sonarr.dashboard.request.internalUrl": [ + "services-arr.html#services-arr-options-shb.arr.sonarr.dashboard.request.internalUrl" + ], + "services-arr-options-shb.arr.sonarr.dashboard.result": [ + "services-arr.html#services-arr-options-shb.arr.sonarr.dashboard.result" + ], "services-arr-options-shb.arr.sonarr.dataDir": [ "services-arr.html#services-arr-options-shb.arr.sonarr.dataDir" ], @@ -2735,6 +2951,30 @@ "services-arr-options-shb.arr.sonarr.subdomain": [ "services-arr.html#services-arr-options-shb.arr.sonarr.subdomain" ], + "services-arr-usage": [ + "services-arr.html#services-arr-usage" + ], + "services-arr-usage-apikeys": [ + "services-arr.html#services-arr-usage-apikeys" + ], + "services-arr-usage-applicationdashboard": [ + "services-arr.html#services-arr-usage-applicationdashboard" + ], + "services-arr-usage-configuration": [ + "services-arr.html#services-arr-usage-configuration" + ], + "services-arr-usage-jackett-proxy": [ + "services-arr.html#services-arr-usage-jackett-proxy" + ], + "services-authelia-features": [ + "blocks-authelia.html#services-authelia-features" + ], + "services-authelia-usage": [ + "blocks-authelia.html#services-authelia-usage" + ], + "services-authelia-usage-applicationdashboard": [ + "blocks-authelia.html#services-authelia-usage-applicationdashboard" + ], "services-category-ai": [ "services.html#services-category-ai" ], @@ -2744,6 +2984,9 @@ "services-category-code": [ "services.html#services-category-code" ], + "services-category-dashboard": [ + "services.html#services-category-dashboard" + ], "services-category-documents": [ "services.html#services-category-documents" ], @@ -2771,6 +3014,9 @@ "services-firefly-iii-declarative-ldap": [ "services-firefly-iii.html#services-firefly-iii-declarative-ldap" ], + "services-firefly-iii-features": [ + "services-firefly-iii.html#services-firefly-iii-features" + ], "services-firefly-iii-impermanence": [ "services-firefly-iii.html#services-firefly-iii-impermanence" ], @@ -2837,6 +3083,21 @@ "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.dashboard": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard" + ], + "services-firefly-iii-options-shb.firefly-iii.dashboard.request": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard.request" + ], + "services-firefly-iii-options-shb.firefly-iii.dashboard.request.externalUrl": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard.request.externalUrl" + ], + "services-firefly-iii-options-shb.firefly-iii.dashboard.request.internalUrl": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard.request.internalUrl" + ], + "services-firefly-iii-options-shb.firefly-iii.dashboard.result": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard.result" + ], "services-firefly-iii-options-shb.firefly-iii.dbPassword": [ "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dbPassword" ], @@ -3050,9 +3311,15 @@ "services-firefly-iii-usage": [ "services-firefly-iii.html#services-firefly-iii-usage" ], + "services-firefly-iii-usage-applicationdashboard": [ + "services-firefly-iii.html#services-firefly-iii-usage-applicationdashboard" + ], "services-firefly-iii-usage-backup": [ "services-firefly-iii.html#services-firefly-iii-usage-backup" ], + "services-firefly-iii-usage-configuration": [ + "services-firefly-iii.html#services-firefly-iii-usage-configuration" + ], "services-forgejo": [ "services-forgejo.html#services-forgejo" ], @@ -3098,6 +3365,21 @@ "services-forgejo-options-shb.forgejo.backup.result.restoreScript": [ "services-forgejo.html#services-forgejo-options-shb.forgejo.backup.result.restoreScript" ], + "services-forgejo-options-shb.forgejo.dashboard": [ + "services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard" + ], + "services-forgejo-options-shb.forgejo.dashboard.request": [ + "services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard.request" + ], + "services-forgejo-options-shb.forgejo.dashboard.request.externalUrl": [ + "services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard.request.externalUrl" + ], + "services-forgejo-options-shb.forgejo.dashboard.request.internalUrl": [ + "services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard.request.internalUrl" + ], + "services-forgejo-options-shb.forgejo.dashboard.result": [ + "services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard.result" + ], "services-forgejo-options-shb.forgejo.databasePassword": [ "services-forgejo.html#services-forgejo-options-shb.forgejo.databasePassword" ], @@ -3359,6 +3641,9 @@ "services-forgejo-usage": [ "services-forgejo.html#services-forgejo-usage" ], + "services-forgejo-usage-applicationdashboard": [ + "services-forgejo.html#services-forgejo-usage-applicationdashboard" + ], "services-forgejo-usage-backup": [ "services-forgejo.html#services-forgejo-usage-backup" ], @@ -3446,6 +3731,21 @@ "services-home-assistant-options-shb.home-assistant.config.unit_system": [ "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.config.unit_system" ], + "services-home-assistant-options-shb.home-assistant.dashboard": [ + "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard" + ], + "services-home-assistant-options-shb.home-assistant.dashboard.request": [ + "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard.request" + ], + "services-home-assistant-options-shb.home-assistant.dashboard.request.externalUrl": [ + "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard.request.externalUrl" + ], + "services-home-assistant-options-shb.home-assistant.dashboard.request.internalUrl": [ + "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard.request.internalUrl" + ], + "services-home-assistant-options-shb.home-assistant.dashboard.result": [ + "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard.result" + ], "services-home-assistant-options-shb.home-assistant.domain": [ "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.domain" ], @@ -3503,6 +3803,9 @@ "services-home-assistant-usage": [ "services-home-assistant.html#services-home-assistant-usage" ], + "services-home-assistant-usage-applicationdashboard": [ + "services-home-assistant.html#services-home-assistant-usage-applicationdashboard" + ], "services-home-assistant-usage-backup": [ "services-home-assistant.html#services-home-assistant-usage-backup" ], @@ -3539,6 +3842,132 @@ "services-home-assistant-usage-voice": [ "services-home-assistant.html#services-home-assistant-usage-voice" ], + "services-homepage": [ + "services-homepage.html#services-homepage" + ], + "services-homepage-features": [ + "services-homepage.html#services-homepage-features" + ], + "services-homepage-options": [ + "services-homepage.html#services-homepage-options" + ], + "services-homepage-options-shb.homepage.domain": [ + "services-homepage.html#services-homepage-options-shb.homepage.domain" + ], + "services-homepage-options-shb.homepage.enable": [ + "services-homepage.html#services-homepage-options-shb.homepage.enable" + ], + "services-homepage-options-shb.homepage.ldap": [ + "services-homepage.html#services-homepage-options-shb.homepage.ldap" + ], + "services-homepage-options-shb.homepage.ldap.userGroup": [ + "services-homepage.html#services-homepage-options-shb.homepage.ldap.userGroup" + ], + "services-homepage-options-shb.homepage.servicesGroups": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.name": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.name" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.group": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.group" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.mode": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.mode" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.owner": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.owner" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.restartUnits": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.restartUnits" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.result": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.result" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.result.path": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.result.path" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request.externalUrl": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request.externalUrl" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request.internalUrl": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request.internalUrl" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.result": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.result" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.name": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.name" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.settings": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.settings" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.sortOrder": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.sortOrder" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.sortOrder": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.sortOrder" + ], + "services-homepage-options-shb.homepage.ssl": [ + "services-homepage.html#services-homepage-options-shb.homepage.ssl" + ], + "services-homepage-options-shb.homepage.ssl.paths": [ + "services-homepage.html#services-homepage-options-shb.homepage.ssl.paths" + ], + "services-homepage-options-shb.homepage.ssl.paths.cert": [ + "services-homepage.html#services-homepage-options-shb.homepage.ssl.paths.cert" + ], + "services-homepage-options-shb.homepage.ssl.paths.key": [ + "services-homepage.html#services-homepage-options-shb.homepage.ssl.paths.key" + ], + "services-homepage-options-shb.homepage.ssl.systemdService": [ + "services-homepage.html#services-homepage-options-shb.homepage.ssl.systemdService" + ], + "services-homepage-options-shb.homepage.sso": [ + "services-homepage.html#services-homepage-options-shb.homepage.sso" + ], + "services-homepage-options-shb.homepage.sso.authEndpoint": [ + "services-homepage.html#services-homepage-options-shb.homepage.sso.authEndpoint" + ], + "services-homepage-options-shb.homepage.sso.authorization_policy": [ + "services-homepage.html#services-homepage-options-shb.homepage.sso.authorization_policy" + ], + "services-homepage-options-shb.homepage.sso.enable": [ + "services-homepage.html#services-homepage-options-shb.homepage.sso.enable" + ], + "services-homepage-options-shb.homepage.subdomain": [ + "services-homepage.html#services-homepage-options-shb.homepage.subdomain" + ], + "services-homepage-usage": [ + "services-homepage.html#services-homepage-usage" + ], + "services-homepage-usage-custom": [ + "services-homepage.html#services-homepage-usage-custom" + ], + "services-homepage-usage-main": [ + "services-homepage.html#services-homepage-usage-main" + ], + "services-homepage-usage-service": [ + "services-homepage.html#services-homepage-usage-service" + ], + "services-homepage-usage-widget": [ + "services-homepage.html#services-homepage-usage-widget" + ], "services-jellyfin": [ "services-jellyfin.html#services-jellyfin" ], @@ -3623,6 +4052,21 @@ "services-jellyfin-options-shb.jellyfin.backup.result.restoreScript": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.backup.result.restoreScript" ], + "services-jellyfin-options-shb.jellyfin.dashboard": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard" + ], + "services-jellyfin-options-shb.jellyfin.dashboard.request": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard.request" + ], + "services-jellyfin-options-shb.jellyfin.dashboard.request.externalUrl": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard.request.externalUrl" + ], + "services-jellyfin-options-shb.jellyfin.dashboard.request.internalUrl": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard.request.internalUrl" + ], + "services-jellyfin-options-shb.jellyfin.dashboard.result": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard.result" + ], "services-jellyfin-options-shb.jellyfin.debug": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.debug" ], @@ -3776,9 +4220,15 @@ "services-jellyfin-usage": [ "services-jellyfin.html#services-jellyfin-usage" ], + "services-jellyfin-usage-applicationdashboard": [ + "services-jellyfin.html#services-jellyfin-usage-applicationdashboard" + ], "services-jellyfin-usage-backup": [ "services-jellyfin.html#services-jellyfin-usage-backup" ], + "services-jellyfin-usage-configuration": [ + "services-jellyfin.html#services-jellyfin-usage-configuration" + ], "services-karakeep": [ "services-karakeep.html#services-karakeep" ], @@ -3824,6 +4274,21 @@ "services-karakeep-options-shb.karakeep.backup.result.restoreScript": [ "services-karakeep.html#services-karakeep-options-shb.karakeep.backup.result.restoreScript" ], + "services-karakeep-options-shb.karakeep.dashboard": [ + "services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard" + ], + "services-karakeep-options-shb.karakeep.dashboard.request": [ + "services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard.request" + ], + "services-karakeep-options-shb.karakeep.dashboard.request.externalUrl": [ + "services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard.request.externalUrl" + ], + "services-karakeep-options-shb.karakeep.dashboard.request.internalUrl": [ + "services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard.request.internalUrl" + ], + "services-karakeep-options-shb.karakeep.dashboard.result": [ + "services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard.result" + ], "services-karakeep-options-shb.karakeep.domain": [ "services-karakeep.html#services-karakeep-options-shb.karakeep.domain" ], @@ -3974,6 +4439,12 @@ "services-karakeep-usage": [ "services-karakeep.html#services-karakeep-usage" ], + "services-karakeep-usage-applicationdashboard": [ + "services-karakeep.html#services-karakeep-usage-applicationdashboard" + ], + "services-karakeep-usage-configuration": [ + "services-karakeep.html#services-karakeep-usage-configuration" + ], "services-mailserver": [ "services-mailserver.html#services-mailserver" ], @@ -4583,6 +5054,21 @@ "services-nextcloudserver-options-shb.nextcloud.backup.result.restoreScript": [ "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.backup.result.restoreScript" ], + "services-nextcloudserver-options-shb.nextcloud.dashboard": [ + "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard" + ], + "services-nextcloudserver-options-shb.nextcloud.dashboard.request": [ + "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard.request" + ], + "services-nextcloudserver-options-shb.nextcloud.dashboard.request.externalUrl": [ + "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard.request.externalUrl" + ], + "services-nextcloudserver-options-shb.nextcloud.dashboard.request.internalUrl": [ + "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard.request.internalUrl" + ], + "services-nextcloudserver-options-shb.nextcloud.dashboard.result": [ + "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard.result" + ], "services-nextcloudserver-options-shb.nextcloud.dataDir": [ "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dataDir" ], @@ -4667,6 +5153,9 @@ "services-nextcloudserver-usage": [ "services-nextcloud.html#services-nextcloudserver-usage" ], + "services-nextcloudserver-usage-applicationdashboard": [ + "services-nextcloud.html#services-nextcloudserver-usage-applicationdashboard" + ], "services-nextcloudserver-usage-backup": [ "services-nextcloud.html#services-nextcloudserver-usage-backup" ], @@ -4754,6 +5243,21 @@ "services-open-webui-options-shb.open-webui.backup.result.restoreScript": [ "services-open-webui.html#services-open-webui-options-shb.open-webui.backup.result.restoreScript" ], + "services-open-webui-options-shb.open-webui.dashboard": [ + "services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard" + ], + "services-open-webui-options-shb.open-webui.dashboard.request": [ + "services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard.request" + ], + "services-open-webui-options-shb.open-webui.dashboard.request.externalUrl": [ + "services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard.request.externalUrl" + ], + "services-open-webui-options-shb.open-webui.dashboard.request.internalUrl": [ + "services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard.request.internalUrl" + ], + "services-open-webui-options-shb.open-webui.dashboard.result": [ + "services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard.result" + ], "services-open-webui-options-shb.open-webui.domain": [ "services-open-webui.html#services-open-webui-options-shb.open-webui.domain" ], @@ -4859,12 +5363,21 @@ "services-open-webui-usage": [ "services-open-webui.html#services-open-webui-usage" ], + "services-open-webui-usage-applicationdashboard": [ + "services-open-webui.html#services-open-webui-usage-applicationdashboard" + ], "services-open-webui-usage-backup": [ "services-open-webui.html#services-open-webui-usage-backup" ], + "services-open-webui-usage-configuration": [ + "services-open-webui.html#services-open-webui-usage-configuration" + ], "services-pinchflat": [ "services-pinchflat.html#services-pinchflat" ], + "services-pinchflat-features": [ + "services-pinchflat.html#services-pinchflat-features" + ], "services-pinchflat-options": [ "services-pinchflat.html#services-pinchflat-options" ], @@ -4901,6 +5414,21 @@ "services-pinchflat-options-shb.pinchflat.backup.result.restoreScript": [ "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.result.restoreScript" ], + "services-pinchflat-options-shb.pinchflat.dashboard": [ + "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard" + ], + "services-pinchflat-options-shb.pinchflat.dashboard.request": [ + "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard.request" + ], + "services-pinchflat-options-shb.pinchflat.dashboard.request.externalUrl": [ + "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard.request.externalUrl" + ], + "services-pinchflat-options-shb.pinchflat.dashboard.request.internalUrl": [ + "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard.request.internalUrl" + ], + "services-pinchflat-options-shb.pinchflat.dashboard.result": [ + "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard.result" + ], "services-pinchflat-options-shb.pinchflat.domain": [ "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.domain" ], @@ -4982,9 +5510,15 @@ "services-pinchflat-usage": [ "services-pinchflat.html#services-pinchflat-usage" ], + "services-pinchflat-usage-applicationdashboard": [ + "services-pinchflat.html#services-pinchflat-usage-applicationdashboard" + ], "services-pinchflat-usage-backup": [ "services-pinchflat.html#services-pinchflat-usage-backup" ], + "services-pinchflat-usage-configuration": [ + "services-pinchflat.html#services-pinchflat-usage-configuration" + ], "services-vaultwarden": [ "services-vaultwarden.html#services-vaultwarden" ], @@ -5039,6 +5573,21 @@ "services-vaultwarden-options-shb.vaultwarden.backup.result.restoreScript": [ "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.backup.result.restoreScript" ], + "services-vaultwarden-options-shb.vaultwarden.dashboard": [ + "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard" + ], + "services-vaultwarden-options-shb.vaultwarden.dashboard.request": [ + "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard.request" + ], + "services-vaultwarden-options-shb.vaultwarden.dashboard.request.externalUrl": [ + "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard.request.externalUrl" + ], + "services-vaultwarden-options-shb.vaultwarden.dashboard.request.internalUrl": [ + "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard.request.internalUrl" + ], + "services-vaultwarden-options-shb.vaultwarden.dashboard.result": [ + "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard.result" + ], "services-vaultwarden-options-shb.vaultwarden.databasePassword": [ "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.databasePassword" ], @@ -5150,6 +5699,9 @@ "services-vaultwarden-usage": [ "services-vaultwarden.html#services-vaultwarden-usage" ], + "services-vaultwarden-usage-applicationdashboard": [ + "services-vaultwarden.html#services-vaultwarden-usage-applicationdashboard" + ], "services-vaultwarden-usage-configuration": [ "services-vaultwarden.html#services-vaultwarden-usage-configuration" ], diff --git a/docs/services.md b/docs/services.md index c4e6481..8dbf8f5 100644 --- a/docs/services.md +++ b/docs/services.md @@ -1,9 +1,9 @@ # Services {#services} -Services are usually web applications that SHB help you self-host. +Services are usually web applications that SHB help you self-host some of your data. Configuration of those is purposely made more opinionated than the upstream nixpkgs modules -in exchange for requiring less options to define. +in exchange for an uniformized configuration experience. That is possible thanks to the extensive use of blocks provided by SHB. ::: {.note} @@ -19,6 +19,7 @@ information is provided in the respective manual sections. | [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 | +| [Homepage][] | Y (1) | Y | N | Y | Y (2) | N | | [Jellyfin][] | Y (1) | Y | Y | Y | Y (2) | N | | [Karakeep][] | Y (1) | Y | Y | Y | Y (2) | N | | [Nextcloud Server][] | Y (1) | Y | Y | Y | Y (2) | P (3) | @@ -40,6 +41,7 @@ Legend: **N**: no but WIP; **P**: partial; **Y**: yes [Firefly-iii]: services-firefly-iii.html [Forgejo]: services-forgejo.html [Home-Assistant]: services-home-assistant.html +[Homepage]: services-homepage.html [Jellyfin]: services-jellyfin.html [Karakeep]: services-karakeep.html [Nextcloud Server]: services-nextcloud.html @@ -48,6 +50,12 @@ Legend: **N**: no but WIP; **P**: partial; **Y**: yes [Simple NixOS Mailserver]: services-mailserver.html [Vaultwarden]: services-vaultwarden.html +## Dashboard {#services-category-dashboard} + +```{=include=} chapters html:into-file=//services-homepage.html +modules/services/homepage/docs/default.md +``` + ## Documents {#services-category-documents} ```{=include=} chapters html:into-file=//services-nextcloud.html diff --git a/flake.nix b/flake.nix index ec8cc6c..7ad9110 100644 --- a/flake.nix +++ b/flake.nix @@ -66,6 +66,9 @@ # The contract dummies are used to show options for contracts. contractDummyModules = [ modules/contracts/backup/dummyModule.nix + modules/contracts/dashboard/dummyModule.nix + modules/contracts/databasebackup/dummyModule.nix + modules/contracts/secret/dummyModule.nix modules/contracts/ssl/dummyModule.nix ]; in @@ -110,6 +113,7 @@ (pkgs.path + "/nixos/modules/services/misc/forgejo.nix") ]; "services/home-assistant" = ./modules/services/home-assistant.nix; + "services/homepage" = ./modules/services/homepage.nix; "services/jellyfin" = ./modules/services/jellyfin.nix; "services/karakeep" = ./modules/services/karakeep.nix; "services/mailserver" = ./modules/services/mailserver.nix; @@ -131,6 +135,14 @@ "backup" ]; }; + "contracts/dashboard" = { + module = ./modules/contracts/dashboard/dummyModule.nix; + optionRoot = [ + "shb" + "contracts" + "dashboard" + ]; + }; "contracts/databasebackup" = { module = ./modules/contracts/databasebackup/dummyModule.nix; optionRoot = [ @@ -298,6 +310,7 @@ // (vm_test "hledger" ./test/services/hledger.nix) // (vm_test "immich" ./test/services/immich.nix) // (vm_test "homeassistant" ./test/services/home-assistant.nix) + // (vm_test "homepage" ./test/services/homepage.nix) // (vm_test "jellyfin" ./test/services/jellyfin.nix) // (vm_test "karakeep" ./test/services/karakeep.nix) // (vm_test "nextcloud" ./test/services/nextcloud.nix) @@ -407,6 +420,7 @@ self.nixosModules.hledger self.nixosModules.immich self.nixosModules.home-assistant + self.nixosModules.homepage self.nixosModules.jellyfin self.nixosModules.karakeep self.nixosModules.mailserver @@ -445,6 +459,7 @@ nixosModules.hledger = modules/services/hledger.nix; nixosModules.immich = modules/services/immich.nix; nixosModules.home-assistant = modules/services/home-assistant.nix; + nixosModules.homepage = modules/services/homepage.nix; nixosModules.jellyfin = modules/services/jellyfin.nix; nixosModules.karakeep = modules/services/karakeep.nix; nixosModules.mailserver = modules/services/mailserver.nix; diff --git a/lib/default.nix b/lib/default.nix index 130856e..293f9e1 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -2,411 +2,420 @@ let inherit (builtins) isAttrs hasAttr; inherit (lib) any concatMapStringsSep concatStringsSep; -in -rec { - # Replace secrets in a file. - # - userConfig is an attrset that will produce a config file. - # - resultPath is the location the config file should have on the filesystem. - # - generator is a function taking two arguments name and value and returning path in the nix - # nix store where the - replaceSecrets = - { - userConfig, - resultPath, - generator, - user ? null, - permissions ? "u=r,g=r,o=", - }: - let - configWithTemplates = withReplacements userConfig; + shb = rec { + # Replace secrets in a file. + # - userConfig is an attrset that will produce a config file. + # - resultPath is the location the config file should have on the filesystem. + # - generator is a function taking two arguments name and value and returning path in the nix + # nix store where the + replaceSecrets = + { + userConfig, + resultPath, + generator, + user ? null, + permissions ? "u=r,g=r,o=", + }: + let + configWithTemplates = withReplacements userConfig; - nonSecretConfigFile = generator "template" configWithTemplates; + nonSecretConfigFile = generator "template" configWithTemplates; - replacements = getReplacements userConfig; - in - replaceSecretsScript { - file = nonSecretConfigFile; - inherit resultPath replacements; - inherit user permissions; - }; - - replaceSecretsFormatAdapter = format: format.generate; - replaceSecretsGeneratorAdapter = - generator: name: value: - pkgs.writeText "generator " (generator value); - toEnvVar = replaceSecretsGeneratorAdapter ( - v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; }) - ); - - template = - file: newPath: replacements: - replaceSecretsScript { - inherit file replacements; - resultPath = newPath; - }; - - genReplacement = - secret: - let - t = - { - transform ? null, - ... - }: - if isNull transform then x: x else transform; - in - lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})"); - - replaceSecretsScript = - { - file, - resultPath, - replacements, - user ? null, - permissions ? "u=r,g=r,o=", - }: - let - templatePath = resultPath + ".template"; - - # We check that the files containing the secrets have the - # correct permissions for us to read them in this separate - # step. Otherwise, the $(cat ...) commands inside the sed - # replacements could fail but not fail individually but - # not fail the whole script. - checkPermissions = concatMapStringsSep "\n" ( - pattern: "cat ${pattern.source} > /dev/null" - ) replacements; - - sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") ( - map genReplacement replacements - ); - - sedCmd = if replacements == [ ] then "cat" else "${pkgs.gnused}/bin/sed ${sedPatterns}"; - in - '' - set -euo pipefail - - ${checkPermissions} - - mkdir -p $(dirname ${templatePath}) - ln -fs ${file} ${templatePath} - rm -f ${resultPath} - touch ${resultPath} - '' - + (lib.optionalString (user != null) '' - chown ${user} ${resultPath} - '') - + '' - ${sedCmd} ${templatePath} > ${resultPath} - chmod ${permissions} ${resultPath} - ''; - - secretFileType = lib.types.submodule { - options = { - source = lib.mkOption { - type = lib.types.path; - description = "File containing the value."; + replacements = getReplacements userConfig; + in + replaceSecretsScript { + file = nonSecretConfigFile; + inherit resultPath replacements; + inherit user permissions; }; - transform = lib.mkOption { - type = lib.types.raw; - description = "An optional function to transform the secret."; - default = null; - example = lib.literalExpression '' - v: "prefix-$${v}-suffix" - ''; + replaceSecretsFormatAdapter = format: format.generate; + replaceSecretsGeneratorAdapter = + generator: name: value: + pkgs.writeText "generator " (generator value); + toEnvVar = replaceSecretsGeneratorAdapter ( + v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; }) + ); + + template = + file: newPath: replacements: + replaceSecretsScript { + inherit file replacements; + resultPath = newPath; + }; + + genReplacement = + secret: + let + t = + { + transform ? null, + ... + }: + if isNull transform then x: x else transform; + in + lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})"); + + replaceSecretsScript = + { + file, + resultPath, + replacements, + user ? null, + permissions ? "u=r,g=r,o=", + }: + let + templatePath = resultPath + ".template"; + + # We check that the files containing the secrets have the + # correct permissions for us to read them in this separate + # step. Otherwise, the $(cat ...) commands inside the sed + # replacements could fail but not fail individually but + # not fail the whole script. + checkPermissions = concatMapStringsSep "\n" ( + pattern: "cat ${pattern.source} > /dev/null" + ) replacements; + + sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") ( + map genReplacement replacements + ); + + sedCmd = if replacements == [ ] then "cat" else "${pkgs.gnused}/bin/sed ${sedPatterns}"; + in + '' + set -euo pipefail + + ${checkPermissions} + + mkdir -p $(dirname ${templatePath}) + ln -fs ${file} ${templatePath} + rm -f ${resultPath} + touch ${resultPath} + '' + + (lib.optionalString (user != null) '' + chown ${user} ${resultPath} + '') + + '' + ${sedCmd} ${templatePath} > ${resultPath} + chmod ${permissions} ${resultPath} + ''; + + secretFileType = lib.types.submodule { + options = { + source = lib.mkOption { + type = lib.types.path; + description = "File containing the value."; + }; + + transform = lib.mkOption { + type = lib.types.raw; + description = "An optional function to transform the secret."; + default = null; + example = lib.literalExpression '' + v: "prefix-$${v}-suffix" + ''; + }; }; }; - }; - secretName = - names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%"; + secretName = + names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%"; - withReplacements = - attrs: - let - valueOrReplacement = - name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name; - in - mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs; + withReplacements = + attrs: + let + valueOrReplacement = + name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name; + in + mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs; - getReplacements = - attrs: - let - addNameField = - name: value: - if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; }; + getReplacements = + attrs: + let + addNameField = + name: value: + if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; }; - secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs; - in - collect (v: builtins.isAttrs v && v ? "source") secretsWithName; + secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs; + in + collect (v: builtins.isAttrs v && v ? "source") secretsWithName; - # Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists. - mapAttrsRecursiveCond = - # A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set. - cond: - # A function, given a list of attribute names and a value, returns a new value. - f: - # Attribute set or list to recursively map over. - set: - let - recurse = - path: val: - if builtins.isAttrs val && cond val then - lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val - else if builtins.isList val && cond val then - lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val - else - f path val; - in - recurse [ ] set; + # Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists. + mapAttrsRecursiveCond = + # A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set. + cond: + # A function, given a list of attribute names and a value, returns a new value. + f: + # Attribute set or list to recursively map over. + set: + let + recurse = + path: val: + if builtins.isAttrs val && cond val then + lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val + else if builtins.isList val && cond val then + lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val + else + f path val; + in + recurse [ ] set; - # Like lib.attrsets.collect but also recurses on lists. - collect = - # Given an attribute's value, determine if recursion should stop. - pred: - # The attribute set to recursively collect. - attrs: - if pred attrs then - [ attrs ] - else if builtins.isAttrs attrs then - lib.lists.concatMap (collect pred) (lib.attrsets.attrValues attrs) - else if builtins.isList attrs then - lib.lists.concatMap (collect pred) attrs - else - [ ]; + # Like lib.attrsets.collect but also recurses on lists. + collect = + # Given an attribute's value, determine if recursion should stop. + pred: + # The attribute set to recursively collect. + attrs: + if pred attrs then + [ attrs ] + else if builtins.isAttrs attrs then + lib.lists.concatMap (collect pred) (lib.attrsets.attrValues attrs) + else if builtins.isList attrs then + lib.lists.concatMap (collect pred) attrs + else + [ ]; - indent = - i: str: - lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str); + indent = + i: str: + lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str); - # Generator for XML - formatXML = - { - enclosingRoot ? null, - }: - { - type = - with lib.types; - let - valueType = - nullOr (oneOf [ - bool - int - float - str - path - (attrsOf valueType) - (listOf valueType) - ]) - // { - description = "XML value"; - }; - in - valueType; + # Generator for XML + formatXML = + { + enclosingRoot ? null, + }: + { + type = + with lib.types; + let + valueType = + nullOr (oneOf [ + bool + int + float + str + path + (attrsOf valueType) + (listOf valueType) + ]) + // { + description = "XML value"; + }; + in + valueType; - generate = - name: value: - pkgs.callPackage ( + generate = + name: value: + pkgs.callPackage ( + { runCommand, python3 }: + runCommand "config" + { + value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; }); + passAsFile = [ "value" ]; + } + ( + pkgs.writers.writePython3 "dict2xml" + { + libraries = with python3.pkgs; [ + python + dict2xml + ]; + } + '' + import os + import json + from dict2xml import dict2xml + + with open(os.environ["valuePath"]) as f: + content = json.loads(f.read()) + if content is None: + print("Could not parse env var valuePath as json") + os.exit(2) + with open(os.environ["out"], "w") as out: + out.write(dict2xml(content)) + '' + ) + ) { }; + + }; + + parseXML = + xml: + let + xmlToJsonFile = pkgs.callPackage ( { runCommand, python3 }: runCommand "config" { - value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; }); - passAsFile = [ "value" ]; + inherit xml; + passAsFile = [ "xml" ]; } ( - pkgs.writers.writePython3 "dict2xml" + pkgs.writers.writePython3 "xml2json" { - libraries = with python3.pkgs; [ - python - dict2xml - ]; + libraries = with python3.pkgs; [ python ]; } '' import os import json - from dict2xml import dict2xml + from collections import ChainMap + from xml.etree import ElementTree + + + def xml_to_dict_recursive(root): + all_descendants = list(root) + if len(all_descendants) == 0: + return {root.tag: root.text} + else: + merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants)) + return {root.tag: dict(merged_dict)} + + + with open(os.environ["xmlPath"]) as f: + root = ElementTree.XML(f.read()) + xml = xml_to_dict_recursive(root) + j = json.dumps(xml) - with open(os.environ["valuePath"]) as f: - content = json.loads(f.read()) - if content is None: - print("Could not parse env var valuePath as json") - os.exit(2) with open(os.environ["out"], "w") as out: - out.write(dict2xml(content)) + out.write(j) '' ) ) { }; + in + builtins.fromJSON (builtins.readFile xmlToJsonFile); - }; + renameAttrName = + attrset: from: to: + (lib.attrsets.filterAttrs (name: v: name == from) attrset) + // { + ${to} = attrset.${from}; + }; - parseXML = - xml: - let - xmlToJsonFile = pkgs.callPackage ( - { runCommand, python3 }: - runCommand "config" + # Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix + # with a nicer diff display function. + check = + { pkgs, tests }: + let + formatValue = + val: + if (builtins.isList val || builtins.isAttrs val) then + builtins.toJSON val + else + builtins.toString val; + + resultToString = { - inherit xml; - passAsFile = [ "xml" ]; - } - ( - pkgs.writers.writePython3 "xml2json" + name, + expected, + result, + }: + builtins.readFile ( + pkgs.runCommand "nix-flake-tests-error" { - libraries = with python3.pkgs; [ python ]; + expected = formatValue expected; + result = formatValue result; + passAsFile = [ + "expected" + "result" + ]; } '' - import os - import json - from collections import ChainMap - from xml.etree import ElementTree - - - def xml_to_dict_recursive(root): - all_descendants = list(root) - if len(all_descendants) == 0: - return {root.tag: root.text} - else: - merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants)) - return {root.tag: dict(merged_dict)} - - - with open(os.environ["xmlPath"]) as f: - root = ElementTree.XML(f.read()) - xml = xml_to_dict_recursive(root) - j = json.dumps(xml) - - with open(os.environ["out"], "w") as out: - out.write(j) + echo "${name} failed (- expected, + result)" > $out + cp ''${expectedPath} ''${expectedPath}.json + cp ''${resultPath} ''${resultPath}.json + ${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out '' - ) + ); + + results = pkgs.lib.runTests tests; + in + if results != [ ] then + builtins.throw (concatStringsSep "\n" (map resultToString (lib.traceValSeq results))) + else + pkgs.runCommand "nix-flake-tests-success" { } "echo > $out"; + + genConfigOutOfBandSystemd = + { + config, + configLocation, + generator, + user ? null, + permissions ? "u=r,g=r,o=", + }: + { + loadCredentials = getLoadCredentials "source" config; + preStart = lib.mkBefore (replaceSecrets { + userConfig = updateToLoadCredentials "source" "$CREDENTIALS_DIRECTORY" config; + resultPath = configLocation; + inherit generator; + inherit user permissions; + }); + }; + + updateToLoadCredentials = + sourceField: rootDir: attrs: + let + hasPlaceholderField = v: isAttrs v && hasAttr sourceField v; + + valueOrLoadCredential = + path: value: + if !(hasPlaceholderField value) then + value + else + value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; }; + in + mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs; + + getLoadCredentials = + sourceField: attrs: + let + hasPlaceholderField = v: isAttrs v && hasAttr sourceField v; + + addPathField = + path: value: if !(hasPlaceholderField value) then value else value // { inherit path; }; + + secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs; + + allSecrets = collect (v: hasPlaceholderField v) secretsWithPath; + + genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}"; + in + map genLoadCredentials allSecrets; + + anyNotNull = any (x: x != null); + + mkJellyfinPlugin = + { + pname, + version, + hash, + url, + }: + pkgs.callPackage ( + { stdenv, fetchzip }: + stdenv.mkDerivation (finalAttrs: { + inherit pname version; + + src = fetchzip { + inherit url hash; + stripRoot = false; + }; + + dontBuild = true; + + installPhase = '' + mkdir $out + cp -r . $out + ''; + }) ) { }; - in - builtins.fromJSON (builtins.readFile xmlToJsonFile); - renameAttrName = - attrset: from: to: - (lib.attrsets.filterAttrs (name: v: name == from) attrset) - // { - ${to} = attrset.${from}; - }; + update = + attr: fn: attrset: + attrset // { ${attr} = fn attrset.${attr}; }; - # Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix - # with a nicer diff display function. - check = - { pkgs, tests }: - let - formatValue = - val: - if (builtins.isList val || builtins.isAttrs val) then - builtins.toJSON val - else - builtins.toString val; - - resultToString = - { - name, - expected, - result, - }: - builtins.readFile ( - pkgs.runCommand "nix-flake-tests-error" - { - expected = formatValue expected; - result = formatValue result; - passAsFile = [ - "expected" - "result" - ]; - } - '' - echo "${name} failed (- expected, + result)" > $out - cp ''${expectedPath} ''${expectedPath}.json - cp ''${resultPath} ''${resultPath}.json - ${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out - '' - ); - - results = pkgs.lib.runTests tests; - in - if results != [ ] then - builtins.throw (concatStringsSep "\n" (map resultToString (lib.traceValSeq results))) - else - pkgs.runCommand "nix-flake-tests-success" { } "echo > $out"; - - genConfigOutOfBandSystemd = - { - config, - configLocation, - generator, - user ? null, - permissions ? "u=r,g=r,o=", - }: - { - loadCredentials = getLoadCredentials "source" config; - preStart = lib.mkBefore (replaceSecrets { - userConfig = updateToLoadCredentials "source" "$CREDENTIALS_DIRECTORY" config; - resultPath = configLocation; - inherit generator; - inherit user permissions; - }); - }; - - updateToLoadCredentials = - sourceField: rootDir: attrs: - let - hasPlaceholderField = v: isAttrs v && hasAttr sourceField v; - - valueOrLoadCredential = - path: value: - if !(hasPlaceholderField value) then - value - else - value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; }; - in - mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs; - - getLoadCredentials = - sourceField: attrs: - let - hasPlaceholderField = v: isAttrs v && hasAttr sourceField v; - - addPathField = - path: value: if !(hasPlaceholderField value) then value else value // { inherit path; }; - - secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs; - - allSecrets = collect (v: hasPlaceholderField v) secretsWithPath; - - genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}"; - in - map genLoadCredentials allSecrets; - - anyNotNull = any (x: x != null); - - mkJellyfinPlugin = - { - pname, - version, - hash, - url, - }: - pkgs.callPackage ( - { stdenv, fetchzip }: - stdenv.mkDerivation (finalAttrs: { - inherit pname version; - - src = fetchzip { - inherit url hash; - stripRoot = false; - }; - - dontBuild = true; - - installPhase = '' - mkdir $out - cp -r . $out - ''; - }) - ) { }; + }; +in +shb +// { + homepage = pkgs.callPackage ./homepage.nix { inherit shb; }; } diff --git a/lib/homepage.nix b/lib/homepage.nix new file mode 100644 index 0000000..0795be2 --- /dev/null +++ b/lib/homepage.nix @@ -0,0 +1,92 @@ +{ lib, shb }: +let + sort = + attr: vs: + map (v: { ${v.name} = v.${attr}; }) ( + lib.sortOn (v: v.sortOrder) (lib.mapAttrsToList (n: v: v // { name = n; }) vs) + ); + + slufigy = builtins.replaceStrings [ "-" ] [ "_" ]; + + mkService = + groupName: serviceName: + { + request, + ... + }: + apiKey: settings: + lib.recursiveUpdate ( + { + href = request.externalUrl; + siteMonitor = if (request.internalUrl == null) then null else request.internalUrl; + icon = "sh-${lib.toLower serviceName}"; + } + // lib.optionalAttrs (apiKey != null) { + widget = { + # Duplicating because widgets call the api key various names + # and duplicating is a hacky but easy solution. + key = "{{HOMEPAGE_FILE_${slufigy groupName}_${slufigy serviceName}}}"; + password = "{{HOMEPAGE_FILE_${slufigy groupName}_${slufigy serviceName}}}"; + type = lib.toLower serviceName; + url = if (request.internalUrl != null) then request.internalUrl else request.externalUrl; + }; + } + ) settings; + + asServiceGroup = + cfg: + sort "services" ( + lib.mapAttrs ( + groupName: groupCfg: + shb.update "services" ( + services: + sort "dashboard" ( + lib.mapAttrs ( + serviceName: serviceCfg: + shb.update "dashboard" ( + dashboard: + (mkService groupName serviceName) dashboard serviceCfg.apiKey (serviceCfg.settings or { }) + ) serviceCfg + ) services + ) + ) groupCfg + ) cfg + ); + + allKeys = + cfg: + let + flat = lib.flatten ( + lib.mapAttrsToList ( + groupName: groupCfg: + lib.mapAttrsToList ( + serviceName: serviceCfg: + lib.optionalAttrs (serviceCfg.apiKey != null) { + inherit serviceName groupName; + inherit (serviceCfg.apiKey.result) path; + } + ) groupCfg.services + ) cfg + ); + + flatWithApiKey = builtins.filter (v: v != { }) flat; + in + builtins.listToAttrs ( + map ( + { + groupName, + serviceName, + path, + }: + lib.nameValuePair "${slufigy groupName}_${slufigy serviceName}" path + ) flatWithApiKey + ); +in +{ + inherit + allKeys + asServiceGroup + mkService + sort + ; +} diff --git a/modules/blocks/authelia.nix b/modules/blocks/authelia.nix index a2af7b1..ee12ed9 100644 --- a/modules/blocks/authelia.nix +++ b/modules/blocks/authelia.nix @@ -390,6 +390,20 @@ in to see exactly what Authelia receives and sends back. ''; }; + + 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.authelia.subdomain}.\${config.shb.authelia.domain}"; + internalUrl = "http://127.0.0.1:${toString listenPort}"; + }; + }; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/blocks/authelia/docs/default.md b/modules/blocks/authelia/docs/default.md index 6d26b4d..4508a24 100644 --- a/modules/blocks/authelia/docs/default.md +++ b/modules/blocks/authelia/docs/default.md @@ -12,7 +12,13 @@ as well as some extensive [troubleshooting](#blocks-authelia-troubleshooting) fe Note that forward authentication is configured with the [nginx block](blocks-nginx.html#blocks-nginx-usage-shbforwardauth). -## Global Setup {#blocks-authelia-global-setup} +## Features {#services-authelia-features} + +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-authelia-usage-applicationdashboard) + +## Usage {#services-authelia-usage} + +### Initial Configuration {#blocks-authelia-usage-configuration} Authelia cannot work without SSL and LDAP. So setting up the Authelia block requires to setup the [SSL block][] first @@ -80,6 +86,22 @@ Crucially, the `shb.authelia.secrets.ldapAdminPasswordFile` must be the same as the `shb.lldap.ldapUserPassword` defined for the [LLDAP block][]. This is done using Sops' `key` option. +### Application Dashboard {#services-authelia-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#blocks-authelia-options-shb.authelia.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Admin.services.Authelia = { + sortOrder = 2; + dashboard.request = config.shb.authelia.dashboard.request; + }; +} +``` + ## SHB OIDC integration {#blocks-authelia-shb-oidc} For services [provided by SelfHostBlocks][services] that handle [OIDC integration][OIDC], diff --git a/modules/blocks/lldap.nix b/modules/blocks/lldap.nix index 15170d5..9d66a6d 100644 --- a/modules/blocks/lldap.nix +++ b/modules/blocks/lldap.nix @@ -333,6 +333,20 @@ in type = types.bool; default = true; }; + + 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.lldap.subdomain}.\${config.shb.lldap.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.webUIListenPort}"; + }; + }; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/blocks/lldap/docs/default.md b/modules/blocks/lldap/docs/default.md index ec0c723..c0b9755 100644 --- a/modules/blocks/lldap/docs/default.md +++ b/modules/blocks/lldap/docs/default.md @@ -7,7 +7,13 @@ across services. [LLDAP]: https://github.com/lldap/lldap -## Global Setup {#blocks-lldap-global-setup} +## Features {#blocks-lldap-features} + +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#blocks-lldap-usage-applicationdashboard) + +## Usage {#blocks-lldap-usage} + +### Initial Configuration {#blocks-lldap-usage-configuration} ```nix shb.lldap = { @@ -29,7 +35,7 @@ shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPasswor This assumes secrets are setup with SOPS as mentioned in [the secrets setup section](usage.html#usage-secrets) of the manual. -## SSL {#blocks-lldap-ssl} +### SSL {#blocks-lldap-usage-ssl} Using SSL is an important security practice, like always. Using the [SSL block][], the configuration to add to the one above is: @@ -44,7 +50,7 @@ shb.certs.certs.letsencrypt.${domain}.extraDomains = [ shb.lldap.ssl = config.shb.certs.certs.letsencrypt.${config.shb.lldap.domain}; ``` -## Restrict Access By IP {#blocks-lldap-restrict-access-by-ip} +### Restrict Access By IP {#blocks-lldap-usage-restrict-access-by-ip} For added security, you can restrict access to the LLDAP UI by adding the following line: @@ -53,6 +59,22 @@ by adding the following line: shb.lldap.restrictAccessIPRange = "192.168.50.0/24"; ``` +### Application Dashboard {#blocks-lldap-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#blocks-lldap-options-shb.lldap.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Admin.services.LLDAP = { + sortOrder = 2; + dashboard.request = config.shb.lldap.dashboard.request; + }; +} +``` + ## Manage Groups {#blocks-lldap-manage-groups} The following snippet will create group named "family" if it does not exist yet. diff --git a/modules/blocks/monitoring.nix b/modules/blocks/monitoring.nix index 4aad94b..f9da03a 100644 --- a/modules/blocks/monitoring.nix +++ b/modules/blocks/monitoring.nix @@ -247,6 +247,21 @@ in }; }; }; + + 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.monitoring.subdomain}.\${config.shb.monitoring.domain}"; + internalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + internalUrlText = "https://\${config.shb.monitoring.subdomain}.\${config.shb.monitoring.domain}"; + }; + }; + }; }; config = lib.mkMerge [ diff --git a/modules/blocks/monitoring/docs/default.md b/modules/blocks/monitoring/docs/default.md index 86e325c..cbef2ab 100644 --- a/modules/blocks/monitoring/docs/default.md +++ b/modules/blocks/monitoring/docs/default.md @@ -17,9 +17,12 @@ This block sets up the monitoring stack for Self Host Blocks. It is composed of: - Registration is enabled through SSO. - Access through [subdomain](#blocks-monitoring-options-shb.monitoring.subdomain) using reverse proxy. - Access through [HTTPS](#blocks-monitoring-options-shb.monitoring.ssl) using reverse proxy. +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#blocks-monitoring-usage-applicationdashboard) ## Usage {#blocks-monitoring-usage} +### Initial Configuration {#blocks-monitoring-usage-configuration} + The following snippet assumes a few blocks have been setup already: - the [secrets block](usage.html#usage-secrets) with SOPS, @@ -110,6 +113,22 @@ You might for example want to update the metrics retention time with: services.prometheus.retentionTime = "60d"; ``` +### Application Dashboard {#blocks-monitoring-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#blocks-monitoring-options-shb.monitoring.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Admin.services.Grafana = { + sortOrder = 10; + dashboard.request = config.shb.monitoring.dashboard.request; + }; +} +``` + ## Provisioning {#blocks-monitoring-provisioning} Self Host Blocks will create automatically the following resources: diff --git a/modules/contracts/backup/docs/default.md b/modules/contracts/backup/docs/default.md index 43bcec1..9c0c1eb 100644 --- a/modules/contracts/backup/docs/default.md +++ b/modules/contracts/backup/docs/default.md @@ -33,7 +33,9 @@ Here is an example module defining such a `backup` option: { options = { myservice.backup = mkOption { - type = contracts.backup.request; + type = lib.types.submodule { + options = contracts.backup.request; + }; default = { user = "myservice"; sourceDirectories = [ diff --git a/modules/contracts/dashboard.nix b/modules/contracts/dashboard.nix new file mode 100644 index 0000000..f1d3685 --- /dev/null +++ b/modules/contracts/dashboard.nix @@ -0,0 +1,77 @@ +{ lib, ... }: +let + inherit (lib) mkOption; + inherit (lib.types) + nullOr + submodule + str + ; +in +{ + mkRequest = + { + serviceName ? "", + externalUrl ? "", + externalUrlText ? null, + internalUrl ? null, + internalUrlText ? null, + apiKey ? null, + }: + mkOption { + description = '' + Request part of the dashboard contract. + ''; + default = { }; + type = submodule { + options = { + externalUrl = + mkOption { + description = '' + URL at which the service can be accessed. + + This URL should go through the reverse proxy. + ''; + type = str; + default = externalUrl; + example = "https://jellyfin.example.com"; + } + // (lib.optionalAttrs (externalUrlText != null) { + defaultText = externalUrlText; + }); + + internalUrl = + mkOption { + description = '' + URL at which the service can be accessed directly. + + This URL should bypass the reverse proxy. + It can be used for example to ping the service + and making sure it is up and running correctly. + ''; + type = nullOr str; + default = internalUrl; + example = "http://127.0.0.1:8081"; + } + // (lib.optionalAttrs (internalUrlText != null) { + defaultText = internalUrlText; + }); + }; + }; + }; + + mkResult = + { + }: + mkOption { + description = '' + Result part of the dashboard contract. + + No option is provided here. + ''; + default = { }; + type = submodule { + options = { + }; + }; + }; +} diff --git a/modules/contracts/dashboard/docs/default.md b/modules/contracts/dashboard/docs/default.md new file mode 100644 index 0000000..a4d9528 --- /dev/null +++ b/modules/contracts/dashboard/docs/default.md @@ -0,0 +1,65 @@ +# Dashboard Contract {#contract-dashboard} + +This NixOS contract is used for user-facing services +that want to be displayed on a dashboard. + +It is a contract between a service that can be accessed through an URL +and a service that wants to show a list of those services. + +## Providers {#contract-dashboard-providers} + +The providers of this contract in SHB are: + + + +- The homepage service under its [shb.homepage.servicesGroups..services..dashboard](#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard) option. + +## Usage {#contracts-dashboard-usage} + +A service that can be shown on a dashboard will provide a `dashboard` option. + +Here is an example module defining such a requester option for this dashboard contract: + +```nix +{ + options = { + myservice.dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${config.myservice.subdomain}.${config.myservice.domain}"; + internalUrl = "http://127.0.0.1:${config.myservice.port}"; + }; + }; + }; + }; +}; +``` + +Then, plug both consumer and provider together in the `config`: + +```nix +{ + config = { + = { + dashboard.request = config.myservice.dashboard.request; + }; + }; +} +``` + +And that's it for the contract part. +For more specific details on each provider, go to their respective manual pages. + +## Contract Reference {#contract-dashboard-options} + +These are all the options that are expected to exist for this contract to be respected. + +```{=include=} options +id-prefix: contracts-dashboard-options- +list-id: selfhostblocks-options +source: @OPTIONS_JSON@ +``` diff --git a/modules/contracts/dashboard/dummyModule.nix b/modules/contracts/dashboard/dummyModule.nix new file mode 100644 index 0000000..691ab60 --- /dev/null +++ b/modules/contracts/dashboard/dummyModule.nix @@ -0,0 +1,30 @@ +{ lib, shb, ... }: +let + inherit (lib) mkOption; + inherit (lib.types) submodule; +in +{ + imports = [ + ../../../lib/module.nix + ]; + + options.shb.contracts.dashboard = mkOption { + description = '' + Contract for user-facing services that want to + be displayed on a dashboard. + + The requester communicates to the provider + how to access the service + through the `request` options. + + The provider reads from the `request` options + and configures what is necessary on its side + to show the service and check its availability. + It does not communicate back to the requester. + ''; + + type = submodule { + options = shb.contracts.dashboard.contract; + }; + }; +} diff --git a/modules/contracts/default.nix b/modules/contracts/default.nix index e9964a8..0f90360 100644 --- a/modules/contracts/default.nix +++ b/modules/contracts/default.nix @@ -48,21 +48,28 @@ let importContract = module: let - importedModule = pkgs.callPackage module { inherit shb; }; + importedModule = pkgs.callPackage module { + shb = shb // { + inherit contracts; + }; + }; in mkContractFunctions { inherit (importedModule) mkRequest mkResult; }; -in -{ - databasebackup = importContract ./databasebackup.nix; - backup = importContract ./backup.nix; - mount = pkgs.callPackage ./mount.nix { }; - secret = importContract ./secret.nix; - ssl = pkgs.callPackage ./ssl.nix { }; - test = { - secret = pkgs.callPackage ./secret/test.nix { inherit shb; }; - databasebackup = pkgs.callPackage ./databasebackup/test.nix { inherit shb; }; - backup = pkgs.callPackage ./backup/test.nix { inherit shb; }; + + contracts = { + databasebackup = importContract ./databasebackup.nix; + dashboard = importContract ./dashboard.nix; + backup = importContract ./backup.nix; + mount = pkgs.callPackage ./mount.nix { }; + secret = importContract ./secret.nix; + ssl = pkgs.callPackage ./ssl.nix { }; + test = { + secret = pkgs.callPackage ./secret/test.nix { inherit shb; }; + databasebackup = pkgs.callPackage ./databasebackup/test.nix { inherit shb; }; + backup = pkgs.callPackage ./backup/test.nix { inherit shb; }; + }; }; -} +in +contracts diff --git a/modules/services/arr.nix b/modules/services/arr.nix index c7bc938..3d152da 100644 --- a/modules/services/arr.nix +++ b/modules/services/arr.nix @@ -392,6 +392,20 @@ let }; }; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.${name}.subdomain}.${cfg.${name}.domain}"; + externalUrlText = "https://\${config.shb.arr.${name}.subdomain}.\${config.shb.arr.${name}.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.${name}.settings.Port}"; + }; + }; + }; } // (c.moreOptions or { }); }; diff --git a/modules/services/arr/docs/default.md b/modules/services/arr/docs/default.md index a9b441d..5668cf6 100644 --- a/modules/services/arr/docs/default.md +++ b/modules/services/arr/docs/default.md @@ -3,12 +3,242 @@ Defined in [`/modules/services/arr.nix`](@REPO@/modules/services/arr.nix). This NixOS module sets up multiple [Servarr](https://wiki.servarr.com/) services. +## Features {#services-arr-features} Compared to the stock module from nixpkgs, this one sets up, in a fully declarative manner LDAP and SSO integration as well as the API key. -This manual page is under construction. +## Usage {#services-arr-usage} + +### Initial Configuration {#services-arr-usage-configuration} + +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.certs.certs.letsencrypt.${domain}.extraDomains = [ + "moviesdl.${domain}" + "seriesdl.${domain}" + "subtitlesdl.${domain}" + "booksdl.${domain}" + "musicdl.${domain}" + "indexer.${domain}" + ]; + + shb.arr = { + radarr = { + inherit domain; + enable = true; + ssl = config.shb.certs.certs.letsencrypt.${domain}; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + sonarr = { + inherit domain; + enable = true; + ssl = config.shb.certs.certs.letsencrypt."${domain}"; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + bazarr = { + inherit domain; + enable = true; + ssl = config.shb.certs.certs.letsencrypt."${domain}"; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + readarr = { + inherit domain; + enable = true; + ssl = config.shb.certs.certs.letsencrypt."${domain}"; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + lidarr = { + inherit domain; + enable = true; + ssl = config.shb.certs.certs.letsencrypt."${domain}"; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + jackett = { + inherit domain; + enable = true; + ssl = config.shb.certs.certs.letsencrypt."${domain}"; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + }; +} +``` + +The user and admin LDAP groups are created automatically. + +### API Keys {#services-arr-usage-apikeys} + +The API keys for each arr service can be created declaratively. + +First, generate one secret for each service with `nix run nixpkgs#openssl -- rand -hex 64` +and store it in your secrets file (for example the SOPS file). + +Then, add the API key to each service: + +```nix +{ + shb.arr = { + radarr = { + settings = { + ApiKey.source = config.shb.sops.secret."radarr/apikey".result.path; + }; + }; + sonarr = { + settings = { + ApiKey.source = config.shb.sops.secret."sonarr/apikey".result.path; + }; + }; + bazarr = { + settings = { + ApiKey.source = config.shb.sops.secret."bazarr/apikey".result.path; + }; + }; + readarr = { + settings = { + ApiKey.source = config.shb.sops.secret."readarr/apikey".result.path; + }; + }; + lidarr = { + settings = { + ApiKey.source = config.shb.sops.secret."lidarr/apikey".result.path; + }; + }; + jackett = { + settings = { + ApiKey.source = config.shb.sops.secret."jackett/apikey".result.path; + }; + }; + }; + + shb.sops.secret."radarr/apikey".request = { + mode = "0440"; + owner = "radarr"; + group = "radarr"; + restartUnits = [ "radarr.service" ]; + }; + shb.sops.secret."sonarr/apikey".request = { + mode = "0440"; + owner = "sonarr"; + group = "sonarr"; + restartUnits = [ "sonarr.service" ]; + }; + shb.sops.secret."bazarr/apikey".request = { + mode = "0440"; + owner = "bazarr"; + group = "bazarr"; + restartUnits = [ "bazarr.service" ]; + }; + shb.sops.secret."readarr/apikey".request = { + mode = "0440"; + owner = "readarr"; + group = "readarr"; + restartUnits = [ "readarr.service" ]; + }; + shb.sops.secret."lidarr/apikey".request = { + mode = "0440"; + owner = "lidarr"; + group = "lidarr"; + restartUnits = [ "lidarr.service" ]; + }; + shb.sops.secret."jackett/apikey".request = { + mode = "0440"; + owner = "jackett"; + group = "jackett"; + restartUnits = [ "jackett.service" ]; + }; +} +``` + +### Application Dashboard {#services-arr-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the various dashboard options. + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Radarr = { + sortOrder = 10; + dashboard.request = config.shb.arr.radarr.dashboard.request; + apiKey.result = config.shb.sops.secret."radarr/homepageApiKey".result; + }; + shb.sops.secret."radarr/homepageApiKey" = { + settings.key = "radarr/apikey"; + request = config.shb.homepage.servicesGroups.Media.services.Radarr.apiKey.request; + }; + shb.homepage.servicesGroups.Media.services.Sonarr = { + sortOrder = 11; + dashboard.request = config.shb.arr.sonarr.dashboard.request; + apiKey.result = config.shb.sops.secret."sonarr/homepageApiKey".result; + }; + shb.sops.secret."sonarr/homepageApiKey" = { + settings.key = "sonarr/apikey"; + request = config.shb.homepage.servicesGroups.Media.services.Sonarr.apiKey.request; + }; + shb.homepage.servicesGroups.Media.services.Bazarr = { + sortOrder = 12; + dashboard.request = config.shb.arr.bazarr.dashboard.request; + apiKey.result = config.shb.sops.secret."bazarr/homepageApiKey".result; + }; + shb.sops.secret."bazarr/homepageApiKey" = { + settings.key = "bazarr/apikey"; + request = config.shb.homepage.servicesGroups.Media.services.Bazarr.apiKey.request; + }; + shb.homepage.servicesGroups.Media.services.Readarr = { + sortOrder = 13; + dashboard.request = config.shb.arr.readarr.dashboard.request; + apiKey.result = config.shb.sops.secret."readarr/homepageApiKey".result; + }; + shb.sops.secret."readarr/homepageApiKey" = { + settings.key = "readarr/apikey"; + request = config.shb.homepage.servicesGroups.Media.services.Readarr.apiKey.request; + }; + shb.homepage.servicesGroups.Media.services.Lidarr = { + sortOrder = 14; + dashboard.request = config.shb.arr.lidarr.dashboard.request; + apiKey.result = config.shb.sops.secret."lidarr/homepageApiKey".result; + }; + shb.sops.secret."lidarr/homepageApiKey" = { + settings.key = "lidarr/apikey"; + request = config.shb.homepage.servicesGroups.Media.services.Lidarr.apiKey.request; + }; + shb.homepage.servicesGroups.Media.services.Jackett = { + sortOrder = 15; + dashboard.request = config.shb.arr.jackett.dashboard.request; + apiKey.result = config.shb.sops.secret."jackett/homepageApiKey".result; + }; + shb.sops.secret."jackett/homepageApiKey" = { + settings.key = "jackett/apikey"; + request = config.shb.homepage.servicesGroups.Media.services.Jackett.apiKey.request; + }; +} +``` + +This example reuses the API keys generated declaratively from the previous section. + +### Jackett Proxy {#services-arr-usage-jackett-proxy} + +The Jackett service can be made to use a proxy with: + +```nix +{ + shb.arr.jackett = { + settings = { + ProxyType = "0"; + ProxyUrl = "127.0.0.1:1234"; + }; + }; +}; +``` ## Options Reference {#services-arr-options} diff --git a/modules/services/audiobookshelf.nix b/modules/services/audiobookshelf.nix index bcea2af..d64a6f8 100644 --- a/modules/services/audiobookshelf.nix +++ b/modules/services/audiobookshelf.nix @@ -152,6 +152,20 @@ in default = false; example = true; }; + + 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.audiobookshelf.subdomain}.\${config.shb.audiobookshelf.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.webPort}"; + }; + }; + }; }; config = lib.mkIf cfg.enable ( diff --git a/modules/services/deluge.nix b/modules/services/deluge.nix index 53f99af..f4c3ff4 100644 --- a/modules/services/deluge.nix +++ b/modules/services/deluge.nix @@ -36,7 +36,7 @@ in options.shb.deluge = { enable = lib.mkEnableOption "the SHB Deluge service"; - enableDashboard = lib.mkEnableOption "the Torrents SHB dashboard" // { + enableDashboard = lib.mkEnableOption "the Torrents SHB monitoring dashboard" // { default = true; }; @@ -301,6 +301,20 @@ in default = null; example = "info"; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${fqdn}"; + externalUrlText = "https://\${config.shb.deluge.subdomain}.\${config.shb.deluge.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.webPort}"; + }; + }; + }; }; config = lib.mkIf cfg.enable ( diff --git a/modules/services/firefly-iii.nix b/modules/services/firefly-iii.nix index 45a1122..8a1b2ba 100644 --- a/modules/services/firefly-iii.nix +++ b/modules/services/firefly-iii.nix @@ -10,6 +10,9 @@ let in { imports = [ + ../blocks/nginx.nix + ../blocks/lldap.nix + ../../lib/module.nix ]; @@ -293,6 +296,22 @@ in }; }; }; + + 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 ( diff --git a/modules/services/firefly-iii/docs/default.md b/modules/services/firefly-iii/docs/default.md index 87ea3b6..c94b972 100644 --- a/modules/services/firefly-iii/docs/default.md +++ b/modules/services/firefly-iii/docs/default.md @@ -12,8 +12,14 @@ It also sets up the Firefly-iii data importer service and nearly automatically links it to the Firefly-iii instance using a Personal Account Token. Instructions on how to do so is given in the next section. +## Features {#services-firefly-iii-features} + +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-firefly-iii-usage-applicationdashboard) + ## Usage {#services-firefly-iii-usage} +### Initial Configuration {#services-firefly-iii-usage-configuration} + The following snippet assumes a few blocks have been setup already: - the [secrets block](usage.html#usage-secrets) with SOPS, @@ -127,6 +133,38 @@ shb.lldap.ensureUsers.USERNAME.groups = [ ]; ``` +### Application Dashboard {#services-firefly-iii-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-firefly-iii-options-shb.firefly-iii.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Finance.services.Firefly-iii = { + sortOrder = 1; + dashboard.request = config.shb.firefly-iii.dashboard.request; + settings.widget.type = "firefly"; + }; +} +``` + +The widget type needs to be set manually otherwise it is not displayed correctly. + +An API key can be set to show extra info: + +```nix +{ + shb.homepage.servicesGroups.Finance.services.Firefly-iii = { + apiKey.result = config.shb.sops.secret."firefly-iii/homepageApiKey".result; + }; + + shb.sops.secret."firefly-iii/homepageApiKey".request = + config.shb.homepage.servicesGroups.Finance.services.Firefly-iii.apiKey.request; +} +``` + ## Database Inspection {#services-firefly-iii-database-inspection} Access the database with: diff --git a/modules/services/forgejo.nix b/modules/services/forgejo.nix index 387a072..9666f89 100644 --- a/modules/services/forgejo.nix +++ b/modules/services/forgejo.nix @@ -408,6 +408,21 @@ in type = bool; default = false; }; + + 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.forgejo.subdomain}.\${config.shb.forgejo.domain}"; + internalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + internalUrlText = "https://\${config.shb.forgejo.subdomain}.\${config.shb.forgejo.domain}"; + }; + }; + }; }; config = mkMerge [ diff --git a/modules/services/forgejo/docs/default.md b/modules/services/forgejo/docs/default.md index 9c39855..a2af906 100644 --- a/modules/services/forgejo/docs/default.md +++ b/modules/services/forgejo/docs/default.md @@ -17,6 +17,7 @@ LDAP and SSO integration as well as one local runner. - Access through [subdomain](#services-forgejo-options-shb.forgejo.subdomain) using reverse proxy. [Manual](#services-forgejo-usage-configuration). - Access through [HTTPS](#services-forgejo-options-shb.forgejo.ssl) using reverse proxy. [Manual](#services-forgejo-usage-configuration). - [Backup](#services-forgejo-options-shb.forgejo.sso) through the [backup block](./blocks-backup.html). [Manual](#services-forgejo-usage-backup). +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-forgejo-usage-applicationdashboard) ## Usage {#services-forgejo-usage} @@ -206,6 +207,22 @@ The name `"forgejo"` in the `instances` can be anything. The `config.shb.forgejo.backup` option provides what directories to backup. You can define any number of Restic instances to backup Forgejo multiple times. +### Application Dashboard {#services-forgejo-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-forgejo-options-shb.forgejo.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Admin.services.Forgejo = { + sortOrder = 1; + dashboard.request = config.shb.forgejo.dashboard.request; + }; +} +``` + ### Extra Settings {#services-forgejo-usage-extra-settings} Other Forgejo settings can be accessed through the nixpkgs [stock service][]. diff --git a/modules/services/grocy.nix b/modules/services/grocy.nix index 3cc090f..a4ddf4a 100644 --- a/modules/services/grocy.nix +++ b/modules/services/grocy.nix @@ -111,6 +111,21 @@ in default = false; example = true; }; + + 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.grocy.subdomain}.\${config.shb.grocy.domain}"; + internalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + internalUrlText = "https://\${config.shb.grocy.subdomain}.\${config.shb.grocy.domain}"; + }; + }; + }; }; config = lib.mkIf cfg.enable ( diff --git a/modules/services/hledger.nix b/modules/services/hledger.nix index 9fabf7a..b4d56f3 100644 --- a/modules/services/hledger.nix +++ b/modules/services/hledger.nix @@ -82,6 +82,21 @@ in default = [ "--forecast" ]; type = lib.types.listOf lib.types.str; }; + + 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.hledger.subdomain}.\${config.shb.hledger.domain}"; + internalUrl = "http://127.0.0.1:${toString config.services.hledger-web.port}"; + internalUrlText = "http://127.0.0.1:\${config.services.hledger-web.port}"; + }; + }; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/services/home-assistant.nix b/modules/services/home-assistant.nix index abfaa7f..b79e43a 100644 --- a/modules/services/home-assistant.nix +++ b/modules/services/home-assistant.nix @@ -218,6 +218,21 @@ in }; }; }; + + 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.home-assistant.subdomain}.\${config.shb.home-assistant.domain}"; + internalUrl = "http://127.0.0.1:${toString config.services.home-assistant.config.http.server_port}"; + internalUrlText = "http://127.0.0.1:\${config.services.home-assistant.config.http.server_port}"; + }; + }; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/services/home-assistant/docs/default.md b/modules/services/home-assistant/docs/default.md index 378cf24..c99c61b 100644 --- a/modules/services/home-assistant/docs/default.md +++ b/modules/services/home-assistant/docs/default.md @@ -15,6 +15,7 @@ LDAP and SSO integration. - Access through [subdomain](#services-home-assistant-options-shb.home-assistant.subdomain) using reverse proxy. [Manual](#services-home-assistant-usage-configuration). - Access through [HTTPS](#services-home-assistant-options-shb.home-assistant.ssl) using reverse proxy. [Manual](#services-home-assistant-usage-configuration). - [Backup](#services-home-assistant-options-shb.home-assistant.backup) through the [backup block](./blocks-backup.html). [Manual](#services-home-assistant-usage-backup). +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-home-assistant-usage-applicationdashboard) - Not yet: declarative SSO. @@ -163,6 +164,57 @@ You can define any number of Restic instances to backup Home-Assistant multiple You will then need to configure more options like the `repository`, as explained in the [restic](blocks-restic.html) documentation. +### Application Dashboard {#services-home-assistant-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-home-assistant-options-shb.home-assistant.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Home.services.HomeAssistant = { + sortOrder = 1; + dashboard.request = config.shb.home-assistant.dashboard.request; + settings.icon = "si-homeassistant"; + }; +} +``` + +The icon needs to be set manually otherwise it is not displayed correctly. + +An API key can be set to show extra info: + +```nix +{ + shb.homepage.servicesGroups.Home.services.HomeAssistant = { + apiKey.result = config.shb.sops.secret."home-assistant/homepageApiKey".result; + }; + + shb.sops.secret."home-assistant/homepageApiKey".request = + config.shb.homepage.servicesGroups.Home.services.HomeAssistant.apiKey.request; +} +``` + +Custom widgets can be set using Home Assistant templating: + +```nix +{ + shb.homepage.servicesGroups.Home.services.HomeAssistant = { + settings.widget.custom = [ + { + template = "{{ states('sensor.power_consumption_power_consumption', with_unit=True, rounded=True) }}"; + label = "energy now"; + } + { + state = "sensor.power_consumption_daily_power_consumption"; + label = "energy today"; + } + ]; + }; +} +``` + ### Extra Components {#services-home-assistant-usage-extra-components} Packaged components can be found in the documentation of the corresponding option diff --git a/modules/services/homepage.nix b/modules/services/homepage.nix new file mode 100644 index 0000000..e5f26b8 --- /dev/null +++ b/modules/services/homepage.nix @@ -0,0 +1,286 @@ +{ + config, + lib, + shb, + ... +}: +let + cfg = config.shb.homepage; + + inherit (lib) types; +in +{ + imports = [ + ../../lib/module.nix + + ../blocks/lldap.nix + ../blocks/nginx.nix + ]; + + options.shb.homepage = { + enable = lib.mkEnableOption "the SHB homepage service"; + + subdomain = lib.mkOption { + type = types.str; + description = '' + Subdomain under which homepage will be served. + + ``` + . + ``` + ''; + example = "homepage"; + }; + + domain = lib.mkOption { + description = '' + Domain under which homepage is served. + + ``` + . + ``` + ''; + type = types.str; + example = "domain.com"; + }; + + ssl = lib.mkOption { + description = "Path to SSL files"; + type = types.nullOr shb.contracts.ssl.certs; + default = null; + }; + + servicesGroups = lib.mkOption { + description = "Group of services that should be showed on the dashboard."; + default = { }; + type = types.attrsOf ( + types.submodule ( + { name, ... }: + { + options = { + name = lib.mkOption { + type = types.str; + description = "Display name of the group. Defaults to the attr name."; + default = name; + }; + sortOrder = lib.mkOption { + description = '' + Order in which groups will be shown. + + The rules are: + + - Lowest number is shown first. + - Two groups having the same number are shown in a consistent (same across multiple deploys) but undefined order. + - Default is null which means at the end. + ''; + type = types.nullOr types.int; + default = null; + }; + services = lib.mkOption { + description = "Services that should be showed in the group on the dashboard."; + default = { }; + type = types.attrsOf ( + types.submodule ( + { name, ... }: + { + options = { + name = lib.mkOption { + type = types.str; + description = "Display name of the service. Defaults to the attr name."; + default = name; + }; + sortOrder = lib.mkOption { + type = types.nullOr types.int; + description = '' + Order in which groups will be shown. + + The rules are: + + - Lowest number is shown first. + - Two groups having the same number are shown in a consistent (same across multiple deploys) but undefined order. + - Default is null which means at the end. + ''; + default = null; + }; + dashboard = lib.mkOption { + description = '' + Provider of the dashboard contract. + + By default: + + - The `serviceName` option comes from the attr name. + - The `icon` option comes from applying `toLower` on the attr name. + - The `siteMonitor` option is set only if `internalUrl` is set. + ''; + type = types.submodule { + options = shb.contracts.dashboard.mkProvider { + resultCfg = { }; + }; + }; + }; + apiKey = lib.mkOption { + description = '' + API key used to access the service. + + This can be used to get data from the service. + ''; + default = null; + type = types.nullOr ( + lib.types.submodule { + options = shb.contracts.secret.mkRequester { + owner = "root"; + restartUnits = [ "homepage-dashboard.service" ]; + }; + } + ); + }; + settings = lib.mkOption { + description = '' + Extra options to pass to the homepage service. + + Check https://gethomepage.dev/configs/services/#icons + if the default icon is not correct. + + And check https://gethomepage.dev/widgets + if the default widget type is not correct. + ''; + default = { }; + type = types.attrsOf types.anything; + example = lib.literalExpression '' + { + icon = "si-homeassistant"; + widget.type = "firefly"; + widget.custom = [ + { + template = "{{ states('sensor.total_power', with_unit=True, rounded=True) }}"; + label = "energy now"; + } + { + state = "sensor.total_power_today"; + label = "energy today"; + } + ]; + } + ''; + }; + }; + } + ) + ); + }; + }; + } + ) + ); + }; + + ldap = lib.mkOption { + description = '' + Setup LDAP integration. + ''; + default = { }; + type = types.submodule { + options = { + userGroup = lib.mkOption { + type = types.str; + description = "Group users must belong to be able to login."; + default = "homepage_user"; + }; + }; + }; + }; + + sso = lib.mkOption { + description = '' + Setup SSO integration. + ''; + default = { }; + type = types.submodule { + options = { + enable = lib.mkEnableOption "SSO integration."; + + authEndpoint = lib.mkOption { + type = lib.types.str; + description = "Endpoint to the SSO provider."; + example = "https://authelia.example.com"; + }; + + authorization_policy = lib.mkOption { + type = types.enum [ + "one_factor" + "two_factor" + ]; + description = "Require one factor (password) or two factor (device) authentication."; + default = "one_factor"; + }; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + services.homepage-dashboard = { + enable = true; + + allowedHosts = "${cfg.subdomain}.${cfg.domain}"; + + settings = { + baseUrl = "https://${cfg.subdomain}.${cfg.domain}"; + startUrl = "https://${cfg.subdomain}.${cfg.domain}"; + disableUpdateCheck = true; + }; + + bookmarks = [ ]; + + services = shb.homepage.asServiceGroup cfg.servicesGroups; + + widgets = [ ]; + }; + + systemd.services.homepage-dashboard.serviceConfig = + let + keys = shb.homepage.allKeys cfg.servicesGroups; + in + { + # LoadCredential = [ + # "Media_Jellyfin:/path" + # ]; + LoadCredential = lib.mapAttrsToList (name: path: "${name}:${path}") keys; + # Environment = [ + # "HOMEPAGE_FILE_Media_Jellyfin=%d/Media_Jellyfin" + # ]; + Environment = lib.mapAttrsToList (name: path: "HOMEPAGE_FILE_${name}=%d/${name}") keys; + }; + + # This should be using a contract instead of setting the option directly. + shb.lldap = lib.mkIf config.shb.lldap.enable { + ensureGroups = { + ${cfg.ldap.userGroup} = { }; + }; + }; + + shb.nginx.vhosts = [ + ( + { + inherit (cfg) subdomain domain ssl; + + upstream = "http://127.0.0.1:${toString config.services.homepage-dashboard.listenPort}/"; + extraConfig = '' + proxy_read_timeout 300s; + proxy_send_timeout 300s; + ''; + autheliaRules = lib.optionals (cfg.sso.enable) [ + { + domain = "${cfg.subdomain}.${cfg.domain}"; + policy = cfg.sso.authorization_policy; + subject = [ "group:${cfg.ldap.userGroup}" ]; + } + ]; + } + // lib.optionalAttrs cfg.sso.enable { + inherit (cfg.sso) authEndpoint; + } + ) + ]; + }; +} diff --git a/modules/services/homepage/docs/Screenshot.png b/modules/services/homepage/docs/Screenshot.png new file mode 100644 index 0000000..5db8eff Binary files /dev/null and b/modules/services/homepage/docs/Screenshot.png differ diff --git a/modules/services/homepage/docs/default.md b/modules/services/homepage/docs/default.md new file mode 100644 index 0000000..f82f9c6 --- /dev/null +++ b/modules/services/homepage/docs/default.md @@ -0,0 +1,191 @@ +# Homepage Service {#services-homepage} + +Defined in [`/modules/services/homepage.nix`](@REPO@/modules/services/homepage.nix), +found in the `selfhostblocks.nixosModules.homepage` module. +See [the manual](usage.html#usage-flake) for how to import the module in your code. + +This service sets up [Homepage Dashboard][] which provides +a highly customizable homepage Docker and service API integrations. + +![](./Screenshot.png) + +[Homepage Dashboard]: https://github.com/gethomepage/homepage + +## Features {#services-homepage-features} + +- Declarative SSO login through forward authentication. + Only users of the [Homepage LDAP user group][] can access the web UI. + This is enforced using the [Authelia block][] which integrates with the LLDAP block. +- Access through [subdomain][] using the reverse proxy. + It is implemented with the [Nginx block][]. +- Access through [HTTPS][] using the reverse proxy. + It is implemented with the [SSL block][]. +- Integration with [secrets contract][] to set the API key for a widget. + +[Homepage LDAP user group]: #services-homepage-options-shb.homepage.ldap.userGroup +[Authelia block]: blocks-authelia.html +[subdomain]: #services-open-webui-options-shb.open-webui.subdomain +[HTTPS]: #services-open-webui-options-shb.open-webui.ssl +[Nginx block]: blocks-nginx.html +[SSL block]: blocks-ssl.html +[secrets contract]: contracts-secret.html + +::: {.note} +The service does not use state so no backup or impermanence integration is provided. +::: + +## Usage {#services-homepage-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). + +::: {.note} +Part of the configuration is done through the `shb.homepage` option described here +and the rest is done through the upstream [`services.homepage-dashboard`][] option. +::: + +[`services.homepage-dashboard`]: https://search.nixos.org/options?query=services.homepage-dashboard + +### Main service configuration {#services-homepage-usage-main} + +This part sets up the web UI and its integration with the other SHB services. +It also creates the various service groups which will hold each service. +The names are arbitrary and you can order them as you wish through the `sortOrder` option. + +```nix +{ + shb.certs.certs.letsencrypt.${domain}.extraDomains = [ + "${config.shb.homepage.subdomain}.${config.shb.homepage.domain}" + ]; + shb.homepage = { + enable = true; + subdomain = "home"; + inherit domain; + ssl = config.shb.certs.certs.letsencrypt.${domain}; + + sso = { + enable = true; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + + servicesGroups = { + Home.sortOrder = 1; + Documents.sortOrder = 2; + Finance.sortOrder = 3; + Media.sortOrder = 4; + Admin.sortOrder = 5; + }; + }; + + services.homepage-dashboard = { + settings = { + statusStyle = "dot"; + disableIndexing = true; + }; + + widgets = [ + { + locale = "fr"; + format = { + dateStyle = "long"; + timeStyle = "long"; + }; + } + ]; + }; +} +``` + +The [Homepage LDAP user group][] is created automatically and users can be added declaratively to the group with:. + +```nix +{ + shb.lldap.ensureUsers.${user}.groups = [ + config.shb.homepage.ldap.userGroup + ]; +} +``` + +### Display SHB service {#services-homepage-usage-service} + +A service consumer of the dashboard contract provides a `dashboard` option that can be used like so: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Jellyfin = { + sortOrder = 2; + dashboard.request = config.shb.jellyfin.dashboard.request; + }; +} +``` + +By default: + + - The `serviceName` option comes from the attr name, here `Jellyfin`. + - The `icon` option comes from applying `toLower` on the attr name. + - The `siteMonitor` option is set only if `internalUrl` is set. + +They can be overridden by setting them in the [settings](#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.settings option) (see option documentation for examples): + +```nix +{ + shb.homepage.servicesGroups.Media.services.Jellyfin = { + sortOrder = 2; + dashboard.request = config.shb..dashboard.request; + settings = { + // custom options here. + }; + }; +} +``` + +Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`. + +### Display custom service {#services-homepage-usage-custom} + +To display a service that does not provide a `dashboard` option like in the previous section, set the values of the request manually: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Jellyfin = { + sortOrder = 2; + dashboard.request = { + externalUrl = "https://jellyfin.example.com"; + internalUrl = "http://127.0.0.1:8081"; + }; + }; +} +``` + +### Add API key for widget {#services-homepage-usage-widget} + +For services [supporting a widget](https://gethomepage.dev/widgets/), +create an API key through the service's web UI if available +then store it securely (using SOPS for example) and provide it through the +`apiKey` option: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Jellyfin = { + sortOrder = 1; + dashboard.request = config.shb.jellyfin.dashboard.request; + apiKey.result = config.shb.sops.secret."jellyfin/homepageApiKey".result; + }; + shb.sops.secret."jellyfin/homepageApiKey".request = + config.shb.homepage.servicesGroups.Media.services.Jellyfin.apiKey.request; +} +``` + +Unfortunately creating API keys declaratively is rarely supported by upstream services. + +## Options Reference {#services-homepage-options} + +```{=include=} options +id-prefix: services-homepage-options- +list-id: selfhostblocks-service-homepage-options +source: @OPTIONS_JSON@ +``` diff --git a/modules/services/immich.nix b/modules/services/immich.nix index 28abfe3..7f0171b 100644 --- a/modules/services/immich.nix +++ b/modules/services/immich.nix @@ -465,6 +465,20 @@ in default = false; example = true; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${fqdn}"; + externalUrlText = "https://\${config.shb.immich.subdomain}.\${config.shb.immich.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; config = mkIf cfg.enable { diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix index 40f8832..0e8c541 100644 --- a/modules/services/jellyfin.nix +++ b/modules/services/jellyfin.nix @@ -317,6 +317,20 @@ in }; }; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${fqdn}"; + externalUrlText = "https://\${config.shb.jellyfin.subdomain}.\${config.shb.jellyfin.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; imports = [ diff --git a/modules/services/jellyfin/docs/default.md b/modules/services/jellyfin/docs/default.md index b36b64b..c06b0eb 100644 --- a/modules/services/jellyfin/docs/default.md +++ b/modules/services/jellyfin/docs/default.md @@ -20,9 +20,12 @@ this one sets up, in a fully declarative manner: - Declarative [LDAP](#services-jellyfin-options-shb.jellyfin.ldap) configuration. - Declarative [SSO](#services-jellyfin-options-shb.jellyfin.sso) configuration. - [Backup](#services-jellyfin-options-shb.jellyfin.backup) through the [backup block](./blocks-backup.html). [Manual](#services-jellyfin-usage-backup). +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-jellyfin-usage-applicationdashboard) ## Usage {#services-jellyfin-usage} +### Initial Configuration {#services-jellyfin-usage-configuration} + The following snippet assumes a few blocks have been setup already: - the [secrets block](usage.html#usage-secrets) with SOPS, @@ -133,6 +136,35 @@ shb.lldap.ensureUsers.USERNAME.groups = [ ]; ``` +### Application Dashboard {#services-jellyfin-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-jellyfin-options-shb.jellyfin.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Jellyfin = { + sortOrder = 1; + dashboard.request = config.shb.jellyfin.dashboard.request; + }; +} +``` + +An API key can be set to show extra info: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Jellyfin = { + apiKey.result = config.shb.sops.secret."jellyfin/homepageApiKey".result; + }; + + shb.sops.secret."jellyfin/homepageApiKey".request = + config.shb.homepage.servicesGroups.Media.services.Jellyfin.apiKey.request; +} +``` + ## Debug {#services-jellyfin-debug} In case of an issue, check the logs for systemd service `jellyfin.service`. diff --git a/modules/services/karakeep.nix b/modules/services/karakeep.nix index 20107b3..942337d 100644 --- a/modules/services/karakeep.nix +++ b/modules/services/karakeep.nix @@ -175,6 +175,20 @@ in }; }; }; + + 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.karakeep.subdomain}.\${config.shb.karakeep.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; config = ( diff --git a/modules/services/karakeep/docs/default.md b/modules/services/karakeep/docs/default.md index d46cb05..cda315e 100644 --- a/modules/services/karakeep/docs/default.md +++ b/modules/services/karakeep/docs/default.md @@ -21,9 +21,12 @@ It integrates well with [Ollama][]. - Access through [subdomain](#services-karakeep-options-shb.karakeep.subdomain) using reverse proxy. - Access through [HTTPS](#services-karakeep-options-shb.karakeep.ssl) using reverse proxy. - [Backup](#services-karakeep-options-shb.karakeep.sso) through the [backup block](./blocks-backup.html). +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-karakeep-usage-applicationdashboard) ## Usage {#services-karakeep-usage} +### Initial Configuration {#services-karakeep-usage-configuration} + The following snippet assumes a few blocks have been setup already: - the [secrets block](usage.html#usage-secrets) with SOPS, @@ -66,6 +69,35 @@ The [user](#services-open-webui-options-shb.open-webui.ldap.userGroup) and [admin](#services-open-webui-options-shb.open-webui.ldap.adminGroup) LDAP groups are created automatically. +### Application Dashboard {#services-karakeep-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-karakeep-options-shb.karakeep.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Documents.services.Karakeep = { + sortOrder = 3; + dashboard.request = config.shb.karakeep.dashboard.request; + }; +} +``` + +An API key can be set to show extra info: + +```nix +{ + shb.homepage.servicesGroups.Documents.services.Karakeep = { + apiKey.result = config.shb.sops.secret."karakeep/homepageApiKey".result; + }; + + shb.sops.secret."karakeep/homepageApiKey".request = + config.shb.homepage.servicesGroups.Documents.services.Karakeep.apiKey.request; +} +``` + ## Integration with Ollama {#services-karakeep-ollama} Assuming ollama is enabled, it will be available on port `config.services.ollama.port`. diff --git a/modules/services/nextcloud-server.nix b/modules/services/nextcloud-server.nix index 6cd6fe0..8cfae23 100644 --- a/modules/services/nextcloud-server.nix +++ b/modules/services/nextcloud-server.nix @@ -694,6 +694,21 @@ in but Self Host Blocks assumes that it will not be the case for most users. ''; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${fqdn}"; + externalUrlText = "https://\${config.shb.nextcloud.subdomain}.\${config.shb.nextcloud.domain}"; + internalUrl = "https://${fqdn}"; + internalUrlText = "https://\${config.shb.nextcloud.subdomain}.\${config.shb.nextcloud.domain}"; + }; + }; + }; }; config = lib.mkMerge [ diff --git a/modules/services/nextcloud-server/docs/default.md b/modules/services/nextcloud-server/docs/default.md index 083a7c2..957e3b2 100644 --- a/modules/services/nextcloud-server/docs/default.md +++ b/modules/services/nextcloud-server/docs/default.md @@ -35,8 +35,8 @@ It is based on the nixpkgs Nextcloud server and provides opinionated defaults. - Forces PostgreSQL as the database. - Forces Redis as the cache and sets good defaults. - Backup of the [`shb.nextcloud.dataDir`][dataDir] through the [backup block](./blocks-backup.html). -- [Dashboard](#services-nextcloudserver-dashboard) for monitoring of reverse proxy, PHP-FPM, and database backups through the [monitoring - block](./blocks-monitoring.html). +- [Monitoring Dashboard](#services-nextcloudserver-dashboard) for monitoring of reverse proxy, PHP-FPM, and database backups through the [monitoring block](./blocks-monitoring.html). +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. - [Integration Tests](@REPO@/test/services/nextcloud.nix) - Tests system cron job is setup correctly. - Tests initial admin user and password are setup correctly. @@ -344,6 +344,22 @@ Note that this will backup the whole PostgreSQL instance, not just the Nextcloud database. This limitation will be lifted in the future. +### Application Dashboard {#services-nextcloudserver-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-nextcloudserver-options-shb.nextcloud.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Documents.services.Nextcloud = { + sortOrder = 1; + dashboard.request = config.shb.nextcloud.dashboard.request; + }; +} +``` + ### Enable Preview Generator App {#services-nextcloudserver-usage-previewgenerator} The following snippet installs and enables the [Preview @@ -537,7 +553,7 @@ by issuing the command `nextcloud-occ config:system:delete instanceid`. Head over to the [Nextcloud demo](demo-nextcloud-server.html) for a demo that installs Nextcloud with or without LDAP integration on a VM with minimal manual steps. -## Dashboard {#services-nextcloudserver-dashboard} +## Monitoring Dashboard {#services-nextcloudserver-dashboard} The dashboard is added to Grafana automatically under "Self Host Blocks > Nextcloud" as long as the Nextcloud service is [enabled][] diff --git a/modules/services/open-webui.nix b/modules/services/open-webui.nix index 99a17e1..233f496 100644 --- a/modules/services/open-webui.nix +++ b/modules/services/open-webui.nix @@ -160,6 +160,20 @@ in }; }; }; + + 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.open-webui.subdomain}.\${config.shb.open-webui.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; config = ( diff --git a/modules/services/open-webui/docs/default.md b/modules/services/open-webui/docs/default.md index d12ec8c..c604bde 100644 --- a/modules/services/open-webui/docs/default.md +++ b/modules/services/open-webui/docs/default.md @@ -21,9 +21,12 @@ This service sets up [Open WebUI][] which provides a frontend to various LLMs. - Access through [subdomain](#services-open-webui-options-shb.open-webui.subdomain) using reverse proxy. - Access through [HTTPS](#services-open-webui-options-shb.open-webui.ssl) using reverse proxy. - [Backup](#services-open-webui-options-shb.open-webui.sso) through the [backup block](./blocks-backup.html). +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-open-webui-usage-applicationdashboard) ## Usage {#services-open-webui-usage} +### Initial Configuration {#services-open-webui-usage-configuration} + The following snippet assumes a few blocks have been setup already: - the [secrets block](usage.html#usage-secrets) with SOPS, @@ -63,6 +66,25 @@ The [user](#services-open-webui-options-shb.open-webui.ldap.userGroup) and [admin](#services-open-webui-options-shb.open-webui.ldap.adminGroup) LDAP groups are created automatically. +### Application Dashboard {#services-open-webui-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-open-webui-options-shb.open-webui.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Documents.services.OpenWebUI = { + sortOrder = 1; + dashboard.request = config.shb.home-assistant.dashboard.request; + settings.icon = "sh-open-webui"; + }; +} +``` + +The icon needs to be set manually otherwise it is not displayed correctly. + ## Integration with OLLAMA {#services-open-webui-ollama} Assuming ollama is enabled, it will be available on port `config.services.ollama.port`. diff --git a/modules/services/paperless.nix b/modules/services/paperless.nix index 8ab3d86..9954276 100644 --- a/modules/services/paperless.nix +++ b/modules/services/paperless.nix @@ -59,10 +59,8 @@ let mkIf lists mkOption - optionals ; inherit (lib.types) - attrs attrsOf bool enum @@ -346,6 +344,20 @@ in }; }; }; + + 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.paperless.subdomain}.\${config.shb.paperless.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; config = mkIf cfg.enable { diff --git a/modules/services/pinchflat.nix b/modules/services/pinchflat.nix index 98f5a82..1ad77fd 100644 --- a/modules/services/pinchflat.nix +++ b/modules/services/pinchflat.nix @@ -1,7 +1,6 @@ { config, lib, - pkgs, shb, ... }: @@ -132,6 +131,20 @@ in }; }; }; + + 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.pinchflat.subdomain}.\${config.shb.pinchflat.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/services/pinchflat/docs/default.md b/modules/services/pinchflat/docs/default.md index b4dcf55..4688025 100644 --- a/modules/services/pinchflat/docs/default.md +++ b/modules/services/pinchflat/docs/default.md @@ -9,8 +9,14 @@ this one sets up, in a fully declarative manner, LDAP and SSO integration and has a nicer option for secrets. +## Features {#services-pinchflat-features} + +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-pinchflat-usage-applicationdashboard) + ## Usage {#services-pinchflat-usage} +### Initial Configuration {#services-pinchflat-usage-configuration} + The following snippet assumes a few blocks have been setup already: - the [secrets block](usage.html#usage-secrets) with SOPS, @@ -46,7 +52,7 @@ Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64` The [user](#services-pinchflat-options-shb.pinchflat.ldap.userGroup) LDAP group is created automatically. -## Backup {#services-pinchflat-usage-backup} +### Backup {#services-pinchflat-usage-backup} Backing up Pinchflat using the [Restic block](blocks-restic.html) is done like so: @@ -63,6 +69,22 @@ The name `"pinchflat"` in the `instances` can be anything. The `config.shb.pinchflat.backup` option provides what directories to backup. You can define any number of Restic instances to backup Pinchflat multiple times. +### Application Dashboard {#services-pinchflat-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-pinchflat-options-shb.pinchflat.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Pinchflat = { + sortOrder = 2; + dashboard.request = config.shb.pinchflat.dashboard.request; + }; +} +``` + ## Options Reference {#services-pinchflat-options} ```{=include=} options diff --git a/modules/services/vaultwarden.nix b/modules/services/vaultwarden.nix index 8e09a29..7adad9e 100644 --- a/modules/services/vaultwarden.nix +++ b/modules/services/vaultwarden.nix @@ -170,6 +170,20 @@ in default = false; example = true; }; + + 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.vaultwarden.subdomain}.\${config.shb.vaultwarden.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/services/vaultwarden/docs/default.md b/modules/services/vaultwarden/docs/default.md index 49c4454..f903454 100644 --- a/modules/services/vaultwarden/docs/default.md +++ b/modules/services/vaultwarden/docs/default.md @@ -12,7 +12,7 @@ This NixOS module is a service that sets up a [Vaultwarden Server](https://githu - Backup of the data directory through the [backup contract](./contracts-backup.html). - [Integration Tests](@REPO@/test/services/vaultwarden.nix) - Tests /admin can only be accessed when authenticated with SSO. -- Access to advanced options not exposed here thanks to how NixOS modules work. +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. ## Usage {#services-vaultwarden-usage} @@ -120,6 +120,22 @@ The name `"vaultwarden"` in the `instances` can be anything. The `config.shb.vaultwarden.backup` option provides what directories to backup. You can define any number of Restic instances to backup Vaultwarden multiple times. +### Application Dashboard {#services-vaultwarden-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-vaultwarden-options-shb.vaultwarden.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Documents.services.Vaultwarden = { + sortOrder = 10; + dashboard.request = config.shb.vaultwarden.dashboard.request; + }; +} +``` + ## Maintenance {#services-vaultwarden-maintenance} No command-line tool is provided to administer Vaultwarden. diff --git a/test/modules/homepage.nix b/test/modules/homepage.nix new file mode 100644 index 0000000..bbe8256 --- /dev/null +++ b/test/modules/homepage.nix @@ -0,0 +1,269 @@ +{ shb }: +{ + testHomepageAsServiceGroup = { + expected = [ + { + "Media" = [ + { + "Jellyfin" = { + "href" = "https://example.com/jellyfin"; + "icon" = "sh-jellyfin"; + "siteMonitor" = "http://127.0.0.1:8096"; + }; + } + ]; + } + ]; + + expr = shb.homepage.asServiceGroup { + Media = { + services = { + Jellyfin = { + dashboard.request = { + externalUrl = "https://example.com/jellyfin"; + internalUrl = "http://127.0.0.1:8096"; + }; + apiKey = null; + }; + }; + }; + }; + }; + + testHomepageAsServiceGroupApiKey = { + expected = [ + { + "Media" = [ + { + "Jellyfin" = { + "href" = "https://example.com/jellyfin"; + "icon" = "sh-jellyfin"; + "siteMonitor" = "http://127.0.0.1:8096"; + "widget" = { + "key" = "{{HOMEPAGE_FILE_Media_Jellyfin}}"; + "password" = "{{HOMEPAGE_FILE_Media_Jellyfin}}"; + "type" = "jellyfin"; + "url" = "http://127.0.0.1:8096"; + }; + }; + } + ]; + } + ]; + + expr = shb.homepage.asServiceGroup { + Media = { + services = { + Jellyfin = { + dashboard.request = { + externalUrl = "https://example.com/jellyfin"; + internalUrl = "http://127.0.0.1:8096"; + }; + apiKey.result.path = "path_D"; + }; + }; + }; + }; + }; + + testHomepageAsServiceGroupNoServiceMonitor = { + expected = [ + { + "Media" = [ + { + "Jellyfin" = { + "href" = "https://example.com/jellyfin"; + "icon" = "sh-jellyfin"; + "siteMonitor" = null; + }; + } + ]; + } + ]; + + expr = shb.homepage.asServiceGroup { + Media = { + services = { + Jellyfin = { + dashboard.request = { + externalUrl = "https://example.com/jellyfin"; + internalUrl = null; + }; + apiKey = null; + }; + }; + }; + }; + }; + + testHomepageAsServiceGroupOverride = { + expected = [ + { + "Media" = [ + { + "Jellyfin" = { + "href" = "https://example.com/jellyfin"; + "icon" = "sh-icon"; + "siteMonitor" = "http://127.0.0.1:8096"; + }; + } + ]; + } + ]; + + expr = shb.homepage.asServiceGroup { + Media = { + services = { + Jellyfin = { + dashboard.request = { + externalUrl = "https://example.com/jellyfin"; + internalUrl = "http://127.0.0.1:8096"; + }; + settings = { + icon = "sh-icon"; + }; + apiKey = null; + }; + }; + }; + }; + }; + + testHomepageAsServiceGroupSortOrder = { + expected = [ + { "C" = [ ]; } + { "A" = [ ]; } + { "B" = [ ]; } + ]; + + expr = shb.homepage.asServiceGroup { + A = { + sortOrder = 2; + services = { }; + }; + B = { + sortOrder = 3; + services = { }; + }; + C = { + sortOrder = 1; + services = { }; + }; + }; + }; + + testHomepageAsServiceServicesSortOrder = { + expected = [ + { + "Media" = [ + { + "A" = { + "href" = "https://example.com/a"; + "icon" = "sh-a"; + "siteMonitor" = null; + }; + } + { + "C" = { + "href" = "https://example.com/c"; + "icon" = "sh-c"; + "siteMonitor" = null; + }; + } + { + "B" = { + "href" = "https://example.com/b"; + "icon" = "sh-b"; + "siteMonitor" = null; + }; + } + ]; + } + ]; + + expr = shb.homepage.asServiceGroup { + Media = { + sortOrder = null; + services = { + A = { + sortOrder = 1; + dashboard.request = { + externalUrl = "https://example.com/a"; + internalUrl = null; + }; + apiKey = null; + }; + B = { + sortOrder = 3; + dashboard.request = { + externalUrl = "https://example.com/b"; + internalUrl = null; + }; + apiKey = null; + }; + C = { + sortOrder = 2; + dashboard.request = { + externalUrl = "https://example.com/c"; + internalUrl = null; + }; + apiKey = null; + }; + }; + }; + }; + }; + + testHomepageAllKeys = { + expected = { + "A_A" = "path_A"; + "A_B" = "path_B"; + "B_D" = "path_D"; + }; + + expr = shb.homepage.allKeys { + A = { + sortOrder = 1; + services = { + A = { + sortOrder = 1; + dashboard.request = { + externalUrl = "https://example.com/a"; + internalUrl = null; + }; + apiKey.result.path = "path_A"; + }; + B = { + sortOrder = 2; + dashboard.request = { + externalUrl = "https://example.com/b"; + internalUrl = null; + }; + apiKey.result.path = "path_B"; + }; + }; + }; + B = { + sortOrder = 2; + services = { + C = { + sortOrder = 1; + dashboard.request = { + externalUrl = "https://example.com/a"; + internalUrl = null; + }; + apiKey = null; + }; + D = { + sortOrder = 2; + dashboard.request = { + externalUrl = "https://example.com/b"; + internalUrl = null; + }; + apiKey.result.path = "path_D"; + }; + }; + }; + }; + }; +} diff --git a/test/modules/lib.nix b/test/modules/lib.nix index 589c23e..9711d9a 100644 --- a/test/modules/lib.nix +++ b/test/modules/lib.nix @@ -131,3 +131,4 @@ in ''; }; } +// import ./homepage.nix { inherit shb; } diff --git a/test/services/homepage.nix b/test/services/homepage.nix new file mode 100644 index 0000000..f0cdd7f --- /dev/null +++ b/test/services/homepage.nix @@ -0,0 +1,209 @@ +{ shb, ... }: +let + commonTestScript = shb.test.mkScripts { + hasSSL = { node, ... }: !(isNull node.config.shb.homepage.ssl); + waitForServices = + { ... }: + [ + "homepage-dashboard.service" + "nginx.service" + ]; + waitForPorts = + { node, ... }: + [ + node.config.services.homepage-dashboard.listenPort + ]; + }; + + basic = + { config, ... }: + { + imports = [ + shb.test.baseModule + ../../modules/blocks/hardcodedsecret.nix + ../../modules/services/homepage.nix + ]; + + test = { + subdomain = "h"; + }; + + shb.homepage = { + enable = true; + inherit (config.test) subdomain domain; + + servicesGroups.MyHomeGroup.services.TestService.dashboard = { }; + }; + }; + + clientLogin = + { config, ... }: + { + imports = [ + shb.test.baseModule + shb.test.clientLoginModule + ]; + test = { + subdomain = "h"; + }; + + 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('TestService')).to_be_visible()" + ]; + } + ]; + }; + }; + + https = + { config, ... }: + { + shb.homepage = { + ssl = config.shb.certs.certs.selfsigned.n; + }; + }; + + ldap = + { config, ... }: + { + shb.homepage = { + ldap = { + userGroup = "user_group"; + }; + }; + }; + + clientLoginSso = + { config, ... }: + { + imports = [ + shb.test.baseModule + shb.test.clientLoginModule + ]; + test = { + subdomain = "h"; + }; + + 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('TestService')).to_be_visible(timeout=10000)" + ]; + } + # Bob, with its admin role only, cannot login into Karakeep because admins do not exist in Karakeep. + { + 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.homepage = { + sso = { + enable = true; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + }; + }; +in +{ + basic = shb.test.runNixOSTest { + name = "homepage_basic"; + + nodes.client = { + imports = [ + clientLogin + ]; + }; + nodes.server = { + imports = [ + basic + ]; + }; + + testScript = commonTestScript.access; + }; + + https = shb.test.runNixOSTest { + name = "homepage_https"; + + nodes.client = { + imports = [ + clientLogin + ]; + }; + nodes.server = { + imports = [ + basic + shb.test.certs + https + ]; + }; + + testScript = commonTestScript.access; + }; + + sso = shb.test.runNixOSTest { + name = "homepage_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; + }; + }; +}