homepage: init with dashboard contract

This commit is contained in:
ibizaman 2026-02-23 22:36:27 +01:00 committed by Pierre Penninckx
parent 77869300ee
commit f485cc60fe
50 changed files with 3072 additions and 411 deletions

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
<!-- Read these docs at https://shb.skarabox.com -->
# 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

View file

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

View file

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

92
lib/homepage.nix Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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:
<!-- Somehow generate this list -->
- The homepage service under its [shb.homepage.servicesGroups.<name>.services.<name>.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 = {
<provider-module> = {
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@
```

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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][].

View file

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

View file

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

View file

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

View file

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

View file

@ -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.
```
<subdomain>.<domain>
```
'';
example = "homepage";
};
domain = lib.mkOption {
description = ''
Domain under which homepage is served.
```
<subdomain>.<domain>
```
'';
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;
}
)
];
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View file

@ -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.<service>.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@
```

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

269
test/modules/homepage.nix Normal file
View file

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

View file

@ -131,3 +131,4 @@ in
'';
};
}
// import ./homepage.nix { inherit shb; }

209
test/services/homepage.nix Normal file
View file

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