From 438821de68f117acefe73851cfaa92fb49ab70c4 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Mon, 19 Feb 2024 01:04:14 +0000
Subject: [PATCH 01/18] flake.lock: Update
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/a4d4fe8c5002202493e87ec8dbc91335ff55552c' (2024-02-15)
  → 'github:nixos/nixpkgs/5863c27340ba4de8f83e7e3c023b9599c3cb3c80' (2024-02-16)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/48afd3264ec52bee85231a7122612e2c5202fa74' (2024-02-13)
  → 'github:Mic92/sops-nix/ffed177a9d2c685901781c3c6c9024ae0ffc252b' (2024-02-18)
• Updated input 'sops-nix/nixpkgs':
    'github:NixOS/nixpkgs/442d407992384ed9c0e6d352de75b69079904e4e' (2024-02-09)
  → 'github:NixOS/nixpkgs/6e2f00c83911461438301db0dba5281197fe4b3a' (2024-02-17)
• Updated input 'sops-nix/nixpkgs-stable':
    'github:NixOS/nixpkgs/d8cd80616c8800feec0cab64331d7c3d5a1a6d98' (2024-02-10)
  → 'github:NixOS/nixpkgs/69405156cffbdf2be50153f13cbdf9a0bea38e49' (2024-02-17)
---
 flake.lock | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/flake.lock b/flake.lock
index 8e636f8..dbc3e2b 100644
--- a/flake.lock
+++ b/flake.lock
@@ -35,11 +35,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1707956935,
-        "narHash": "sha256-ZL2TrjVsiFNKOYwYQozpbvQSwvtV/3Me7Zwhmdsfyu4=",
+        "lastModified": 1708118438,
+        "narHash": "sha256-kk9/0nuVgA220FcqH/D2xaN6uGyHp/zoxPNUmPCMmEE=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "a4d4fe8c5002202493e87ec8dbc91335ff55552c",
+        "rev": "5863c27340ba4de8f83e7e3c023b9599c3cb3c80",
         "type": "github"
       },
       "original": {
@@ -51,11 +51,11 @@
     },
     "nixpkgs-stable": {
       "locked": {
-        "lastModified": 1707603439,
-        "narHash": "sha256-LodBVZ3+ehJP2azM5oj+JrhfNAAzmTJ/OwAIOn0RfZ0=",
+        "lastModified": 1708210246,
+        "narHash": "sha256-Q8L9XwrBK53fbuuIFMbjKvoV7ixfLFKLw4yV+SD28Y8=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "d8cd80616c8800feec0cab64331d7c3d5a1a6d98",
+        "rev": "69405156cffbdf2be50153f13cbdf9a0bea38e49",
         "type": "github"
       },
       "original": {
@@ -67,11 +67,11 @@
     },
     "nixpkgs_2": {
       "locked": {
-        "lastModified": 1707451808,
-        "narHash": "sha256-UwDBUNHNRsYKFJzyTMVMTF5qS4xeJlWoeyJf+6vvamU=",
+        "lastModified": 1708151420,
+        "narHash": "sha256-MGT/4aGCWQPQiu6COqJdCj9kSpLPiShgbwpbC38YXC8=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "442d407992384ed9c0e6d352de75b69079904e4e",
+        "rev": "6e2f00c83911461438301db0dba5281197fe4b3a",
         "type": "github"
       },
       "original": {
@@ -112,11 +112,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1707842202,
-        "narHash": "sha256-3dTBbCzHJBinwhsisGJHW1HLBsLbj91+a5ZDXt7ttW0=",
+        "lastModified": 1708225343,
+        "narHash": "sha256-Q0uVUOfumc1DcKsIJIfMCHph08MjkOvZxvPb/Vi8hWw=",
         "owner": "Mic92",
         "repo": "sops-nix",
-        "rev": "48afd3264ec52bee85231a7122612e2c5202fa74",
+        "rev": "ffed177a9d2c685901781c3c6c9024ae0ffc252b",
         "type": "github"
       },
       "original": {

From 810d8e5aaa5ae98a0ebac2790e992f4c2fd43be2 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Tue, 20 Feb 2024 01:02:02 +0000
Subject: [PATCH 02/18] flake.lock: Update
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/5863c27340ba4de8f83e7e3c023b9599c3cb3c80' (2024-02-16)
  → 'github:nixos/nixpkgs/b98a4e1746acceb92c509bc496ef3d0e5ad8d4aa' (2024-02-18)
---
 flake.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/flake.lock b/flake.lock
index dbc3e2b..4182546 100644
--- a/flake.lock
+++ b/flake.lock
@@ -35,11 +35,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1708118438,
-        "narHash": "sha256-kk9/0nuVgA220FcqH/D2xaN6uGyHp/zoxPNUmPCMmEE=",
+        "lastModified": 1708296515,
+        "narHash": "sha256-FyF489fYNAUy7b6dkYV6rGPyzp+4tThhr80KNAaF/yY=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "5863c27340ba4de8f83e7e3c023b9599c3cb3c80",
+        "rev": "b98a4e1746acceb92c509bc496ef3d0e5ad8d4aa",
         "type": "github"
       },
       "original": {

From 0fccc02d84386e9e6bfc8c2c5762ddccb00cad2f Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Wed, 21 Feb 2024 01:03:17 +0000
Subject: [PATCH 03/18] flake.lock: Update
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Flake lock file updates:

• Updated input 'sops-nix':
    'github:Mic92/sops-nix/ffed177a9d2c685901781c3c6c9024ae0ffc252b' (2024-02-18)
  → 'github:Mic92/sops-nix/acfcce2a36da17ebb724d2e100d47881880c2e48' (2024-02-20)
---
 flake.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/flake.lock b/flake.lock
index 4182546..9e8f03d 100644
--- a/flake.lock
+++ b/flake.lock
@@ -112,11 +112,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1708225343,
-        "narHash": "sha256-Q0uVUOfumc1DcKsIJIfMCHph08MjkOvZxvPb/Vi8hWw=",
+        "lastModified": 1708456161,
+        "narHash": "sha256-Rh5kJvLZySEPkOxCIX1XA0SpDnYjjXSvixLwKsrcpVE=",
         "owner": "Mic92",
         "repo": "sops-nix",
-        "rev": "ffed177a9d2c685901781c3c6c9024ae0ffc252b",
+        "rev": "acfcce2a36da17ebb724d2e100d47881880c2e48",
         "type": "github"
       },
       "original": {

From 6649d7622baf0f1ac62a4dc6c58cbba85da092a7 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Thu, 22 Feb 2024 01:02:25 +0000
Subject: [PATCH 04/18] flake.lock: Update
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/b98a4e1746acceb92c509bc496ef3d0e5ad8d4aa' (2024-02-18)
  → 'github:nixos/nixpkgs/0e74ca98a74bc7270d28838369593635a5db3260' (2024-02-21)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/acfcce2a36da17ebb724d2e100d47881880c2e48' (2024-02-20)
  → 'github:Mic92/sops-nix/f6b80ab6cd25e57f297fe466ad689d8a77057c11' (2024-02-21)
---
 flake.lock | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/flake.lock b/flake.lock
index 9e8f03d..4ddf2ac 100644
--- a/flake.lock
+++ b/flake.lock
@@ -35,11 +35,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1708296515,
-        "narHash": "sha256-FyF489fYNAUy7b6dkYV6rGPyzp+4tThhr80KNAaF/yY=",
+        "lastModified": 1708475490,
+        "narHash": "sha256-g1v0TsWBQPX97ziznfJdWhgMyMGtoBFs102xSYO4syU=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "b98a4e1746acceb92c509bc496ef3d0e5ad8d4aa",
+        "rev": "0e74ca98a74bc7270d28838369593635a5db3260",
         "type": "github"
       },
       "original": {
@@ -112,11 +112,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1708456161,
-        "narHash": "sha256-Rh5kJvLZySEPkOxCIX1XA0SpDnYjjXSvixLwKsrcpVE=",
+        "lastModified": 1708500294,
+        "narHash": "sha256-mvJIecY3tDKZh7297mqOtOuAvP7U1rqjfLNfmfkjFpU=",
         "owner": "Mic92",
         "repo": "sops-nix",
-        "rev": "acfcce2a36da17ebb724d2e100d47881880c2e48",
+        "rev": "f6b80ab6cd25e57f297fe466ad689d8a77057c11",
         "type": "github"
       },
       "original": {

From 4538b64b92dac52ea0ceaaa88ab0c485ff0bdbe0 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Sat, 24 Feb 2024 01:00:02 +0000
Subject: [PATCH 05/18] flake.lock: Update
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/0e74ca98a74bc7270d28838369593635a5db3260' (2024-02-21)
  → 'github:nixos/nixpkgs/cbc4211f0afffe6dfd2478a62615dd5175a13f9a' (2024-02-23)
---
 flake.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/flake.lock b/flake.lock
index 4ddf2ac..48ac82a 100644
--- a/flake.lock
+++ b/flake.lock
@@ -35,11 +35,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1708475490,
-        "narHash": "sha256-g1v0TsWBQPX97ziznfJdWhgMyMGtoBFs102xSYO4syU=",
+        "lastModified": 1708655239,
+        "narHash": "sha256-ZrP/yACUvDB+zbqYJsln4iwotbH6CTZiTkANJ0AgDv4=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "0e74ca98a74bc7270d28838369593635a5db3260",
+        "rev": "cbc4211f0afffe6dfd2478a62615dd5175a13f9a",
         "type": "github"
       },
       "original": {

From a0cd48b5bc35c26b00ae3bcf98845870aa9da77c Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Mon, 26 Feb 2024 01:04:46 +0000
Subject: [PATCH 06/18] flake.lock: Update
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/cbc4211f0afffe6dfd2478a62615dd5175a13f9a' (2024-02-23)
  → 'github:nixos/nixpkgs/73de017ef2d18a04ac4bfd0c02650007ccb31c2a' (2024-02-24)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/f6b80ab6cd25e57f297fe466ad689d8a77057c11' (2024-02-21)
  → 'github:Mic92/sops-nix/2874fbbe4a65bd2484b0ad757d27a16107f6bc17' (2024-02-25)
• Updated input 'sops-nix/nixpkgs':
    'github:NixOS/nixpkgs/6e2f00c83911461438301db0dba5281197fe4b3a' (2024-02-17)
  → 'github:NixOS/nixpkgs/f63ce824cd2f036216eb5f637dfef31e1a03ee89' (2024-02-24)
• Updated input 'sops-nix/nixpkgs-stable':
    'github:NixOS/nixpkgs/69405156cffbdf2be50153f13cbdf9a0bea38e49' (2024-02-17)
  → 'github:NixOS/nixpkgs/89a2a12e6c8c6a56c72eb3589982c8e2f89c70ea' (2024-02-25)
---
 flake.lock | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/flake.lock b/flake.lock
index 48ac82a..3b91889 100644
--- a/flake.lock
+++ b/flake.lock
@@ -35,11 +35,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1708655239,
-        "narHash": "sha256-ZrP/yACUvDB+zbqYJsln4iwotbH6CTZiTkANJ0AgDv4=",
+        "lastModified": 1708807242,
+        "narHash": "sha256-sRTRkhMD4delO/hPxxi+XwLqPn8BuUq6nnj4JqLwOu0=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "cbc4211f0afffe6dfd2478a62615dd5175a13f9a",
+        "rev": "73de017ef2d18a04ac4bfd0c02650007ccb31c2a",
         "type": "github"
       },
       "original": {
@@ -51,11 +51,11 @@
     },
     "nixpkgs-stable": {
       "locked": {
-        "lastModified": 1708210246,
-        "narHash": "sha256-Q8L9XwrBK53fbuuIFMbjKvoV7ixfLFKLw4yV+SD28Y8=",
+        "lastModified": 1708819810,
+        "narHash": "sha256-1KosU+ZFXf31GPeCBNxobZWMgHsSOJcrSFA6F2jhzdE=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "69405156cffbdf2be50153f13cbdf9a0bea38e49",
+        "rev": "89a2a12e6c8c6a56c72eb3589982c8e2f89c70ea",
         "type": "github"
       },
       "original": {
@@ -67,11 +67,11 @@
     },
     "nixpkgs_2": {
       "locked": {
-        "lastModified": 1708151420,
-        "narHash": "sha256-MGT/4aGCWQPQiu6COqJdCj9kSpLPiShgbwpbC38YXC8=",
+        "lastModified": 1708751719,
+        "narHash": "sha256-0uWOKSpXJXmXswOvDM5Vk3blB74apFB6rNGWV5IjoN0=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "6e2f00c83911461438301db0dba5281197fe4b3a",
+        "rev": "f63ce824cd2f036216eb5f637dfef31e1a03ee89",
         "type": "github"
       },
       "original": {
@@ -112,11 +112,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1708500294,
-        "narHash": "sha256-mvJIecY3tDKZh7297mqOtOuAvP7U1rqjfLNfmfkjFpU=",
+        "lastModified": 1708830076,
+        "narHash": "sha256-Cjh2xdjxC6S6nW6Whr2dxSeh8vjodzhTmQdI4zPJ4RA=",
         "owner": "Mic92",
         "repo": "sops-nix",
-        "rev": "f6b80ab6cd25e57f297fe466ad689d8a77057c11",
+        "rev": "2874fbbe4a65bd2484b0ad757d27a16107f6bc17",
         "type": "github"
       },
       "original": {

From 382544e06e2ed22c84110b479d4e66d291b644e0 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Tue, 27 Feb 2024 01:02:19 +0000
Subject: [PATCH 07/18] flake.lock: Update
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Flake lock file updates:

• Updated input 'sops-nix':
    'github:Mic92/sops-nix/2874fbbe4a65bd2484b0ad757d27a16107f6bc17' (2024-02-25)
  → 'github:Mic92/sops-nix/a1c8de14f60924fafe13aea66b46157f0150f4cf' (2024-02-26)
---
 flake.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/flake.lock b/flake.lock
index 3b91889..23ac301 100644
--- a/flake.lock
+++ b/flake.lock
@@ -112,11 +112,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1708830076,
-        "narHash": "sha256-Cjh2xdjxC6S6nW6Whr2dxSeh8vjodzhTmQdI4zPJ4RA=",
+        "lastModified": 1708987867,
+        "narHash": "sha256-k2lDaDWNTU5sBVHanYzjDKVDmk29RHIgdbbXu5sdzBA=",
         "owner": "Mic92",
         "repo": "sops-nix",
-        "rev": "2874fbbe4a65bd2484b0ad757d27a16107f6bc17",
+        "rev": "a1c8de14f60924fafe13aea66b46157f0150f4cf",
         "type": "github"
       },
       "original": {

From 65a37456e0a8e817ba8bae701d9f04812d49ec8c Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Wed, 28 Feb 2024 01:02:19 +0000
Subject: [PATCH 08/18] flake.lock: Update
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/73de017ef2d18a04ac4bfd0c02650007ccb31c2a' (2024-02-24)
  → 'github:nixos/nixpkgs/13aff9b34cc32e59d35c62ac9356e4a41198a538' (2024-02-26)
---
 flake.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/flake.lock b/flake.lock
index 23ac301..6773800 100644
--- a/flake.lock
+++ b/flake.lock
@@ -35,11 +35,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1708807242,
-        "narHash": "sha256-sRTRkhMD4delO/hPxxi+XwLqPn8BuUq6nnj4JqLwOu0=",
+        "lastModified": 1708984720,
+        "narHash": "sha256-gJctErLbXx4QZBBbGp78PxtOOzsDaQ+yw1ylNQBuSUY=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "73de017ef2d18a04ac4bfd0c02650007ccb31c2a",
+        "rev": "13aff9b34cc32e59d35c62ac9356e4a41198a538",
         "type": "github"
       },
       "original": {

From 8cb1f323f44fb87b0191147edd1c18514ac8d1ce Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Thu, 29 Feb 2024 01:02:04 +0000
Subject: [PATCH 09/18] flake.lock: Update
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Flake lock file updates:

• Updated input 'flake-utils':
    'github:numtide/flake-utils/1ef2e671c3b0c19053962c07dbda38332dcebf26' (2024-01-15)
  → 'github:numtide/flake-utils/d465f4819400de7c8d874d50b982301f28a84605' (2024-02-28)
---
 flake.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/flake.lock b/flake.lock
index 6773800..b0542ba 100644
--- a/flake.lock
+++ b/flake.lock
@@ -5,11 +5,11 @@
         "systems": "systems"
       },
       "locked": {
-        "lastModified": 1705309234,
-        "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
+        "lastModified": 1709126324,
+        "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
+        "rev": "d465f4819400de7c8d874d50b982301f28a84605",
         "type": "github"
       },
       "original": {

From 6cf83e737ecd5b133c461213f640b80cebadef3f Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Fri, 1 Mar 2024 01:08:28 +0000
Subject: [PATCH 10/18] flake.lock: Update
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/13aff9b34cc32e59d35c62ac9356e4a41198a538' (2024-02-26)
  → 'github:nixos/nixpkgs/9099616b93301d5cf84274b184a3a5ec69e94e08' (2024-02-28)
---
 flake.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/flake.lock b/flake.lock
index b0542ba..b709bcb 100644
--- a/flake.lock
+++ b/flake.lock
@@ -35,11 +35,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1708984720,
-        "narHash": "sha256-gJctErLbXx4QZBBbGp78PxtOOzsDaQ+yw1ylNQBuSUY=",
+        "lastModified": 1709150264,
+        "narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "13aff9b34cc32e59d35c62ac9356e4a41198a538",
+        "rev": "9099616b93301d5cf84274b184a3a5ec69e94e08",
         "type": "github"
       },
       "original": {

From fa206d0e1515fb0e49393e7ada6d7e5c6ec1df58 Mon Sep 17 00:00:00 2001
From: ibizaman <ibizapeanut@gmail.com>
Date: Thu, 29 Feb 2024 15:34:53 -0800
Subject: [PATCH 11/18] move templating code to lib file

---
 flake.nix                             |   7 ++
 lib/default.nix                       | 109 +++++++++++++++++++++++--
 modules/blocks/authelia.nix           |  61 ++++++++++++--
 modules/services/home-assistant.nix   |  78 ++++++++++++------
 modules/services/jellyfin.nix         |  26 ++++--
 modules/services/nextcloud-server.nix |   2 +-
 modules/services/vaultwarden.nix      |  17 ++--
 test/modules/lib.nix                  | 110 ++++++++++++++++++++++++++
 test/vm/authelia.nix                  |   5 +-
 test/vm/lib.nix                       |  82 +++++++++++++++++++
 10 files changed, 439 insertions(+), 58 deletions(-)
 create mode 100644 test/modules/lib.nix
 create mode 100644 test/vm/lib.nix

diff --git a/flake.nix b/flake.nix
index 2191f9a..f91c79b 100644
--- a/flake.nix
+++ b/flake.nix
@@ -88,13 +88,20 @@
                 mergeTests (importFiles [
                   ./test/modules/arr.nix
                   ./test/modules/davfs.nix
+                  ./test/modules/lib.nix
                   ./test/modules/nginx.nix
                   ./test/modules/postgresql.nix
                 ]);
             };
+
+            lib = nix-flake-tests.lib.check {
+              inherit pkgs;
+              tests = pkgs.callPackage ./test/modules/lib.nix {};
+            };
           }
           // (vm_test "authelia" ./test/vm/authelia.nix)
           // (vm_test "ldap" ./test/vm/ldap.nix)
+          // (vm_test "lib" ./test/vm/lib.nix)
           // (vm_test "postgresql" ./test/vm/postgresql.nix)
           // (vm_test "monitoring" ./test/vm/monitoring.nix)
           // (vm_test "nextcloud" ./test/vm/nextcloud.nix)
diff --git a/lib/default.nix b/lib/default.nix
index fdb48ea..c47643f 100644
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -1,13 +1,110 @@
-{ lib }:
-{
-  template = file: newPath: replacements:
+{ pkgs, lib }:
+rec {
+  replaceSecrets = { userConfig, resultPath, generator }:
     let
-      templatePath = newPath + ".template";
+      configWithTemplates = withReplacements userConfig;
+
+      nonSecretConfigFile = pkgs.writeText "${resultPath}.template" (generator configWithTemplates);
+
+      replacements = getReplacements userConfig;
+    in
+      replaceSecretsScript {
+        file = nonSecretConfigFile;
+        inherit resultPath replacements;
+      };
+
+  template = file: newPath: replacements: replaceSecretsScript { inherit file replacements; resultPath = newPath; };
+  replaceSecretsScript = { file, resultPath, replacements }:
+    let
+      templatePath = resultPath + ".template";
       sedPatterns = lib.strings.concatStringsSep " " (lib.attrsets.mapAttrsToList (from: to: "-e \"s|${from}|${to}|\"") replacements);
     in
       ''
+      set -euo pipefail
+      set -x
       ln -fs ${file} ${templatePath}
-      rm ${newPath} || :
-      sed ${sedPatterns} ${templatePath} > ${newPath}
+      rm -f ${resultPath}
+      ${pkgs.gnused}/bin/sed ${sedPatterns} ${templatePath}
+      ${pkgs.gnused}/bin/sed ${sedPatterns} ${templatePath} > ${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 = name:
+      "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) name)}%";
+
+  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; };
+
+      secretsWithName = mapAttrsRecursiveCond (v: ! v ? "source") addNameField attrs;
+
+      allSecrets = collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
+
+      t = { transform ? null, ... }: if isNull transform then x: x else transform;
+
+      genReplacement = secret:
+        lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
+    in
+      lib.attrsets.listToAttrs (map genReplacement allSecrets);
+      
+  # 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
+      [];
 }
diff --git a/modules/blocks/authelia.nix b/modules/blocks/authelia.nix
index 6f19f1e..af3928d 100644
--- a/modules/blocks/authelia.nix
+++ b/modules/blocks/authelia.nix
@@ -94,9 +94,54 @@ in
     };
 
     oidcClients = lib.mkOption {
-      type = lib.types.listOf lib.types.anything;
       description = "OIDC clients";
       default = [];
+      type = lib.types.listOf (lib.types.submodule {
+        freeformType = lib.types.attrsOf lib.types.anything;
+
+        options = {
+          id = lib.mkOption {
+            type = lib.types.str;
+            description = "Unique identifier of the OIDC client.";
+          };
+
+          description = lib.mkOption {
+            type = lib.types.nullOr lib.types.str;
+            description = "Human readable description of the OIDC client.";
+            default = null;
+          };
+
+          secret = lib.mkOption {
+            type = shblib.secretFileType;
+            description = "File containing the shared secret with the OIDC client.";
+          };
+
+          public = lib.mkOption {
+            type = lib.types.bool;
+            description = "If the OIDC client is public or not.";
+            default = false;
+            apply = v: if v then "true" else "false";
+          };
+
+          authorization_policy = lib.mkOption {
+            type = lib.types.enum [ "one_factor" "two_factor" ];
+            description = "Require one factor (password) or two factor (device) authentication.";
+            default = "one_factor";
+          };
+
+          redirect_uris = lib.mkOption {
+            type = lib.types.listOf lib.types.str;
+            description = "List of uris that are allowed to be redirected to.";
+          };
+
+          scopes = lib.mkOption {
+            type = lib.types.listOf lib.types.str;
+            description = "Scopes to ask for";
+            example = [ "openid" "profile" "email" "groups" ];
+            default = [];
+          };
+        };
+      });
     };
 
     smtp = lib.mkOption {
@@ -291,13 +336,13 @@ in
     systemd.services."authelia-${fqdn}".preStart =
       let
         mkCfg = clients:
-        let
-          addTemplate = client: (builtins.removeAttrs client ["secretFile"]) // {secret = "%SECRET_${client.id}%";};
-          tmplFile = pkgs.writeText "oidc_clients.yaml" (lib.generators.toYAML {} {identity_providers.oidc.clients = map addTemplate clients;});
-          replace = client: {"%SECRET_${client.id}%" = "$(cat ${toString client.secretFile})";};
-          replacements = lib.foldl (container: client: container // (replace client) ) {} clients;
-        in
-          shblib.template tmplFile "/var/lib/authelia-${fqdn}/oidc_clients.yaml" replacements;
+          shblib.replaceSecrets {
+            userConfig = {
+              identity_providers.oidc.clients = clients;
+            };
+            resultPath = "/var/lib/authelia-${fqdn}/oidc_clients.yaml";
+            generator = lib.generators.toYAML {};
+          };
       in
         lib.mkBefore (mkCfg cfg.oidcClients);
 
diff --git a/modules/services/home-assistant.nix b/modules/services/home-assistant.nix
index 6f16d5d..6790c10 100644
--- a/modules/services/home-assistant.nix
+++ b/modules/services/home-assistant.nix
@@ -4,6 +4,7 @@ let
   cfg = config.shb.home-assistant;
 
   contracts = pkgs.callPackage ../contracts {};
+  shblib = pkgs.callPackage ../../lib {};
 
   fqdn = "${cfg.subdomain}.${cfg.domain}";
 
@@ -18,6 +19,15 @@ let
     export PATH=${pkgs.gnused}/bin:${pkgs.curl}/bin:${pkgs.jq}/bin
     exec ${pkgs.bash}/bin/bash ${ldap_auth_script_repo}/example_configs/lldap-ha-auth.sh $@
   '';
+
+  # Filter secrets from config. Secrets are those of the form { source = <path>; }
+  secrets = lib.attrsets.filterAttrs (k: v: builtins.isAttrs v) cfg.config;
+
+  nonSecrets = (lib.attrsets.filterAttrs (k: v: !(builtins.isAttrs v)) cfg.config);
+
+  configWithSecretsIncludes =
+    nonSecrets
+    // (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets);
 in
 {
   options.shb.home-assistant = {
@@ -41,6 +51,41 @@ in
       default = null;
     };
 
+    config = lib.mkOption {
+      description = "See all available settings at https://www.home-assistant.io/docs/configuration/basic/";
+      type = lib.types.submodule {
+        freeformType = lib.types.attrsOf lib.types.str;
+        options = {
+          name = lib.mkOption {
+            type = lib.types.oneOf [ lib.types.str shblib.secretFileType ];
+            description = "Name of the Home Assistant instance.";
+          };
+          country = lib.mkOption {
+            type = lib.types.oneOf [ lib.types.str shblib.secretFileType ];
+            description = "Two letter country code where this instance is located.";
+          };
+          latitude = lib.mkOption {
+            type = lib.types.oneOf [ lib.types.str shblib.secretFileType ];
+            description = "Latitude where this instance is located.";
+          };
+          longitude = lib.mkOption {
+            type = lib.types.oneOf [ lib.types.str shblib.secretFileType ];
+            description = "Longitude where this instance is located.";
+          };
+          time_zone = lib.mkOption {
+            type = lib.types.oneOf [ lib.types.str shblib.secretFileType ];
+            description = "Timezone of this instance.";
+            example = "America/Los_Angeles";
+          };
+          unit_system = lib.mkOption {
+            type = lib.types.oneOf [ lib.types.str (lib.types.enum [ "metric" "us_customary" ]) ];
+            description = "Timezone of this instance.";
+            example = "America/Los_Angeles";
+          };
+        };
+      };
+    };
+
     ldap = lib.mkOption {
       description = ''
         LDAP Integration App. [Manual](https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_auth_ldap.html)
@@ -91,12 +136,6 @@ in
       };
     };
 
-    sopsFile = lib.mkOption {
-      type = lib.types.path;
-      description = "Sops file location";
-      example = "secrets/homeassistant.yaml";
-    };
-
     backupCfg = lib.mkOption {
       type = lib.types.anything;
       description = "Backup configuration for home-assistant";
@@ -144,14 +183,8 @@ in
           trusted_proxies = "127.0.0.1";
         };
         logger.default = "info";
-        homeassistant = {
+        homeassistant = configWithSecretsIncludes // {
           external_url = "https://${cfg.subdomain}.${cfg.domain}";
-          name = "!secret name";
-          country = "!secret country";
-          latitude = "!secret latitude_home";
-          longitude = "!secret longitude_home";
-          time_zone = "!secret time_zone";
-          unit_system = "metric";
           auth_providers =
             (lib.optionals (!cfg.ldap.enable || cfg.ldap.keepDefaultAuth) [
               {
@@ -256,23 +289,18 @@ in
             }
           }
         '';
-        storage = "${config.services.home-assistant.configDir}/.storage";
-        file = "${storage}/onboarding";
+        storage = "${config.services.home-assistant.configDir}";
+        file = "${storage}/.storage/onboarding";
       in
         ''
           if ! -f ${file}; then
             mkdir -p ${storage} && cp ${onboarding} ${file}
           fi
-        '');
-
-    sops.secrets."home-assistant" = {
-      inherit (cfg) sopsFile;
-      mode = "0440";
-      owner = "hass";
-      group = "hass";
-      path = "${config.services.home-assistant.configDir}/secrets.yaml";
-      restartUnits = [ "home-assistant.service" ];
-    };
+        '' + shblib.replaceSecrets {
+          userConfig = cfg.config;
+          resultPath = "${config.services.home-assistant.configDir}/secrets.yaml";
+          generator = lib.generators.toYAML {};
+        });
 
     systemd.tmpfiles.rules = [
       "f ${config.services.home-assistant.configDir}/automations.yaml 0755 hass hass"
diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix
index 148418d..510bb51 100644
--- a/modules/services/jellyfin.nix
+++ b/modules/services/jellyfin.nix
@@ -348,19 +348,33 @@ in
           </BrandingOptions>
         '';
       in
-        shblib.template ldapConfig "/var/lib/jellyfin/plugins/configurations/LDAP-Auth.xml" {
-          "%LDAP_PASSWORD%" = "$(cat ${cfg.ldapPasswordFile})";
+        shblib.replaceSecretsScript {
+          file = ldapConfig;
+          resultPath = "/var/lib/jellyfin/plugins/configurations/LDAP-Auth.xml";
+          userConfig = {
+            "%LDAP_PASSWORD%" = "$(cat ${cfg.ldapPasswordFile})";
+          };
         }
-        + shblib.template ssoConfig "/var/lib/jellyfin/plugins/configurations/SSO-Auth.xml" {
-          "%SSO_SECRET%" = "$(cat ${cfg.ssoSecretFile})";
+        + shblib.replaceSecretsScript {
+          file = ssoConfig;
+          resultPath = "/var/lib/jellyfin/plugins/configurations/SSO-Auth.xml";
+          userConfig = {
+            "%SSO_SECRET%" = "$(cat ${cfg.ssoSecretFile})";
+          };
         }
-        + shblib.template brandingConfig "/var/lib/jellyfin/config/branding.xml" {"%a%" = "%a%";};
+        + shblib.replaceSecretsScript {
+          file = brandingConfig;
+          resultPath = "/var/lib/jellyfin/config/branding.xml";
+          userConfig = {
+            "%a%" = "%a%";
+          };
+        };
 
     shb.authelia.oidcClients = [
       {
         id = cfg.oidcClientID;
         description = "Jellyfin";
-        secretFile = cfg.ssoSecretFile;
+        secret.source = cfg.ssoSecretFile;
         public = false;
         authorization_policy = "one_factor";
         redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.oidcProvider}" ];
diff --git a/modules/services/nextcloud-server.nix b/modules/services/nextcloud-server.nix
index f3c7e65..e0a4f2a 100644
--- a/modules/services/nextcloud-server.nix
+++ b/modules/services/nextcloud-server.nix
@@ -829,7 +829,7 @@ in
         {
           id = cfg.apps.sso.clientID;
           description = "Nextcloud";
-          secretFile = cfg.apps.sso.secretFileForAuthelia;
+          secret.source = cfg.apps.sso.secretFileForAuthelia;
           public = "false";
           authorization_policy = cfg.apps.sso.authorization_policy;
           redirect_uris = [ "${protocol}://${fqdnWithPort}/apps/oidc_login/oidc" ];
diff --git a/modules/services/vaultwarden.nix b/modules/services/vaultwarden.nix
index 3430960..4405097 100644
--- a/modules/services/vaultwarden.nix
+++ b/modules/services/vaultwarden.nix
@@ -148,16 +148,15 @@ in
       "f /var/lib/bitwarden_rs/vaultwarden.env 0640 vaultwarden vaultwarden"
     ];
     systemd.services.vaultwarden.preStart =
-      let
-        envFile = pkgs.writeText "vaultwarden.env" ''
-        DATABASE_URL=postgresql://vaultwarden:%DB_PASSWORD%@127.0.0.1:5432/vaultwarden
-        SMTP_PASSWORD=%SMTP_PASSWORD%
-        '';
-      in
-        shblib.template envFile "/var/lib/bitwarden_rs/vaultwarden.env" {
-          "%DB_PASSWORD%" = "$(cat ${cfg.databasePasswordFile})";
-          "%SMTP_PASSWORD%" = "$(cat ${cfg.smtp.passwordFile})";
+      shblib.replaceSecrets {
+        userConfig = {
+          DATABASE_URL.source = cfg.databasePasswordFile;
+          DATABASE_URL.transform = v: "postgresql://vaultwarden:${v}@127.0.0.1:5432/vaultwarden";
+          SMTP_PASSWORD.source = cfg.smtp.passwordFile;
         };
+        resultPath = "/var/lib/bitwarden_rs/vaultwarden.env";
+        generator = v: lib.generators.toINIWithGlobalSection {} { globalSection = v; };
+      };
 
     shb.nginx.autheliaProtect = [
       {
diff --git a/test/modules/lib.nix b/test/modules/lib.nix
new file mode 100644
index 0000000..a34dcf5
--- /dev/null
+++ b/test/modules/lib.nix
@@ -0,0 +1,110 @@
+{ pkgs, lib, ... }:
+let
+  shblib = pkgs.callPackage ../../lib {};
+in
+{
+  # Tests that withReplacements can:
+  # - recurse in attrs and lists
+  # - .source field is understood
+  # - .transform field is understood
+  # - if .source field is found, ignores other fields
+  testLibWithReplacements = {
+    expected =
+      let
+        item = root: {
+          a = "A";
+          b = "%SECRET_${root}B%";
+          c = "%SECRET_${root}C%";
+        };
+      in
+        (item "") // {
+          nestedAttr = item "NESTEDATTR_";
+          nestedList = [ (item "NESTEDLIST_0_") ];
+          doubleNestedList = [ { n = (item "DOUBLENESTEDLIST_0_N_"); } ];
+        };
+    expr =
+      let
+        item = {
+          a = "A";
+          b.source = "/path/B";
+          b.transform = null;
+          c.source = "/path/C";
+          c.transform = v: "prefix-${v}-suffix";
+          c.other = "other";
+        };
+      in
+        shblib.withReplacements (
+          item // {
+            nestedAttr = item;
+            nestedList = [ item ];
+            doubleNestedList = [ { n = item; } ];
+          }
+        );
+  };
+
+  testLibWithReplacementsRootList = {
+    expected =
+      let
+        item = root: {
+          a = "A";
+          b = "%SECRET_${root}B%";
+          c = "%SECRET_${root}C%";
+        };
+      in
+        [
+          (item "0_")
+          (item "1_")
+          [ (item "2_0_") ]
+          [ { n = (item "3_0_N_"); } ]
+        ];
+    expr =
+      let
+        item = {
+          a = "A";
+          b.source = "/path/B";
+          b.transform = null;
+          c.source = "/path/C";
+          c.transform = v: "prefix-${v}-suffix";
+          c.other = "other";
+        };
+      in
+        shblib.withReplacements [
+          item
+          item
+          [ item ]
+          [ { n = item; } ]
+        ];
+  };
+
+  testLibGetReplacements = {
+    expected =
+      let
+        secrets = root: {
+          "%SECRET_${root}B%" = "$(cat /path/B)";
+          "%SECRET_${root}C%" = "prefix-$(cat /path/C)-suffix";
+        };
+      in
+        (secrets "") //
+        (secrets "NESTEDATTR_") //
+        (secrets "NESTEDLIST_0_") //
+        (secrets "DOUBLENESTEDLIST_0_N_");
+    expr =
+      let
+        item = {
+          a = "A";
+          b.source = "/path/B";
+          b.transform = null;
+          c.source = "/path/C";
+          c.transform = v: "prefix-${v}-suffix";
+          c.other = "other";
+        };
+      in
+        shblib.getReplacements (
+          item // {
+            nestedAttr = item;
+            nestedList = [ item ];
+            doubleNestedList = [ { n = item; } ];
+          }
+        );
+  };
+}
diff --git a/test/vm/authelia.nix b/test/vm/authelia.nix
index da1bf98..45d3cc6 100644
--- a/test/vm/authelia.nix
+++ b/test/vm/authelia.nix
@@ -10,7 +10,6 @@ in
       imports = [
         {
           options = {
-            shb.ssl.enable = lib.mkEnableOption "ssl";
             shb.backup = lib.mkOption { type = lib.types.anything; };
           };
         }
@@ -49,7 +48,7 @@ in
           {
             id = "client1";
             description = "My Client 1";
-            secretFile = pkgs.writeText "secret" "mysecuresecret";
+            secret.source = pkgs.writeText "secret" "mysecuresecret";
             public = false;
             authorization_policy = "one_factor";
             redirect_uris = [ "http://client1.machine/redirect" ];
@@ -57,7 +56,7 @@ in
           {
             id = "client2";
             description = "My Client 2";
-            secretFile = pkgs.writeText "secret" "myothersecret";
+            secret.source = pkgs.writeText "secret" "myothersecret";
             public = false;
             authorization_policy = "one_factor";
             redirect_uris = [ "http://client2.machine/redirect" ];
diff --git a/test/vm/lib.nix b/test/vm/lib.nix
new file mode 100644
index 0000000..5a79c0a
--- /dev/null
+++ b/test/vm/lib.nix
@@ -0,0 +1,82 @@
+{ pkgs, lib, ... }:
+let
+  shblib = pkgs.callPackage ../../lib {};
+in
+{
+  template =
+    let
+      aSecret = pkgs.writeText "a-secret.txt" "Secret of A";
+      bSecret = pkgs.writeText "b-secret.txt" "Secret of B";
+      userConfig = {
+        a.a.source = aSecret;
+        b.source = bSecret;
+        b.transform = v: "prefix-${v}-suffix";
+        c = "not secret C";
+        d.d = "not secret D";
+      };
+
+      wantedConfig = {
+        a.a = "Secret of A";
+        b = "prefix-Secret of B-suffix";
+        c = "not secret C";
+        d.d = "not secret D";
+      };
+
+      configWithTemplates = shblib.withReplacements userConfig;
+
+      nonSecretConfigFile = pkgs.writeText "config.yaml.template" (lib.generators.toJSON {} configWithTemplates);
+
+      replacements = shblib.getReplacements userConfig;
+
+      replaceInTemplate = shblib.replaceSecretsScript {
+        file = nonSecretConfigFile;
+        resultPath = "/var/lib/config.yaml";
+        inherit replacements;
+      };
+
+      replaceInTemplate2 = shblib.replaceSecrets {
+        inherit userConfig;
+        resultPath = "/var/lib/config2.yaml";
+        generator = lib.generators.toJSON {};
+      };
+    in
+      pkgs.nixosTest {
+        name = "lib-template";
+        nodes.machine = { config, pkgs, ... }:
+          {
+            imports = [
+              {
+                options = {
+                  libtest.config = lib.mkOption {
+                    type = lib.types.attrsOf (lib.types.oneOf [ lib.types.str shblib.secretFileType ]);
+                  };
+                };
+              }
+            ];
+
+            system.activationScripts = {
+              libtest = replaceInTemplate;
+              libtest2 = replaceInTemplate2;
+            };
+          };
+
+        testScript = { nodes, ... }: ''
+        import json
+        start_all()
+
+        wantedConfig = json.loads('${lib.generators.toJSON {} wantedConfig}')
+        gotConfig = json.loads(machine.succeed("cat /var/lib/config.yaml"))
+        gotConfig2 = json.loads(machine.succeed("cat /var/lib/config2.yaml"))
+
+        # For debugging purpose
+        print(machine.succeed("cat ${pkgs.writeText "replaceInTemplate" replaceInTemplate}"))
+        print(machine.succeed("cat ${pkgs.writeText "replaceInTemplate2" replaceInTemplate2}"))
+
+        if wantedConfig != gotConfig:
+          raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
+
+        if wantedConfig != gotConfig2:
+          raise Exception("\nwantedConfig:  {}\n!= gotConfig2: {}".format(wantedConfig, gotConfig))
+        '';
+      };
+}

From 53d46cda56a0be3b267a26a623bc023eebe0dff4 Mon Sep 17 00:00:00 2001
From: ibizaman <ibizapeanut@gmail.com>
Date: Thu, 29 Feb 2024 20:34:09 -0800
Subject: [PATCH 12/18] update home-assistant demo

---
 demo/homeassistant/README.md    | 11 +++-------
 demo/homeassistant/flake.lock   | 38 ++++++++++++++++-----------------
 demo/homeassistant/flake.nix    | 35 ++++++++++++++++++++++++++++++
 demo/homeassistant/secrets.yaml | 10 ++++++---
 4 files changed, 64 insertions(+), 30 deletions(-)

diff --git a/demo/homeassistant/README.md b/demo/homeassistant/README.md
index 348f5b1..7b5b835 100644
--- a/demo/homeassistant/README.md
+++ b/demo/homeassistant/README.md
@@ -230,21 +230,16 @@ SOPS_AGE_KEY_FILE=keys.txt nix run --impure nixpkgs#sops -- \
 The `secrets.yaml` file must follow the format:
 
 ```yaml
-home-assistant: |
-    name: "My Instance"
+home-assistant:
     country: "US"
-    latitude_home: "0.100"
-    longitude_home: "-0.100"
+    latitude: "0.100"
+    longitude: "-0.100"
     time_zone: "America/Los_Angeles"
-    unit_system: "metric"
 lldap:
     user_password: XXX...
     jwt_secret: YYY...
 ```
 
-> Important: the value of the `home-assistant` field is a string that looks like yaml. Do _not_
-> remove the pipe (|) sign.
-
 You can generate random secrets with:
 
 ```bash
diff --git a/demo/homeassistant/flake.lock b/demo/homeassistant/flake.lock
index 6bf946d..fcd21e0 100644
--- a/demo/homeassistant/flake.lock
+++ b/demo/homeassistant/flake.lock
@@ -5,11 +5,11 @@
         "systems": "systems"
       },
       "locked": {
-        "lastModified": 1705309234,
-        "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
+        "lastModified": 1709126324,
+        "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
+        "rev": "d465f4819400de7c8d874d50b982301f28a84605",
         "type": "github"
       },
       "original": {
@@ -35,11 +35,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1707092692,
-        "narHash": "sha256-ZbHsm+mGk/izkWtT4xwwqz38fdlwu7nUUKXTOmm4SyE=",
+        "lastModified": 1709150264,
+        "narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "faf912b086576fd1a15fca610166c98d47bc667e",
+        "rev": "9099616b93301d5cf84274b184a3a5ec69e94e08",
         "type": "github"
       },
       "original": {
@@ -51,27 +51,27 @@
     },
     "nixpkgs-stable": {
       "locked": {
-        "lastModified": 1705957679,
-        "narHash": "sha256-Q8LJaVZGJ9wo33wBafvZSzapYsjOaNjP/pOnSiKVGHY=",
+        "lastModified": 1708819810,
+        "narHash": "sha256-1KosU+ZFXf31GPeCBNxobZWMgHsSOJcrSFA6F2jhzdE=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "9a333eaa80901efe01df07eade2c16d183761fa3",
+        "rev": "89a2a12e6c8c6a56c72eb3589982c8e2f89c70ea",
         "type": "github"
       },
       "original": {
         "owner": "NixOS",
-        "ref": "release-23.05",
+        "ref": "release-23.11",
         "repo": "nixpkgs",
         "type": "github"
       }
     },
     "nixpkgs_2": {
       "locked": {
-        "lastModified": 1706925685,
-        "narHash": "sha256-hVInjWMmgH4yZgA4ZtbgJM1qEAel72SYhP5nOWX4UIM=",
+        "lastModified": 1708751719,
+        "narHash": "sha256-0uWOKSpXJXmXswOvDM5Vk3blB74apFB6rNGWV5IjoN0=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "79a13f1437e149dc7be2d1290c74d378dad60814",
+        "rev": "f63ce824cd2f036216eb5f637dfef31e1a03ee89",
         "type": "github"
       },
       "original": {
@@ -111,11 +111,11 @@
         "sops-nix": "sops-nix"
       },
       "locked": {
-        "lastModified": 1707374005,
-        "narHash": "sha256-W3p8hBLUdlHAG7yxT250jImnFmXe83tN119/jRiBYdo=",
+        "lastModified": 1709267447,
+        "narHash": "sha256-5Q467FhpS18L/+5iB3wsWaR9tBqdzNt0fpdkZJNqNxc=",
         "owner": "ibizaman",
         "repo": "selfhostblocks",
-        "rev": "7d0276e9f2509bc6f175358c318374fedfc64422",
+        "rev": "fa206d0e1515fb0e49393e7ada6d7e5c6ec1df58",
         "type": "github"
       },
       "original": {
@@ -130,11 +130,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1707015547,
-        "narHash": "sha256-YZr0OrqWPdbwBhxpBu69D32ngJZw8AMgZtJeaJn0e94=",
+        "lastModified": 1708987867,
+        "narHash": "sha256-k2lDaDWNTU5sBVHanYzjDKVDmk29RHIgdbbXu5sdzBA=",
         "owner": "Mic92",
         "repo": "sops-nix",
-        "rev": "23f61b897c00b66855074db471ba016e0cda20dd",
+        "rev": "a1c8de14f60924fafe13aea66b46157f0150f4cf",
         "type": "github"
       },
       "original": {
diff --git a/demo/homeassistant/flake.nix b/demo/homeassistant/flake.nix
index bc20515..e8d5eba 100644
--- a/demo/homeassistant/flake.nix
+++ b/demo/homeassistant/flake.nix
@@ -18,7 +18,42 @@
           enable = true;
           domain = "example.com";
           subdomain = "ha";
+          config = {
+            name = "SHB Home Assistant";
+            country.source = config.sops.secrets."home-assistant/country".path;
+            latitude.source = config.sops.secrets."home-assistant/latitude".path;
+            longitude.source = config.sops.secrets."home-assistant/longitude".path;
+            time_zone.source = config.sops.secrets."home-assistant/time_zone".path;
+            unit_system = "metric";
+          };
+        };
+        sops.secrets."home-assistant/country" = {
           sopsFile = ./secrets.yaml;
+          mode = "0440";
+          owner = "hass";
+          group = "hass";
+          restartUnits = [ "home-assistant.service" ];
+        };
+        sops.secrets."home-assistant/latitude" = {
+          sopsFile = ./secrets.yaml;
+          mode = "0440";
+          owner = "hass";
+          group = "hass";
+          restartUnits = [ "home-assistant.service" ];
+        };
+        sops.secrets."home-assistant/longitude" = {
+          sopsFile = ./secrets.yaml;
+          mode = "0440";
+          owner = "hass";
+          group = "hass";
+          restartUnits = [ "home-assistant.service" ];
+        };
+        sops.secrets."home-assistant/time_zone" = {
+          sopsFile = ./secrets.yaml;
+          mode = "0440";
+          owner = "hass";
+          group = "hass";
+          restartUnits = [ "home-assistant.service" ];
         };
 
         nixpkgs.config.permittedInsecurePackages = [
diff --git a/demo/homeassistant/secrets.yaml b/demo/homeassistant/secrets.yaml
index e0e488c..5b1900d 100644
--- a/demo/homeassistant/secrets.yaml
+++ b/demo/homeassistant/secrets.yaml
@@ -1,4 +1,8 @@
-home-assistant: ENC[AES256_GCM,data:acEXqx3bdQp0zB5FnHCBsic/kgu2L8Q6h/fsfrLmdk7SOfzEibPpPLCCv8eYmh4D5VuIAsq/PeJ3k+uqWGbTrJt7EIcxt0kYTLRuWZRG8YJH1+HCxoKcO/mx9bwbRd3LtXiVscgP9zIZLoLPK2XieFKOeg==,iv:dJ7FUkquMI4g4K2Nnv3kFFQk/va2QgwfgGoWif5f2tU=,tag:6LIBt9whdRPVsoF1RY3Pew==,type:str]
+home-assistant:
+    country: ENC[AES256_GCM,data:2Ng=,iv:/VMB6yi3e8piAx8DzLGGhLsozxWUWX2R7NcmACFng8Q=,tag:Tx0Iy1AnLmPrnYu7XtbesA==,type:str]
+    latitude: ENC[AES256_GCM,data:p/O1HW4=,iv:CRgL4wcM3gMNu/OAHVoQuLcRD9J3SbkxsjvobiabQ0g=,tag:uIo5Rv7geOtVcarp4Qkqww==,type:str]
+    longitude: ENC[AES256_GCM,data:sVyww6F7,iv:9EZYXSkv+rhD77lqmC+c8i+wf46KPYloVoK+ok3bWYY=,tag:c+lmtcGvULtMdu9ZTDewjA==,type:str]
+    time_zone: ENC[AES256_GCM,data:JKXdsQZrtB1B77klxuemw1tZbg==,iv:nItJfpwp2XWmBHbohrjNMWQ8TpL2Xsv22UujZRgDscw=,tag:wrHbA1yycutUUn79F9wy6Q==,type:str]
 lldap:
     user_password: ENC[AES256_GCM,data:JrFraqFSqAhRVjB5fagIoB864aejt24q+qqWeu8ySC0=,iv:RS7VS+9tsSknn9SwpfyYVi41m3lN4SkZ4CSwrzH/Eso=,tag:5L7fx6/KhDtjHPruwac/sw==,type:str]
     jwt_secret: ENC[AES256_GCM,data:W1T/QoxuzMD+2AL7sP5KkMcC+GvFdd4kfd70rHLnQD+jWNs9G0igkC/BxxgbIfnSASwtSnBaaiU6/pxLFOcUVh0Nyd0Zmb/KTbagpUvSl//AZnTt/WKF9Q/8sqKzsGv0QdMyZKWi4cxiEILcTbxOsgwriFGgOJ1k5N8JEif15ig=,iv:rHlRt6nWMz8rVmU0aKH6VWWVXunOfJcDvZOxgWbK1FI=,tag:qC6N61rE8CfPSXrsEqFoIQ==,type:str]
@@ -26,8 +30,8 @@ sops:
             VlJpS1BYd2UrZU1mZTEwU1BYODhqM2sKvQnFV8xsy1tEmYZu4izBYb7XQqTPOLTL
             bRkU6n17uiyXNbiXDAbX0Png/XmVG96/+Zl38BBXPQvARX8c2tzq6w==
             -----END AGE ENCRYPTED FILE-----
-    lastmodified: "2024-01-23T00:46:58Z"
-    mac: ENC[AES256_GCM,data:kBkUCStabQ32JK/UDPATgOz3HoI/dVkNLsl6uEhHk8ODbF+ZBg6BDEaxtMFFh0bV+71klAmF0KsL/kHKiHlbNuoNWOxwbsANGeL8xtV6JCU58zTF0nfgAP/3KJYveridgylRRZS5hYl5Mg+z6Zdgw+43r3Iiizf86BZVc5OaDyY=,iv:ZXWLXQUrVIwYCCVnXI0jTf5paOWNuujG/Pw+Nf/M34A=,tag:+P/UJqBI3prcxEUO4Zqu/A==,type:str]
+    lastmodified: "2024-02-12T05:07:51Z"
+    mac: ENC[AES256_GCM,data:MOmvK0g6Wj+fND154QUhmXujsDOKMO5CRRckru+eDRPeHcJZUnI/jjolcI8y+LEdhUVf0Ln8E38GSxZT/8EW3CfCNkOUikGFdfxuQ2uzNp/1wMvNaF988lrXMBfQ7Il18AiYVK0QhGReGXJa6wBVUb2Qfrg41WC65UvQtMOByqI=,iv:Rscvq1l7YgNapC0NkabQHBzirzsPEr8ykAQqx+qGoi0=,tag:ud+K72bnUV1hnsjcewNrsw==,type:str]
     pgp: []
     unencrypted_suffix: _unencrypted
     version: 3.8.1

From d0d94e61c87771e4afcfcf095cd8e626eb6fb0ef Mon Sep 17 00:00:00 2001
From: ibizaman <ibizapeanut@gmail.com>
Date: Thu, 29 Feb 2024 20:39:51 -0800
Subject: [PATCH 13/18] use better defaults for nextcloud preview app

---
 modules/services/nextcloud-server.nix         | 33 +++++++++++++++++++
 .../services/nextcloud-server/docs/default.md |  9 +++++
 2 files changed, 42 insertions(+)

diff --git a/modules/services/nextcloud-server.nix b/modules/services/nextcloud-server.nix
index e0a4f2a..3c2943e 100644
--- a/modules/services/nextcloud-server.nix
+++ b/modules/services/nextcloud-server.nix
@@ -239,6 +239,27 @@ in
               options = {
                 enable = lib.mkEnableOption "Nextcloud Preview Generator App";
 
+                recommendedSettings = lib.mkOption {
+                  type = lib.types.bool;
+                  description = ''
+                    Better defaults than the defaults. Taken from [this article](http://web.archive.org/web/20200513043150/https://ownyourbits.com/2019/06/29/understanding-and-improving-nextcloud-previews/).
+
+                    Sets the following options:
+
+                    ```
+                    nextcloud-occ config:app:set previewgenerator squareSizes --value="32 256"
+                    nextcloud-occ config:app:set previewgenerator widthSizes  --value="256 384"
+                    nextcloud-occ config:app:set previewgenerator heightSizes --value="256"
+                    nextcloud-occ config:system:set preview_max_x --value 2048
+                    nextcloud-occ config:system:set preview_max_y --value 2048
+                    nextcloud-occ config:system:set jpeg_quality --value 60
+                    nextcloud-occ config:app:set preview jpeg_quality --value="60"
+                    ```
+                  '';
+                  default = true;
+                  example = false;
+                };
+
                 debug = lib.mkOption {
                   type = lib.types.bool;
                   description = "Enable more verbose logging.";
@@ -649,6 +670,18 @@ in
         inherit ((nextcloudApps cfg.version)) previewgenerator;
       };
 
+      # Values taken from
+      # http://web.archive.org/web/20200513043150/https://ownyourbits.com/2019/06/29/understanding-and-improving-nextcloud-previews/
+      systemd.services.nextcloud-setup.script = lib.mkIf cfg.apps.previewgenerator.recommendedSettings ''
+        ${occ} config:app:set previewgenerator squareSizes --value="32 256"
+        ${occ} config:app:set previewgenerator widthSizes  --value="256 384"
+        ${occ} config:app:set previewgenerator heightSizes --value="256"
+        ${occ} config:system:set preview_max_x --value 2048
+        ${occ} config:system:set preview_max_y --value 2048
+        ${occ} config:system:set jpeg_quality --value 60
+        ${occ} config:app:set preview jpeg_quality --value="60"
+      '';
+
       # Configured as defined in https://github.com/nextcloud/previewgenerator
       systemd.timers.nextcloud-cron-previewgenerator = {
         wantedBy = [ "timers.target" ];
diff --git a/modules/services/nextcloud-server/docs/default.md b/modules/services/nextcloud-server/docs/default.md
index faf7191..3f293ed 100644
--- a/modules/services/nextcloud-server/docs/default.md
+++ b/modules/services/nextcloud-server/docs/default.md
@@ -281,6 +281,15 @@ Note that you still need to generate the previews for any pre-existing files wit
 nextcloud-occ -vvv preview:generate-all
 ```
 
+The default settings generates all possible sizes which is a waste since most are not used. SHB will
+change the generation settings to optimize disk space and CPU usage as outlined in [this
+article](http://web.archive.org/web/20200513043150/https://ownyourbits.com/2019/06/29/understanding-and-improving-nextcloud-previews/).
+You can opt-out with:
+
+```nix
+shb.nextcloud.apps.previewgenerator.recommendedSettings = false;
+```
+
 ### Enable OnlyOffice App {#services-nextcloud-server-usage-onlyoffice}
 
 The following snippet installs and enables the [Only

From 937902a7f030b1d9647682e28a9a6cfdf3ff5fac Mon Sep 17 00:00:00 2001
From: ibizaman <ibizapeanut@gmail.com>
Date: Thu, 29 Feb 2024 20:41:24 -0800
Subject: [PATCH 14/18] add section about nextcloud appdata folder

---
 .../services/nextcloud-server/docs/default.md | 25 +++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/modules/services/nextcloud-server/docs/default.md b/modules/services/nextcloud-server/docs/default.md
index 3f293ed..5a28cd2 100644
--- a/modules/services/nextcloud-server/docs/default.md
+++ b/modules/services/nextcloud-server/docs/default.md
@@ -331,6 +331,31 @@ See [my blog
 post](http://blog.tiserbox.com/posts/2023-08-12-what%27s-up-with-nextcloud-webdav-slowness.html) for
 how to look at the traces.
 
+### Appdata Location {#services-nextcloud-server-server-usage-appdata}
+
+The appdata folder is a special folder located under the `shb.nextcloud.dataDir` directory. It is
+named `appdata_<instanceid>` with the Nextcloud's instance ID as a suffix. You can find your current
+instance ID with `nextcloud-occ config:system:get instanceid`. In there, you will find one subfolder
+for every installed app that needs to store files.
+
+For performance reasons, it is recommended to store this folder on a fast drive that is optimized
+for randomized read and write access. The best would be either an SSD or an NVMe drive.
+
+If you intentionally put Nextcloud's `shb.nextcloud.dataDir` folder on a HDD with spinning disks,
+for example because they offer more disk space, then the appdata folder is also located on spinning
+drives. You are thus faced with a conundrum. The only way to solve this is to bind mount a folder
+from an SSD over the appdata folder. SHB does not provide (yet?) a declarative way to setup this but
+this command should be enough:
+
+```bash
+mount /dev/sdd /srv/sdd
+mkdir -p /srv/sdd/appdata_nextcloud
+mount --bind /srv/sdd/appdata_nextcloud /var/lib/nextcloud/data/appdata_ocxvky2f5ix7
+```
+
+Note that you can re-generate a new appdata folder by issuing the command `occ config:system:delete
+instanceid`.
+
 ## Demo {#services-nextcloud-server-demo}
 
 Head over to the [Nextcloud demo](demo-nextcloud-server.html) for a demo that installs Nextcloud with or

From 8c2373430d9a42f0ac27a80daab217008e91b38b Mon Sep 17 00:00:00 2001
From: ibizaman <ibizapeanut@gmail.com>
Date: Thu, 29 Feb 2024 21:52:49 -0800
Subject: [PATCH 15/18] fix jellyfin and nextcloud-server after changes to lib

---
 modules/services/jellyfin.nix         | 6 +++---
 modules/services/nextcloud-server.nix | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix
index 510bb51..1571f12 100644
--- a/modules/services/jellyfin.nix
+++ b/modules/services/jellyfin.nix
@@ -351,21 +351,21 @@ in
         shblib.replaceSecretsScript {
           file = ldapConfig;
           resultPath = "/var/lib/jellyfin/plugins/configurations/LDAP-Auth.xml";
-          userConfig = {
+          replacements = {
             "%LDAP_PASSWORD%" = "$(cat ${cfg.ldapPasswordFile})";
           };
         }
         + shblib.replaceSecretsScript {
           file = ssoConfig;
           resultPath = "/var/lib/jellyfin/plugins/configurations/SSO-Auth.xml";
-          userConfig = {
+          replacements = {
             "%SSO_SECRET%" = "$(cat ${cfg.ssoSecretFile})";
           };
         }
         + shblib.replaceSecretsScript {
           file = brandingConfig;
           resultPath = "/var/lib/jellyfin/config/branding.xml";
-          userConfig = {
+          replacements = {
             "%a%" = "%a%";
           };
         };
diff --git a/modules/services/nextcloud-server.nix b/modules/services/nextcloud-server.nix
index 3c2943e..b0199b3 100644
--- a/modules/services/nextcloud-server.nix
+++ b/modules/services/nextcloud-server.nix
@@ -863,7 +863,7 @@ in
           id = cfg.apps.sso.clientID;
           description = "Nextcloud";
           secret.source = cfg.apps.sso.secretFileForAuthelia;
-          public = "false";
+          public = false;
           authorization_policy = cfg.apps.sso.authorization_policy;
           redirect_uris = [ "${protocol}://${fqdnWithPort}/apps/oidc_login/oidc" ];
           scopes = [

From 046ae67083046cfeb9e65b98e569446fd2f0679b Mon Sep 17 00:00:00 2001
From: ibizaman <ibizapeanut@gmail.com>
Date: Thu, 29 Feb 2024 21:54:45 -0800
Subject: [PATCH 16/18] optionally make nextcloud systemd services depend on
 mount point

---
 modules/services/nextcloud-server.nix           | 17 ++++++++++++++++-
 .../services/nextcloud-server/docs/default.md   | 12 ++++++++++++
 2 files changed, 28 insertions(+), 1 deletion(-)

diff --git a/modules/services/nextcloud-server.nix b/modules/services/nextcloud-server.nix
index b0199b3..197f251 100644
--- a/modules/services/nextcloud-server.nix
+++ b/modules/services/nextcloud-server.nix
@@ -82,6 +82,13 @@ in
       default = "/var/lib/nextcloud";
     };
 
+    mountPointServices = lib.mkOption {
+      description = "If given, all the systemd services and timers will depend on the specified mount point systemd services.";
+      type = lib.types.listOf lib.types.str;
+      default = [];
+      example = lib.literalExpression ''["var.mount"]'';
+    };
+
     adminUser = lib.mkOption {
       type = lib.types.str;
       description = "Username of the initial admin user.";
@@ -616,10 +623,17 @@ in
       systemd.services.phpfpm-nextcloud.preStart = ''
       mkdir -p /var/log/xdebug; chown -R nextcloud: /var/log/xdebug
       '';
+      systemd.services.phpfpm-nextcloud.requires = cfg.mountPointServices;
+      systemd.services.phpfpm-nextcloud.after = cfg.mountPointServices;
 
       systemd.services.nextcloud-cron.path = [
         pkgs.perl
       ];
+      systemd.timers.nextcloud-cron.requires = cfg.mountPointServices;
+      systemd.timers.nextcloud-cron.after = cfg.mountPointServices;
+
+      systemd.services.nextcloud-setup.requires = cfg.mountPointServices;
+      systemd.services.nextcloud-setup.after = cfg.mountPointServices;
 
       # Sets up backup for Nextcloud.
       shb.backup.instances.nextcloud = {
@@ -685,7 +699,8 @@ in
       # Configured as defined in https://github.com/nextcloud/previewgenerator
       systemd.timers.nextcloud-cron-previewgenerator = {
         wantedBy = [ "timers.target" ];
-        after = [ "nextcloud-setup.service" ];
+        requires = cfg.mountPointServices;
+        after = [ "nextcloud-setup.service" ] + cfg.mountPointServices;
         timerConfig.OnBootSec = "10m";
         timerConfig.OnUnitActiveSec = "10m";
         timerConfig.Unit = "nextcloud-cron-previewgenerator.service";
diff --git a/modules/services/nextcloud-server/docs/default.md b/modules/services/nextcloud-server/docs/default.md
index 5a28cd2..d89e2f1 100644
--- a/modules/services/nextcloud-server/docs/default.md
+++ b/modules/services/nextcloud-server/docs/default.md
@@ -83,6 +83,18 @@ shb.nextcloud = {
 
 After deploying, the Nextcloud server will be reachable at `http://nextcloud.example.com`.
 
+### Mount Point  {#services-nextcloud-server-mount-point}
+
+If the `dataDir` exists in a mount point, it is highly recommended to make the various Nextcloud
+services wait on the mount point before starting. Doing that is just a matter of setting the `mountPointServices` option.
+
+Assuming a mount point on `/var`, the configuration would look like so:
+
+```nix
+fileSystems."/var".device = "...";
+shb.nextcloud.mountPointServices = [ "var.mount" ];
+```
+
 ### With LDAP Support {#services-nextcloud-server-usage-ldap}
 
 :::: {.note}

From 97f213a137aaf976d84300a1e0568ffaeb782060 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Sat, 2 Mar 2024 01:01:07 +0000
Subject: [PATCH 17/18] flake.lock: Update
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/9099616b93301d5cf84274b184a3a5ec69e94e08' (2024-02-28)
  → 'github:nixos/nixpkgs/1536926ef5621b09bba54035ae2bb6d806d72ac8' (2024-02-29)
---
 flake.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/flake.lock b/flake.lock
index b709bcb..ab89304 100644
--- a/flake.lock
+++ b/flake.lock
@@ -35,11 +35,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1709150264,
-        "narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=",
+        "lastModified": 1709237383,
+        "narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "9099616b93301d5cf84274b184a3a5ec69e94e08",
+        "rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
         "type": "github"
       },
       "original": {

From e80cc0d3aad5301ec7a57869e5bc2f36c6b0871d Mon Sep 17 00:00:00 2001
From: ibizaman <ibizapeanut@gmail.com>
Date: Sat, 2 Mar 2024 22:58:36 -0800
Subject: [PATCH 18/18] add vm tests for jellyfin and regroup ldap and sso
 options

---
 flake.nix                     |   1 +
 lib/default.nix               |   2 +-
 modules/services/jellyfin.nix | 188 ++++++++++++--------
 test/vm/jellyfin.nix          | 326 ++++++++++++++++++++++++++++++++++
 4 files changed, 439 insertions(+), 78 deletions(-)
 create mode 100644 test/vm/jellyfin.nix

diff --git a/flake.nix b/flake.nix
index f91c79b..2c171bb 100644
--- a/flake.nix
+++ b/flake.nix
@@ -100,6 +100,7 @@
             };
           }
           // (vm_test "authelia" ./test/vm/authelia.nix)
+          // (vm_test "jellyfin" ./test/vm/jellyfin.nix)
           // (vm_test "ldap" ./test/vm/ldap.nix)
           // (vm_test "lib" ./test/vm/lib.nix)
           // (vm_test "postgresql" ./test/vm/postgresql.nix)
diff --git a/lib/default.nix b/lib/default.nix
index c47643f..4846a3e 100644
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -22,9 +22,9 @@ rec {
       ''
       set -euo pipefail
       set -x
+      mkdir -p $(dirname ${templatePath})
       ln -fs ${file} ${templatePath}
       rm -f ${resultPath}
-      ${pkgs.gnused}/bin/sed ${sedPatterns} ${templatePath}
       ${pkgs.gnused}/bin/sed ${sedPatterns} ${templatePath} > ${resultPath}
       '';
 
diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix
index 1571f12..dcc1450 100644
--- a/modules/services/jellyfin.nix
+++ b/modules/services/jellyfin.nix
@@ -30,62 +30,94 @@ in
       default = null;
     };
 
-    ldapHost = lib.mkOption {
-      type = lib.types.str;
-      description = "host serving the LDAP server";
-      example = "127.0.0.1";
+    ldap = lib.mkOption {
+      description = "LDAP configuration.";
+      default = {};
+      type = lib.types.submodule {
+        options = {
+          enable = lib.mkEnableOption "LDAP";
+
+          host = lib.mkOption {
+            type = lib.types.str;
+            description = "Host serving the LDAP server.";
+            example = "127.0.0.1";
+          };
+
+          port = lib.mkOption {
+            type = lib.types.int;
+            description = "Port where the LDAP server is listening.";
+            example = 389;
+          };
+
+          dcdomain = lib.mkOption {
+            type = lib.types.str;
+            description = "DC domain for LDAP.";
+            example = "dc=mydomain,dc=com";
+          };
+
+          userGroup = lib.mkOption {
+            type = lib.types.str;
+            description = "LDAP user group";
+            default = "jellyfin_user";
+          };
+
+          adminGroup = lib.mkOption {
+            type = lib.types.str;
+            description = "LDAP admin group";
+            default = "jellyfin_admin";
+          };
+
+          passwordFile = lib.mkOption {
+            type = lib.types.path;
+            description = "File containing the LDAP admin password.";
+          };
+        };
+      };
     };
 
-    ldapPort = lib.mkOption {
-      type = lib.types.int;
-      description = "port where the LDAP server is listening";
-      example = 389;
-    };
+    sso = lib.mkOption {
+      description = "SSO configuration.";
+      default = {};
+      type = lib.types.submodule {
+        options = {
+          enable = lib.mkEnableOption "SSO";
 
-    dcdomain = lib.mkOption {
-      type = lib.types.str;
-      description = "dc domain for ldap";
-      example = "dc=mydomain,dc=com";
-    };
+          provider = lib.mkOption {
+            type = lib.types.str;
+            description = "OIDC provider name";
+            default = "Authelia";
+          };
 
-    oidcProvider = lib.mkOption {
-      type = lib.types.str;
-      description = "OIDC provider name";
-      default = "Authelia";
-    };
+          endpoint = lib.mkOption {
+            type = lib.types.str;
+            description = "OIDC endpoint for SSO";
+            example = "https://authelia.example.com";
+          };
 
-    authEndpoint = lib.mkOption {
-      type = lib.types.str;
-      description = "OIDC endpoint for SSO";
-      example = "https://authelia.example.com";
-    };
+          clientID = lib.mkOption {
+            type = lib.types.str;
+            description = "Client ID for the OIDC endpoint";
+            default = "jellyfin";
+          };
 
-    oidcClientID = lib.mkOption {
-      type = lib.types.str;
-      description = "Client ID for the OIDC endpoint";
-      default = "jellyfin";
-    };
+          adminUserGroup = lib.mkOption {
+            type = lib.types.str;
+            description = "OIDC admin group";
+            default = "jellyfin_admin";
+          };
 
-    oidcAdminUserGroup = lib.mkOption {
-      type = lib.types.str;
-      description = "OIDC admin group";
-      default = "jellyfin_admin";
-    };
+          userGroup = lib.mkOption {
+            type = lib.types.str;
+            description = "OIDC user group";
+            default = "jellyfin_user";
+          };
 
-    oidcUserGroup = lib.mkOption {
-      type = lib.types.str;
-      description = "OIDC user group";
-      default = "jellyfin_user";
-    };
-
-    ldapPasswordFile = lib.mkOption {
-      type = lib.types.path;
-      description = "File containing the LDAP admin password.";
-    };
-
-    ssoSecretFile = lib.mkOption {
-      type = lib.types.path;
-      description = "File containing the SSO shared secret.";
+          secretFile = lib.mkOption {
+            type = lib.types.path;
+            description = "File containing the OIDC shared secret.";
+          };
+        };
+      };
     };
   };
 
@@ -107,6 +139,8 @@ in
       };
     };
 
+    services.nginx.enable = true;
+
     # Take advice from https://jellyfin.org/docs/general/networking/nginx/ and https://nixos.wiki/wiki/Plex
     services.nginx.virtualHosts."${fqdn}" = {
       forceSSL = !(isNull cfg.ssl);
@@ -238,17 +272,17 @@ in
         ldapConfig = pkgs.writeText "LDAP-Auth.xml" ''
           <?xml version="1.0" encoding="utf-8"?>
           <PluginConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-            <LdapServer>${cfg.ldapHost}</LdapServer>
-            <LdapPort>${builtins.toString cfg.ldapPort}</LdapPort>
+            <LdapServer>${cfg.ldap.host}</LdapServer>
+            <LdapPort>${builtins.toString cfg.ldap.port}</LdapPort>
             <UseSsl>false</UseSsl>
             <UseStartTls>false</UseStartTls>
             <SkipSslVerify>false</SkipSslVerify>
-            <LdapBindUser>uid=admin,ou=people,${cfg.dcdomain}</LdapBindUser>
+            <LdapBindUser>uid=admin,ou=people,${cfg.ldap.dcdomain}</LdapBindUser>
             <LdapBindPassword>%LDAP_PASSWORD%</LdapBindPassword>
-            <LdapBaseDn>ou=people,${cfg.dcdomain}</LdapBaseDn>
-            <LdapSearchFilter>(memberof=cn=jellyfin_user,ou=groups,${cfg.dcdomain})</LdapSearchFilter>
-            <LdapAdminBaseDn>ou=people,${cfg.dcdomain}</LdapAdminBaseDn>
-            <LdapAdminFilter>(memberof=cn=jellyfin_admin,ou=groups,${cfg.dcdomain})</LdapAdminFilter>
+            <LdapBaseDn>ou=people,${cfg.ldap.dcdomain}</LdapBaseDn>
+            <LdapSearchFilter>(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})</LdapSearchFilter>
+            <LdapAdminBaseDn>ou=people,${cfg.ldap.dcdomain}</LdapAdminBaseDn>
+            <LdapAdminFilter>(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})</LdapAdminFilter>
             <EnableLdapAdminFilterMemberUid>false</EnableLdapAdminFilterMemberUid>
             <LdapSearchAttributes>uid, cn, mail, displayName</LdapSearchAttributes>
             <LdapClientCertPath />
@@ -271,22 +305,22 @@ in
             <OidConfigs>
               <item>
                 <key>
-                  <string>${cfg.oidcProvider}</string>
+                  <string>${cfg.sso.provider}</string>
                 </key>
                 <value>
                   <PluginConfiguration>
-                    <OidEndpoint>${cfg.authEndpoint}</OidEndpoint>
-                    <OidClientId>${cfg.oidcClientID}</OidClientId>
+                    <OidEndpoint>${cfg.sso.endpoint}</OidEndpoint>
+                    <OidClientId>${cfg.sso.clientID}</OidClientId>
                     <OidSecret>%SSO_SECRET%</OidSecret>
                     <Enabled>true</Enabled>
                     <EnableAuthorization>true</EnableAuthorization>
                     <EnableAllFolders>true</EnableAllFolders>
                     <EnabledFolders />
                     <AdminRoles>
-                      <string>${cfg.oidcAdminUserGroup}</string>
+                      <string>${cfg.sso.adminUserGroup}</string>
                     </AdminRoles>
                     <Roles>
-                      <string>${cfg.oidcUserGroup}</string>
+                      <string>${cfg.sso.userGroup}</string>
                     </Roles>
                     <EnableFolderRoles>false</EnableFolderRoles>
                     <FolderRoleMappings />
@@ -305,15 +339,15 @@ in
         brandingConfig = pkgs.writeText "branding.xml" ''
           <?xml version="1.0" encoding="utf-8"?>
           <BrandingOptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-            <LoginDisclaimer>&lt;a href="https://${cfg.subdomain}.${cfg.domain}/SSO/OID/p/${cfg.oidcProvider}" class="raised cancel block emby-button authentik-sso"&gt;
-                Sign in with ${cfg.oidcProvider}&amp;nbsp;
+            <LoginDisclaimer>&lt;a href="https://${cfg.subdomain}.${cfg.domain}/SSO/OID/p/${cfg.sso.provider}" class="raised cancel block emby-button authentik-sso"&gt;
+                Sign in with ${cfg.sso.provider}&amp;nbsp;
                 &lt;img alt="OpenID Connect (authentik)" title="OpenID Connect (authentik)" class="oauth-login-image" src="https://raw.githubusercontent.com/goauthentik/authentik/master/web/icons/icon.png"&gt;
               &lt;/a&gt;
               &lt;a href="https://${cfg.subdomain}.${cfg.domain}/SSOViews/linking" class="raised cancel block emby-button authentik-sso"&gt;
-                Link ${cfg.oidcProvider} config&amp;nbsp;
+                Link ${cfg.sso.provider} config&amp;nbsp;
               &lt;/a&gt;
-              &lt;a href="${cfg.authEndpoint}" class="raised cancel block emby-button authentik-sso"&gt;
-                ${cfg.oidcProvider} config&amp;nbsp;
+              &lt;a href="${cfg.sso.endpoint}" class="raised cancel block emby-button authentik-sso"&gt;
+                ${cfg.sso.provider} config&amp;nbsp;
               &lt;/a&gt;
             </LoginDisclaimer>
             <CustomCss>
@@ -348,36 +382,36 @@ in
           </BrandingOptions>
         '';
       in
-        shblib.replaceSecretsScript {
+        lib.strings.optionalString cfg.ldap.enable (shblib.replaceSecretsScript {
           file = ldapConfig;
           resultPath = "/var/lib/jellyfin/plugins/configurations/LDAP-Auth.xml";
           replacements = {
-            "%LDAP_PASSWORD%" = "$(cat ${cfg.ldapPasswordFile})";
+            "%LDAP_PASSWORD%" = "$(cat ${cfg.ldap.passwordFile})";
           };
-        }
-        + shblib.replaceSecretsScript {
+        })
+        + lib.strings.optionalString cfg.sso.enable (shblib.replaceSecretsScript {
           file = ssoConfig;
           resultPath = "/var/lib/jellyfin/plugins/configurations/SSO-Auth.xml";
           replacements = {
-            "%SSO_SECRET%" = "$(cat ${cfg.ssoSecretFile})";
+            "%SSO_SECRET%" = "$(cat ${cfg.sso.secretFile})";
           };
-        }
-        + shblib.replaceSecretsScript {
+        })
+        + lib.strings.optionalString cfg.sso.enable (shblib.replaceSecretsScript {
           file = brandingConfig;
           resultPath = "/var/lib/jellyfin/config/branding.xml";
           replacements = {
             "%a%" = "%a%";
           };
-        };
+        });
 
-    shb.authelia.oidcClients = [
+    shb.authelia.oidcClients = lib.lists.optionals (!(isNull cfg.sso)) [
       {
-        id = cfg.oidcClientID;
+        id = cfg.sso.clientID;
         description = "Jellyfin";
-        secret.source = cfg.ssoSecretFile;
+        secret.source = cfg.sso.secretFile;
         public = false;
         authorization_policy = "one_factor";
-        redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.oidcProvider}" ];
+        redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.sso.provider}" ];
       }
     ];
 
diff --git a/test/vm/jellyfin.nix b/test/vm/jellyfin.nix
new file mode 100644
index 0000000..0cdef24
--- /dev/null
+++ b/test/vm/jellyfin.nix
@@ -0,0 +1,326 @@
+{ pkgs, lib, ... }:
+{
+  basic = pkgs.nixosTest {
+    name = "jellyfin-basic";
+
+    nodes.server = { config, pkgs, ... }: {
+      imports = [
+        {
+          options = {
+            shb.backup = lib.mkOption { type = lib.types.anything; };
+            shb.authelia = lib.mkOption { type = lib.types.anything; };
+          };
+        }
+        ../../modules/services/jellyfin.nix
+      ];
+
+      shb.jellyfin = {
+        enable = true;
+        domain = "example.com";
+        subdomain = "j";
+      };
+      # Nginx port.
+      networking.firewall.allowedTCPPorts = [ 80 ];
+    };
+
+    nodes.client = {};
+
+    # TODO: Test login
+    testScript = { nodes, ... }: ''
+    import json
+
+    def curl(target, format, endpoint):
+        return json.loads(target.succeed(
+            "curl --fail-with-body --silent --show-error --output /dev/null --location"
+            + " --connect-to j.example.com:443:server:443"
+            + " --connect-to j.example.com:80:server:80"
+            + f" --write-out '{format}'"
+            + " " + endpoint
+        ))
+
+    start_all()
+    server.wait_for_unit("jellyfin.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_open_port(8096)
+
+    response = curl(client, """{"code":%{response_code}}""", "http://j.example.com")
+
+    if response['code'] != 200:
+        raise Exception(f"Code is {response['code']}")
+    '';
+  };
+
+  ldap = pkgs.nixosTest {
+    name = "jellyfin-ldap";
+
+    nodes.server = { config, pkgs, ... }: {
+      imports = [
+        {
+          options = {
+            shb.backup = lib.mkOption { type = lib.types.anything; };
+            shb.authelia = lib.mkOption { type = lib.types.anything; };
+          };
+        }
+        ../../modules/blocks/ldap.nix
+        ../../modules/services/jellyfin.nix
+      ];
+
+      shb.ldap = {
+        enable = true;
+        domain = "example.com";
+        subdomain = "ldap";
+        ldapPort = 3890;
+        webUIListenPort = 17170;
+        dcdomain = "dc=example,dc=com";
+        ldapUserPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
+        jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret";
+      };
+
+      shb.jellyfin = {
+        enable = true;
+        domain = "example.com";
+        subdomain = "j";
+
+        ldap = {
+          enable = true;
+          host = "127.0.0.1";
+          port = config.shb.ldap.ldapPort;
+          dcdomain = config.shb.ldap.dcdomain;
+          passwordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
+        };
+      };
+      # Nginx port.
+      networking.firewall.allowedTCPPorts = [ 80 ];
+    };
+
+    nodes.client = {};
+
+    # TODO: Test login with ldap user
+    testScript = { nodes, ... }: ''
+    import json
+
+    def curl(target, format, endpoint):
+        return json.loads(target.succeed(
+            "curl --fail-with-body --silent --show-error --output /dev/null --location"
+            + " --connect-to j.example.com:443:server:443"
+            + " --connect-to j.example.com:80:server:80"
+            + f" --write-out '{format}'"
+            + " " + endpoint
+        ))
+
+    start_all()
+    server.wait_for_unit("jellyfin.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_unit("lldap.service")
+    server.wait_for_open_port(8096)
+
+    response = curl(client, """{"code":%{response_code}}""", "http://j.example.com")
+
+    if response['code'] != 200:
+        raise Exception(f"Code is {response['code']}")
+    '';
+  };
+
+  cert = pkgs.nixosTest {
+    name = "jellyfin_cert";
+
+    nodes.server = { config, pkgs, ... }: {
+      imports = [
+        {
+          options = {
+            shb.backup = lib.mkOption { type = lib.types.anything; };
+            shb.authelia = lib.mkOption { type = lib.types.anything; };
+          };
+        }
+        ../../modules/blocks/nginx.nix
+        ../../modules/blocks/postgresql.nix
+        ../../modules/blocks/ssl.nix
+        ../../modules/services/jellyfin.nix
+      ];
+
+      shb.certs = {
+        cas.selfsigned.myca = {
+          name = "My CA";
+        };
+        certs.selfsigned = {
+          n = {
+            ca = config.shb.certs.cas.selfsigned.myca;
+            domain = "*.example.com";
+            group = "nginx";
+          };
+        };
+      };
+
+      systemd.services.nginx.after = [ config.shb.certs.certs.selfsigned.n.systemdService ];
+      systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ];
+
+      shb.jellyfin = {
+        enable = true;
+        domain = "example.com";
+        subdomain = "j";
+        ssl = config.shb.certs.certs.selfsigned.n;
+      };
+      # Nginx port.
+      networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+      shb.nginx.accessLog = true;
+    };
+
+    nodes.client = {};
+
+    # TODO: Test login
+    testScript = { nodes, ... }: ''
+    import json
+    import os
+    import pathlib
+
+    def curl(target, format, endpoint):
+        return json.loads(target.succeed(
+            "curl --fail-with-body --silent --show-error --output /dev/null --location"
+            + " --connect-to j.example.com:443:server:443"
+            + " --connect-to j.example.com:80:server:80"
+            + f" --write-out '{format}'"
+            + " " + endpoint
+        ))
+
+    start_all()
+    server.wait_for_unit("jellyfin.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_open_port(8096)
+
+    server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt")
+    client.succeed("rm -r /etc/ssl/certs")
+    client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt")
+
+    response = curl(client, """{"code":%{response_code}}""", "https://j.example.com")
+
+    if response['code'] != 200:
+        raise Exception(f"Code is {response['code']}")
+    '';
+  };
+
+  sso = pkgs.nixosTest {
+    name = "jellyfin_sso";
+
+    nodes.server = { config, pkgs, ... }: {
+      imports = [
+        {
+          options = {
+            shb.backup = lib.mkOption { type = lib.types.anything; };
+          };
+        }
+        ../../modules/blocks/authelia.nix
+        ../../modules/blocks/ldap.nix
+        ../../modules/blocks/postgresql.nix
+        ../../modules/blocks/ssl.nix
+        ../../modules/services/jellyfin.nix
+      ];
+
+      shb.ldap = {
+        enable = true;
+        domain = "example.com";
+        subdomain = "ldap";
+        ldapPort = 3890;
+        webUIListenPort = 17170;
+        dcdomain = "dc=example,dc=com";
+        ldapUserPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
+        jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret";
+      };
+
+      shb.certs = {
+        cas.selfsigned.myca = {
+          name = "My CA";
+        };
+        certs.selfsigned = {
+          n = {
+            ca = config.shb.certs.cas.selfsigned.myca;
+            domain = "*.example.com";
+            group = "nginx";
+          };
+        };
+      };
+
+      systemd.services.nginx.after = [ config.shb.certs.certs.selfsigned.n.systemdService ];
+      systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ];
+
+      shb.authelia = {
+        enable = true;
+        domain = "example.com";
+        subdomain = "auth";
+        ssl = config.shb.certs.certs.selfsigned.n;
+
+        ldapEndpoint = "ldap://127.0.0.1:${builtins.toString config.shb.ldap.ldapPort}";
+        dcdomain = config.shb.ldap.dcdomain;
+
+        secrets = {
+          jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret";
+          ldapAdminPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
+          sessionSecretFile = pkgs.writeText "sessionSecret" "sessionSecret";
+          storageEncryptionKeyFile = pkgs.writeText "storageEncryptionKey" "storageEncryptionKey";
+          identityProvidersOIDCHMACSecretFile = pkgs.writeText "identityProvidersOIDCHMACSecret" "identityProvidersOIDCHMACSecret";
+          identityProvidersOIDCIssuerPrivateKeyFile = (pkgs.runCommand "gen-private-key" {} ''
+          mkdir $out
+          ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
+          '') + "/private.pem";
+        };
+      };
+
+      shb.jellyfin = {
+        enable = true;
+        domain = "example.com";
+        subdomain = "j";
+        ssl = config.shb.certs.certs.selfsigned.n;
+
+        ldap = {
+          enable = true;
+          host = "127.0.0.1";
+          port = config.shb.ldap.ldapPort;
+          dcdomain = config.shb.ldap.dcdomain;
+          passwordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
+        };
+
+        sso = {
+          enable = true;
+          endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
+          secretFile = pkgs.writeText "ssoSecretFile" "ssoSecretFile";
+        };
+      };
+      # Nginx port.
+      networking.firewall.allowedTCPPorts = [ 80 443 ];
+    };
+
+    nodes.client = {};
+
+    # TODO: Test login with ldap user
+    testScript = { nodes, ... }: ''
+    import json
+    import os
+    import pathlib
+
+    def curl(target, format, endpoint):
+        return json.loads(target.succeed(
+            "curl --fail-with-body --silent --show-error --output /dev/null --location"
+            + " --connect-to j.example.com:443:server:443"
+            + " --connect-to j.example.com:80:server:80"
+            + f" --write-out '{format}'"
+            + " " + endpoint
+        ))
+
+    start_all()
+    server.wait_for_unit("jellyfin.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_unit("lldap.service")
+    server.wait_for_unit("authelia-auth.example.com.service")
+    server.wait_for_open_port(8096)
+
+    server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt")
+    client.succeed("rm -r /etc/ssl/certs")
+    client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt")
+
+    response = curl(client, """{"code":%{response_code}}""", "https://j.example.com")
+
+    if response['code'] != 200:
+        raise Exception(f"Code is {response['code']}")
+    '';
+  };
+}