feat: Add nginx module

Change-Id: I34fbb926c4b7eab344c1c14de4e4b5f82c6c30eb
Reviewed-on: https://git.clicks.codes/c/Infra/NixFiles/+/785
Reviewed-by: Samuel Shuert <coded@clicks.codes>
Tested-by: Skyler Grey <minion@clicks.codes>
diff --git a/modules/nixos/clicks/security/acme/default.nix b/modules/nixos/clicks/security/acme/default.nix
index 655e39f..7acb887 100644
--- a/modules/nixos/clicks/security/acme/default.nix
+++ b/modules/nixos/clicks/security/acme/default.nix
@@ -11,16 +11,33 @@
   options.clicks.security.acme = {
     enable = lib.mkEnableOption "Acme defaults";
 
-    email = lib.mkOption {
-      type = lib.types.str;
-      default = "";
-      description = "Email address to use for Let's Encrypt registration.";
-    };
-
     staging = lib.mkOption {
       type = lib.types.bool;
       default = false;
-      description = "Use the Let's Encrypt staging server.";
+      description = "Use the Let's Encrypt staging server";
+    };
+
+    defaults = {
+      email = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = null;
+        description = "Email address to use for Let's Encrypt registration";
+      };
+
+      dnsProvider = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        description = "Default provider for getting web certificates";
+        default = config.clicks.services.nginx.defaultDnsProvider;
+      };
+
+      environmentFile = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default =
+          if config.clicks.security.acme.defaults.dnsProvider == null
+          then null
+          else throw "config.clicks.security.acme: You should provide an environment file default (or explicitly set to null) if you are using a DNS provider";
+        description = "Environment file containing DNS provider credentials";
+      };
     };
   };
 
@@ -29,7 +46,7 @@
       acceptTerms = true;
 
       defaults = {
-        inherit (cfg) email;
+        inherit (cfg.defaults) email dnsProvider environmentFile;
 
         group = lib.mkIf config.services.nginx.enable "nginx";
         server = lib.mkIf cfg.staging "https://acme-staging-v02.api.letsencrypt.org/directory";
diff --git a/modules/nixos/clicks/services/nginx/default.nix b/modules/nixos/clicks/services/nginx/default.nix
new file mode 100644
index 0000000..90d6405
--- /dev/null
+++ b/modules/nixos/clicks/services/nginx/default.nix
@@ -0,0 +1,90 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, config, ... }:
+let
+  cfg = config.clicks.services.nginx;
+in
+{
+  options.clicks.services.nginx = {
+    enable = lib.mkEnableOption "Enable Nginx routing";
+    hosts = lib.options.mkOption {
+      type = lib.types.attrsOf (lib.clicks.types.nginx.host config);
+      description = "Attrset of web domain to host data";
+      default = {};
+    };
+    defaultDnsProvider = lib.options.mkOption {
+      type = lib.types.nullOr lib.types.str;
+      description = "Default provider for getting web certificates";
+      default = null;
+    };
+    xClacksOverhead.enable = lib.options.mkOption {
+      type = lib.types.bool;
+      description = "Write the header `X-Clacks-Overhead: GNU Terry Pratchett` on all virtual host locations";
+      default = true;
+    };
+  };
+
+  config = lib.modules.mkIf cfg.enable (let
+    processedHosts = lib.clicks.nginx.http.internal.serviceTranslation cfg.hosts;
+    hostsList = lib.attrsets.attrsToList processedHosts;
+    nginxHosts = lib.attrsets.mapAttrs (_: host: lib.attrsets.removeAttrs host [ "authWith" "dnsProvider" ]) processedHosts;
+    acmeCerts = lib.attrsets.mapAttrs (_: host: {
+      inherit (host) dnsProvider;
+      webroot = if host.dnsProvider == null
+                then config.security.acme.defaults.webroot
+                else null;
+    }) processedHosts;
+    tailscaleAuthHosts = lib.pipe hostsList [
+      (lib.lists.filter (host: host.value.authWith == "tailscale"))
+      (map (host: host.name))
+    ];
+  in {
+    services.nginx = {
+      enable = true;
+      enableReload = true;
+      virtualHosts = {
+        "default_server_ssl" = {
+          listen = [
+            {
+              ssl = true;
+              port = 443;
+              addr = "0.0.0.0";
+              extraParameters = [
+                "default_server"
+              ];
+            }
+          ];
+
+          rejectSSL = true;
+        };
+        "default_server" = {
+          listen = [
+            {
+              port = 80;
+              addr = "0.0.0.0";
+              extraParameters = [
+                "default_server"
+              ];
+            }
+          ];
+
+          locations."/" = {
+            return = 444;
+          };
+        };
+      } // nginxHosts;
+    };
+
+    security.acme.certs = acmeCerts;
+
+    clicks.services.tailscaleAuth = lib.mkIf (lib.lists.length tailscaleAuthHosts > 0) {
+      enable = true;
+
+      hosts = tailscaleAuthHosts;
+    };
+
+    networking.firewall.allowedTCPPorts = [ 80 443 ];
+  });
+}
diff --git a/modules/nixos/clicks/services/nginx/tailscaleAuth/default.nix b/modules/nixos/clicks/services/nginx/tailscaleAuth/default.nix
new file mode 100644
index 0000000..af9e07f
--- /dev/null
+++ b/modules/nixos/clicks/services/nginx/tailscaleAuth/default.nix
@@ -0,0 +1,38 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, config, ... }:
+let
+  cfg = config.clicks.services.tailscaleAuth;
+in
+{
+  options.clicks.services.tailscaleAuth = {
+    enable = lib.mkEnableOption "Enable tailscaleAuth for Nginx";
+    expectedTailnet = lib.mkOption {
+      type = lib.types.nullOr lib.types.str;
+      description = "The tailnet to expect when authenticating";
+      default =  null;
+    };
+    hosts = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
+      description = "A list of hosts to put behind tailscale auth";
+      default = [];
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.expectedTailnet == null || lib.clicks.strings.endsWith ".ts.net" cfg.expectedTailnet;
+        message = "Your expected tailnet must be an official *.ts.net tailnet, headscale is not supported";
+      }
+    ];
+
+    services.nginx.tailscaleAuth = {
+      enable = true;
+      expectedTailnet = lib.modules.mkIf (cfg.expectedTailnet != null) cfg.expectedTailnet;
+
+      virtualHosts = cfg.hosts;
+    };
+  };
+}