Add ERPNext

ERPNext is many things that Clicks wants. In particular, we're interested
in task tracking and ticketing. Thanks to Frappix by blaggacao, it's not
even too challenging to install on nix anymore!

We'll be using the domain frappe.clicks.codes (rather than
erpnext.clicks.codes) because we would like to be able to install more
frappe apps on the same site without it seeming weird. We will, however,
make the domain redirect to give a nicer way to remember this.

Change-Id: I418d172c5de2b6d6918e1c5f55c7f1d6e1faa2ec
Reviewed-on: https://git.clicks.codes/c/Infra/NixFiles/+/535
Reviewed-by: Samuel Shuert <coded@clicks.codes>
Tested-by: Skyler Grey <minion@clicks.codes>
diff --git a/modules/common/frappe.nix b/modules/common/frappe.nix
new file mode 100644
index 0000000..8479aff
--- /dev/null
+++ b/modules/common/frappe.nix
@@ -0,0 +1,40 @@
+{ pkgs, config, ... }: {
+  services.frappe = {
+    enable = true;
+    project = "frappe";
+
+    package = pkgs.frappix.frappe;
+
+    apps = [
+      pkgs.frappix.erpnext
+    ];
+
+    sites."frappe.clicks.codes" = {
+      domains = [
+        "frappe.clicks.codes"
+      ];
+
+      apps = [
+        "frappe"
+        "erpnext"
+      ];
+    };
+
+    adminPassword = config.sops.secrets."modules/common/frappe.nix:adminPassword".path;
+
+    gunicorn_workers = 9;
+  };
+
+  services.nginx.virtualHosts."frappe.clicks.codes" = {
+    enableACME = true;
+  };
+
+  sops.secrets."modules/common/frappe.nix:adminPassword" = {
+    mode = "0400";
+    owner = config.users.users.root.name;
+    group = config.users.users.root.group;
+    sopsFile = ../../secrets/frappe.json;
+    format = "json";
+    key = "adminPassword";
+  };
+}
diff --git a/modules/common/nginx-routes.nix b/modules/common/nginx-routes.nix
index 61f8c20..d01e84c 100644
--- a/modules/common/nginx-routes.nix
+++ b/modules/common/nginx-routes.nix
@@ -12,7 +12,14 @@
     ]))
     (Hosts [ "resume.coded.codes" "resume.thecoded.prof" ] (ReverseProxy "coded:1024"))
     (Hosts [ "vaultwarden.clicks.codes" "passwords.clicks.codes" ] (ReverseProxy "generic:1028"))
-    (Hosts [ "taiga.clicks.codes" "projects.clicks.codes" "tasks.clicks.codes" "issues.clicks.codes" "kanban.clicks.codes" ] (ReverseProxy "generic:1029"))
+    (Hosts [
+      "erpnext.clicks.codes"
+      "projects.clicks.codes"
+      "tasks.clicks.codes"
+      "issues.clicks.codes"
+      "kanban.clicks.codes"
+      "taiga.clicks.codes"
+    ] (Redirect "https://frappe.clicks.codes/app"))
     (Host "login.clicks.codes" (ReverseProxy "127.0.0.1:9083"))
     (Hosts [ "gerrit.clicks.codes" "git.clicks.codes" ]
       (ReverseProxy "generic:1024"))
diff --git a/modules/common/taiga.nix b/modules/common/taiga.nix
deleted file mode 100644
index 85fc4e3..0000000
--- a/modules/common/taiga.nix
+++ /dev/null
@@ -1,174 +0,0 @@
-{ config, pkgs, ... }: let
-  openid_environment = {
-    ENABLE_OPENID = "True";
-    OPENID_USER_URL = "https://login.clicks.codes/realms/master/protocol/openid-connect/userinfo";
-    OPENID_TOKEN_URL = "https://login.clicks.codes/realms/master/protocol/openid-connect/token";
-    OPENID_CLIENT_ID = "taiga";
-    OPENID_NAME = "Clicks Keycloak";
-
-    # PUBLIC_REGISTER_ENABLED = "True";
-
-    OPENID_ID_FIELD = "sub";
-    OPENID_USERNAME_FIELD = "preferred_username";
-    OPENID_FULLNAME_FIELD = "name";
-    OPENID_EMAIL_FIELD = "email";
-    OPENID_SCOPE="openid email";
-
-    OPENID_FILTER = "enabled";
-    OPENID_FILTER_FIELD = "taiga_access";
-  };
-  backend_environment = openid_environment // {
-    POSTGRES_DB = "taiga";
-    POSTGRES_USER = "taiga";
-    POSTGRES_HOST = "172.20.0.1";
-
-    TAIGA_SITES_SCHEME = "https";
-    TAIGA_SITES_DOMAIN = "taiga.clicks.codes";
-    TAIGA_SUBPATH = "";
-
-    EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend";
-    DEFAULT_FROM_EMAIL = "taiga@clicks.codes";
-    EMAIL_USE_TLS = "True";
-    EMAIL_USE_SSL = "False"; # not needed when using TLS
-    EMAIL_HOST = "mail.clicks.codes";
-    EMAIL_PORT = "587";
-    EMAIL_HOST_USER = "taiga@clicks.codes";
-
-    RABBITMQ_USER = "taiga";
-
-    ENABLE_TELEMETRY = "False";
-  };
-  credential_environment_files = [
-    config.sops.secrets.taiga_credentials_env.path
-    # TODO: OPENID_CLIENT_SECRET
-  ];
-
-  host_static_folder = "/var/taiga/back/static";
-  host_media_folder = "/var/taiga/back/media";
-
-  backend_volumes = [
-    "${host_static_folder}:/taiga-back/static"
-    "${host_media_folder}:/taiga-back/media"
-  ];
-
-  taiga_version = "latest";
-  taiga_base_version = "latest";  # events, etc. only have X.X.0 versions
-in {
-  sops.secrets.taiga_credentials_env = {
-    mode = "0660";
-    owner = config.users.users.root.name;
-    group = config.users.users.root.group;
-    sopsFile = ../../secrets/taiga.env.bin;
-    format = "binary";
-  };
-
-  networking.firewall.interfaces.taiga.allowedTCPPorts = [ 5432 ];
-
-  systemd.services = {
-    "docker-network-taiga" = {
-      serviceConfig.Type = "oneshot";
-      wantedBy = [
-        "docker-taiga-back.service"
-        "docker-taiga-async.service"
-        "docker-taiga-async-rabbitmq.service"
-        "docker-taiga-front.service"
-        "docker-taiga-events.service"
-        "docker-taiga-events-rabbitmq.service"
-        "docker-taiga-protected.service"
-        "docker-taiga-gateway.service"
-      ];
-      script = ''
-        ${pkgs.docker}/bin/docker network inspect taiga > /dev/null 2>&1 || ${pkgs.docker}/bin/docker network create taiga --gateway 172.20.0.1 --subnet 172.20.0.0/16 --opt com.docker.network.bridge.name=taiga
-      '';
-    };
-    docker-taiga-back.requires = [
-      "docker-taiga-events-rabbitmq.service"
-      "docker-taiga-async-rabbitmq.service"
-      "postgresql.service"
-    ];
-    docker-taiga-async.requires = [
-      "docker-taiga-events-rabbitmq.service"
-      "docker-taiga-async-rabbitmq.service"
-      "postgresql.service"
-    ];
-    docker-taiga-gateway.requires = [
-      "docker-taiga-front.service"
-      "docker-taiga-back.service"
-      "docker-taiga-events.service"
-    ];
-    docker-taiga-events.requires = [
-      "docker-taiga-events-rabbitmq.service"
-    ];
-  };
-  virtualisation.oci-containers.containers = {
-    taiga-back = {
-      image = "taigaio/taiga-back:${taiga_version}";
-      environment = backend_environment;
-      environmentFiles = credential_environment_files;
-      volumes = backend_volumes;
-      extraOptions = [ "--network=taiga" ];
-    };
-    taiga-async = {
-      image = "taigaio/taiga-back:${taiga_version}";
-      environment = backend_environment;
-      environmentFiles = credential_environment_files;
-      volumes = backend_volumes;
-      extraOptions = [ "--network=taiga" ];
-    };
-    taiga-async-rabbitmq = {
-      image = "rabbitmq:3.8-management-alpine";
-      environment = {
-        RABBITMQ_DEFAULT_USER = "taiga";
-        RABBITMQ_DEFAULT_VHOST = "taiga";
-      };
-      environmentFiles = credential_environment_files;
-      volumes = [ "/var/taiga/rabbitmq/async:/var/lib/rabbitmq" ];
-      extraOptions = [ "--network=taiga" ];
-    };
-    taiga-front = {
-      image = "taigaio/taiga-front:${taiga_version}";
-      environment = openid_environment // {
-        TAIGA_URL = "https://taiga.clicks.codes";
-        TAIGA_WEBSOCKETS_URL = "wss://taiga.clicks.codes";
-        TAIGA_SUBPATH = "";
-      };
-      extraOptions = [ "--network=taiga" ];
-    };
-    taiga-events = {
-      image = "taigaio/taiga-events:${taiga_base_version}";
-      environment = {
-        RABBITMQ_USER = "taiga";
-      };
-      environmentFiles = credential_environment_files;
-      extraOptions = [ "--network=taiga" ];
-    };
-    taiga-events-rabbitmq = {
-      image = "rabbitmq:3.8-management-alpine";
-      environment = {
-        RABBITMQ_DEFAULT_USER = "taiga";
-        RABBITMQ_DEFAULT_VHOST = "taiga";
-      };
-      environmentFiles = credential_environment_files;
-      volumes = [ "/var/taiga/rabbitmq/events:/var/lib/rabbitmq" ];
-      extraOptions = [ "--network=taiga" ];
-    };
-    taiga-protected = {
-      image = "taigaio/taiga-protected:${taiga_base_version}";
-      environment = {
-        MAX_AGE = "600";
-      };
-      environmentFiles = credential_environment_files;
-      extraOptions = [ "--network=taiga" ];
-    };
-    taiga-gateway = {
-      image = "nginx:1.19-alpine";
-      ports = [ "127.0.0.255:1029:80/tcp" ];
-      volumes = [
-        "${./taiga/taiga-gateway.conf}:/etc/nginx/conf.d/default.conf"
-        "${host_static_folder}:/taiga/static"
-        "${host_media_folder}:/taiga/media"
-      ];
-      extraOptions = [ "--network=taiga" ];
-    };
-  };
-}
diff --git a/modules/common/taiga/taiga-gateway.conf b/modules/common/taiga/taiga-gateway.conf
deleted file mode 100644
index ebaeb68..0000000
--- a/modules/common/taiga/taiga-gateway.conf
+++ /dev/null
@@ -1,75 +0,0 @@
-server {
-    listen 80 default_server;
-
-    client_max_body_size 100M;
-    charset utf-8;
-
-    # Frontend
-    location / {
-        proxy_pass http://taiga-front/;
-        proxy_pass_header Server;
-        proxy_set_header Host $http_host;
-        proxy_redirect off;
-        proxy_set_header X-Real-IP $remote_addr;
-        proxy_set_header X-Scheme $scheme;
-    }
-
-    # API
-    location /api/ {
-        proxy_pass http://taiga-back:8000/api/;
-        proxy_pass_header Server;
-        proxy_set_header Host $http_host;
-        proxy_redirect off;
-        proxy_set_header X-Real-IP $remote_addr;
-        proxy_set_header X-Scheme $scheme;
-    }
-
-    # Admin
-    location /admin/ {
-        proxy_pass http://taiga-back:8000/admin/;
-        proxy_pass_header Server;
-        proxy_set_header Host $http_host;
-        proxy_redirect off;
-        proxy_set_header X-Real-IP $remote_addr;
-        proxy_set_header X-Scheme $scheme;
-    }
-
-    # Static
-    location /static/ {
-        alias /taiga/static/;
-    }
-
-    # Media
-    location /_protected/ {
-        internal;
-        alias /taiga/media/;
-        add_header Content-disposition "attachment";
-    }
-
-    # Unprotected section
-    location /media/exports/ {
-        alias /taiga/media/exports/;
-        add_header Content-disposition "attachment";
-    }
-
-    location /media/ {
-        proxy_set_header Host $http_host;
-        proxy_set_header X-Real-IP $remote_addr;
-        proxy_set_header X-Scheme $scheme;
-        proxy_set_header X-Forwarded-Proto $scheme;
-        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-        proxy_pass http://taiga-protected:8003/;
-        proxy_redirect off;
-    }
-
-    # Events
-    location /events {
-        proxy_pass http://taiga-events:8888/events;
-        proxy_http_version 1.1;
-        proxy_set_header Upgrade $http_upgrade;
-        proxy_set_header Connection "upgrade";
-        proxy_connect_timeout 7d;
-        proxy_send_timeout 7d;
-        proxy_read_timeout 7d;
-    }
-}