Add nextcloud

Nextcloud will allow us to easily replace google docs with open source
alternatives such as Collabora Online.

Other options considered:
- Privatebin (not for collaborative document editing, we do have an instance at
  https://paste.clicks.codes/)
- Etherpad (seemed to require a nontrivial amount of setup and an old version of
  nodejs)
- Owncloud (looks good too, but Nextcloud is the offering I'm more familiar with
  as I've used instances of it before (and some friends host it!))
- Standalone Collabora Online server (would need some other WOPI host or to use
  filesystem... which, no)
- Cryptpad (I'm less familiar with it, and we don't seem to need the things
  which are its selling points)

Still TODO (for followup changes):
- Integrate with Keycloak (added in I53e33f8c7fa21220e5407d4cd75a705c8e19f9a3)
  for authentication

Change-Id: I8cf10ee09b068eb7d74cd2d5619b509eb9581f8d
diff --git a/flake.nix b/flake.nix
index 838808d..3022dc2 100644
--- a/flake.nix
+++ b/flake.nix
@@ -66,6 +66,7 @@
               ./modules/loginctl-linger.nix
               ./modules/matrix.nix
               ./modules/mongodb.nix
+              ./modules/nextcloud.nix
               ./modules/node.nix
               ./modules/postgres.nix
               ./modules/privatebin.nix
diff --git a/modules/caddy.nix b/modules/caddy.nix
index eddcf80..26e0a58 100644
--- a/modules/caddy.nix
+++ b/modules/caddy.nix
@@ -1,5 +1,6 @@
 { base, config, pkgs, lib, ... }: lib.recursiveUpdate
 {
+  services.nginx.enable = false; # PrivateBin, nextcloud etc. attempts to enable nginx but we already use caddy
   services.caddy.enable = true;
   services.caddy.configFile = lib.pipe ./caddy/caddyfile.nix [
     import
diff --git a/modules/caddy/caddyfile.nix b/modules/caddy/caddyfile.nix
index 0b2acf4..4924c6f 100644
--- a/modules/caddy/caddyfile.nix
+++ b/modules/caddy/caddyfile.nix
@@ -382,6 +382,11 @@
             "${pkgs.privatebin}/share/privatebin"
             "unix/${config.services.phpfpm.pools.privatebin.socket}"
           )
+          (PHPRoute
+            [ "cloud.clicks.codes" "nextcloud.clicks.codes" "docs.clicks.codes" ]
+            "${config.services.nextcloud.package}"
+            "unix/${config.services.phpfpm.pools.nextcloud.socket}"
+          )
         ];
       };
       srv1 = {
diff --git a/modules/nextcloud.nix b/modules/nextcloud.nix
new file mode 100644
index 0000000..9605bfc
--- /dev/null
+++ b/modules/nextcloud.nix
@@ -0,0 +1,48 @@
+{config, pkgs, lib, ...}: {
+    sops.secrets.clicks_nextcloud_db_password = {
+        mode = lib.mkForce "0440";
+        group = lib.mkForce "nextcloud";
+    };
+
+    users.users.nextcloud = {
+        isSystemUser = true;
+        createHome = true;
+        home = "/var/lib/nextcloud";
+        group = config.users.groups.nextcloud.name;
+        shell = pkgs.bashInteractive;
+    };
+    users.groups.nextcloud = {};
+
+
+    services.nextcloud.enable = true;
+    services.nextcloud.config.adminpassFile = config.sops.secrets.nextcloud_admin_password.path;
+    services.nextcloud.hostName = "cloud.clicks.codes";
+    services.nextcloud.package = pkgs.nextcloud27;
+    services.nextcloud.poolSettings = {
+        pm = "dynamic";
+        "pm.max_children" = "32";
+        "pm.max_requests" = "500";
+        "pm.max_spare_servers" = "4";
+        "pm.min_spare_servers" = "2";
+        "pm.start_servers" = "2";
+        "listen.owner" = config.users.users.nextcloud.name;
+        "listen.group" = config.users.users.nextcloud.group;
+    };
+
+    services.nextcloud.config = {
+        dbtype = "pgsql";
+        dbport = config.services.postgresql.port;
+        dbpassFile = config.sops.secrets.clicks_nextcloud_db_password.path;
+        dbname = "nextcloud";
+        dbhost = "localhost";
+        extraTrustedDomains = [ "nextcloud.clicks.codes" "docs.clicks.codes" ];
+    };
+
+    sops.secrets.nextcloud_admin_password = {
+        mode = "0600";
+        owner = config.users.users.nextcloud.name;
+        group = config.users.users.nextcloud.group;
+        sopsFile = ../secrets/nextcloud.json;
+        format = "json";
+    };
+}
\ No newline at end of file
diff --git a/modules/postgres.nix b/modules/postgres.nix
index d2844c1..cedb222 100644
--- a/modules/postgres.nix
+++ b/modules/postgres.nix
@@ -14,6 +14,7 @@
       "vaultwarden"
       "privatebin"
       "keycloak"
+      "nextcloud"
     ];
 
     ensureUsers = [
@@ -48,6 +49,12 @@
           "DATABASE privatebin" = "ALL PRIVILEGES";
         };
       }
+      {
+        name = "nextcloud";
+        ensurePermissions = {
+          "DATABASE nextcloud" = "ALL PRIVILEGES";
+        };
+      }
     ] ++ (map
       (name: (
         {
@@ -82,6 +89,7 @@
       { user = "keycloak"; passwordFile = config.sops.secrets.clicks_keycloak_db_password.path; }
       { user = "vaultwarden"; passwordFile = config.sops.secrets.clicks_bitwarden_db_password.path; }
       { user = "privatebin"; passwordFile = config.sops.secrets.clicks_privatebin_db_password.path; }
+      { user = "nextcloud"; passwordFile = config.sops.secrets.clicks_nextcloud_db_password.path; }
     ] [
       (map (userData: ''
         $PSQL -tAc "ALTER USER ${userData.user} PASSWORD '$(cat ${userData.passwordFile})';"
@@ -95,6 +103,7 @@
     "clicks_keycloak_db_password"
     "clicks_bitwarden_db_password"
     "clicks_privatebin_db_password"
+    "clicks_nextcloud_db_password"
   ] [
     (map (name: {
       inherit name;
diff --git a/modules/privatebin.nix b/modules/privatebin.nix
index 5dd6a28..e82c389 100644
--- a/modules/privatebin.nix
+++ b/modules/privatebin.nix
@@ -1,7 +1,6 @@
 { config, lib, base, ... }:
 lib.recursiveUpdate
 {
-  services.nginx.enable = false; # PrivateBin attempts to enable nginx but we already use caddy
   services.privatebin = {
     enable = true;
     settings = {
@@ -17,7 +16,7 @@
 
         info = ''Powered by <a href="https://privatebin.info/">PrivateBin</a>. Provided as a service free-of-charge by Clicks. Come chat with us <a href="https://matrix.to/#/#global:coded.codes"> on Matrix</a>'';
         notice = "This service has no guarantee of uptime, and pastes are not backed up. If you need somewhere to host the last words of your wise old grandfather for time immemorial this is not the place.";
-        
+
         langaugeselection = true;
       };
 
diff --git a/modules/syncthing.nix b/modules/syncthing.nix
index d9f482c..cea1012 100644
--- a/modules/syncthing.nix
+++ b/modules/syncthing.nix
@@ -1,6 +1,4 @@
 { pkgs, ... }: {
-  environment.systemPackages = with pkgs; [ syncthing ];
-
   services.syncthing.enable = true;
   services.syncthing.openDefaultPorts = true;
 
diff --git a/secrets/nextcloud.json b/secrets/nextcloud.json
new file mode 100644
index 0000000..e31d61d
--- /dev/null
+++ b/secrets/nextcloud.json
@@ -0,0 +1,28 @@
+{
+	"nextcloud_admin_password": "ENC[AES256_GCM,data:fVg/Kt+oQBWSAwTT+j4RZNimMZSXfoxD4oNOosFy6F1+YIFVuxajmm05k2FHS9cUcmHcpF1oqoBrroLgMvWVGn7ZkFOa26KwT7Is3IOlnsNaL96dsLh1biYw4dCSftfbPvIiWdS1lZnqXWU99/yleq3qpS9qcb8tNq2TCHecZco=,iv:VMEoabNI5lGum+pFW93JHa9+C0OGZMxnG4J4/15D5No=,tag:E5eOVADG8EwH3WM5oh1hig==,type:str]",
+	"sops": {
+		"kms": null,
+		"gcp_kms": null,
+		"azure_kv": null,
+		"hc_vault": null,
+		"age": [
+			{
+				"recipient": "age15mv77dpnh5762gk5rsw2u79uza4tg8cu6r3nlwjudlzmdqqck3ss6mg9dy",
+				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4T2dxSmN3YkxzZGthaHpR\ndVN6NWtPcGFmMlBwcjJKWG94Yjk2eTR2VHhvCmc5YXJlSUdxOFJVdm0yVmlsSDc4\ndjlXL3h0eHFhUml5bEYyT003cnFVTWMKLS0tIEE4dzllVzlsWjhFbFBhWElrZEJP\nazBneU0rYTFlTCtiZW9RU09hZnZua3MKl4ubpy7xRx4BCQ6A1SawLKMNID9Xd3N2\nhctRsfaMISWwFnw8yOO14zT02bxXXy1NS8vpQSQyuybv++FoUAklxQ==\n-----END AGE ENCRYPTED FILE-----\n"
+			},
+			{
+				"recipient": "age1m7k864feyuezllp2hj4edkccn36rthrvfw969j6f0l3c0mhh5emsnfx6pd",
+				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBScytCcHJXM2dzTlA1bW4w\nLzhvOXNxSkpVeTJHN2JERWgyNEw0US9ibVQ0CjB4d09mclFhVkxJYmVjZk56REY3\nY1lpNnJvakFqamhwM3lKeiszSkV1YUUKLS0tIGM1SnlCWGFpL1BjMk01Tllmb3BI\ncUc5Y0xWbnJvZVFpZlBjT3BzZnFvN28KBB/erTTzn44j1dLHqXOg9KmH7srvoDIW\n105QKYsU40h2qtROVscxB5vcpPjxgcacsHFi89wRhUqy1n45nAkNhg==\n-----END AGE ENCRYPTED FILE-----\n"
+			},
+			{
+				"recipient": "age1fxxnmkeuqhhct93c43pwkzhuzzq8857s5hye6pgfpku70kjn4ecqtamfqr",
+				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4dTZWN0wreHAzNGFlTVJo\nZnAxZ3ZQYzFHTGdTUVNnVmVuS1I4YVZqRENvClY3QzVaZGwxRVk1c0JrbDhObzF1\nMFErbFpmMFBpYWhZK3N0ZGtkeklMcm8KLS0tIEpNUFNqa0RRV3RiUnRjTXN6ZnVa\nLzBOenFydE4vSkdQZ0FpamY4VC95THMK796rEHXR0K1VrNZofw7nk6SlvJx9cQcU\nswcrNrGBSrSO2mOX+fRKqBMfP0BMsCdk/jmdwl/RjTqPPQI4/hwaTw==\n-----END AGE ENCRYPTED FILE-----\n"
+			}
+		],
+		"lastmodified": "2023-10-07T21:36:03Z",
+		"mac": "ENC[AES256_GCM,data:w4wB57MrB/1h36VBK6wjG9RbUVf+MUlJyUnkBo83isBnMROZb8SOab3DcUQLl0hysCvvMnSuiLg/OCKi/w2cLGsYb+NELJ9oW02BoEv7SPymfCznmn37l9Tk8ri10V8LJ7hXVp9brJWyIA8HTwcr6EhNJMWhXyiZwQEMwppycHU=,iv:BI6f001a9fXRuVVXUcU8tc3Lb9J5rNV/HInauLMF5o8=,tag:ZzZSqo9iKWyKVDyghq7ORw==,type:str]",
+		"pgp": null,
+		"unencrypted_suffix": "_unencrypted",
+		"version": "3.7.3"
+	}
+}
\ No newline at end of file