summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/web-apps/ihatemoney/default.nix141
-rw-r--r--nixos/modules/services/web-servers/uwsgi.nix19
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/ihatemoney.nix52
-rw-r--r--pkgs/development/python-modules/ihatemoney/default.nix91
-rw-r--r--pkgs/top-level/python-packages.nix2
7 files changed, 300 insertions, 7 deletions
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 1abf87dfcc6..a6c1d7c5d66 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -806,6 +806,7 @@
   ./services/web-apps/gotify-server.nix
   ./services/web-apps/icingaweb2/icingaweb2.nix
   ./services/web-apps/icingaweb2/module-monitoring.nix
+  ./services/web-apps/ihatemoney
   ./services/web-apps/limesurvey.nix
   ./services/web-apps/mattermost.nix
   ./services/web-apps/mediawiki.nix
diff --git a/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix
new file mode 100644
index 00000000000..68769ac8c03
--- /dev/null
+++ b/nixos/modules/services/web-apps/ihatemoney/default.nix
@@ -0,0 +1,141 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.services.ihatemoney;
+  user = "ihatemoney";
+  group = "ihatemoney";
+  db = "ihatemoney";
+  python3 = config.services.uwsgi.package.python3;
+  pkg = python3.pkgs.ihatemoney;
+  toBool = x: if x then "True" else "False";
+  configFile = pkgs.writeText "ihatemoney.cfg" ''
+        from secrets import token_hex
+        # load a persistent secret key
+        SECRET_KEY_FILE = "/var/lib/ihatemoney/secret_key"
+        SECRET_KEY = ""
+        try:
+          with open(SECRET_KEY_FILE) as f:
+            SECRET_KEY = f.read()
+        except FileNotFoundError:
+          pass
+        if not SECRET_KEY:
+          print("ihatemoney: generating a new secret key")
+          SECRET_KEY = token_hex(50)
+          with open(SECRET_KEY_FILE, "w") as f:
+            f.write(SECRET_KEY)
+        del token_hex
+        del SECRET_KEY_FILE
+
+        # "normal" configuration
+        DEBUG = False
+        SQLALCHEMY_DATABASE_URI = '${
+          if cfg.backend == "sqlite"
+          then "sqlite:////var/lib/ihatemoney/ihatemoney.sqlite"
+          else "postgresql:///${db}"}'
+        SQLALCHEMY_TRACK_MODIFICATIONS = False
+        MAIL_DEFAULT_SENDER = ("${cfg.defaultSender.name}", "${cfg.defaultSender.email}")
+        ACTIVATE_DEMO_PROJECT = ${toBool cfg.enableDemoProject}
+        ADMIN_PASSWORD = "${toString cfg.adminHashedPassword /*toString null == ""*/}"
+        ALLOW_PUBLIC_PROJECT_CREATION = ${toBool cfg.enablePublicProjectCreation}
+        ACTIVATE_ADMIN_DASHBOARD = ${toBool cfg.enableAdminDashboard}
+
+        ${cfg.extraConfig}
+  '';
+in
+  {
+    options.services.ihatemoney = {
+      enable = mkEnableOption "ihatemoney webapp. Note that this will set uwsgi to emperor mode running as root";
+      backend = mkOption {
+        type = types.enum [ "sqlite" "postgresql" ];
+        default = "sqlite";
+        description = ''
+          The database engine to use for ihatemoney.
+          If <literal>postgresql</literal> is selected, then a database called
+          <literal>${db}</literal> will be created. If you disable this option,
+          it will however not be removed.
+        '';
+      };
+      adminHashedPassword = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "The hashed password of the administrator. To obtain it, run <literal>ihatemoney generate_password_hash</literal>";
+      };
+      uwsgiConfig = mkOption {
+        type = types.attrs;
+        example = {
+          http = ":8000";
+        };
+        description = "Additionnal configuration of the UWSGI vassal running ihatemoney. It should notably specify on which interfaces and ports the vassal should listen.";
+      };
+      defaultSender = {
+        name = mkOption {
+          type = types.str;
+          default = "Budget manager";
+          description = "The display name of the sender of ihatemoney emails";
+        };
+        email = mkOption {
+          type = types.str;
+          default = "ihatemoney@${config.networking.hostName}";
+          description = "The email of the sender of ihatemoney emails";
+        };
+      };
+      enableDemoProject = mkEnableOption "access to the demo project in ihatemoney";
+      enablePublicProjectCreation = mkEnableOption "permission to create projects in ihatemoney by anyone";
+      enableAdminDashboard = mkEnableOption "ihatemoney admin dashboard";
+      extraConfig = mkOption {
+        type = types.str;
+        default = "";
+        description = "Extra configuration appended to ihatemoney's configuration file. It is a python file, so pay attention to indentation.";
+      };
+    };
+    config = mkIf cfg.enable {
+      services.postgresql = mkIf (cfg.backend == "postgresql") {
+        enable = true;
+        ensureDatabases = [ db ];
+        ensureUsers = [ {
+          name = user;
+          ensurePermissions = {
+            "DATABASE ${db}" = "ALL PRIVILEGES";
+          };
+        } ];
+      };
+      systemd.services.postgresql = mkIf (cfg.backend == "postgresql") {
+        wantedBy = [ "uwsgi.service" ];
+        before = [ "uwsgi.service" ];
+      };
+      systemd.tmpfiles.rules = [
+        "d /var/lib/ihatemoney 770 ${user} ${group}"
+      ];
+      users = {
+        users.${user} = {
+          isSystemUser = true;
+          inherit group;
+        };
+        groups.${group} = {};
+      };
+      services.uwsgi = {
+        enable = true;
+        plugins = [ "python3" ];
+        # the vassal needs to be able to setuid
+        user = "root";
+        group = "root";
+        instance = {
+          type = "emperor";
+          vassals.ihatemoney = {
+            type = "normal";
+            strict = true;
+            uid = user;
+            gid = group;
+            # apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c
+            enable-threads = true;
+            module = "wsgi:application";
+            chdir = "${pkg}/${pkg.pythonModule.sitePackages}/ihatemoney";
+            env = [ "IHATEMONEY_SETTINGS_FILE_PATH=${configFile}" ];
+            pythonPackages = self: [ self.ihatemoney ];
+          } // cfg.uwsgiConfig;
+        };
+      };
+    };
+  }
+
+
diff --git a/nixos/modules/services/web-servers/uwsgi.nix b/nixos/modules/services/web-servers/uwsgi.nix
index 0c727cf44ae..3481b5e6040 100644
--- a/nixos/modules/services/web-servers/uwsgi.nix
+++ b/nixos/modules/services/web-servers/uwsgi.nix
@@ -5,10 +5,6 @@ with lib;
 let
   cfg = config.services.uwsgi;
 
-  uwsgi = pkgs.uwsgi.override {
-    plugins = cfg.plugins;
-  };
-
   buildCfg = name: c:
     let
       plugins =
@@ -23,8 +19,8 @@ let
       python =
         if hasPython2 && hasPython3 then
           throw "`plugins` attribute in UWSGI configuration shouldn't contain both python2 and python3"
-        else if hasPython2 then uwsgi.python2
-        else if hasPython3 then uwsgi.python3
+        else if hasPython2 then cfg.package.python2
+        else if hasPython3 then cfg.package.python3
         else null;
 
       pythonEnv = python.withPackages (c.pythonPackages or (self: []));
@@ -77,6 +73,11 @@ in {
         description = "Where uWSGI communication sockets can live";
       };
 
+      package = mkOption {
+        type = types.package;
+        internal = true;
+      };
+
       instance = mkOption {
         type = types.attrs;
         default = {
@@ -138,7 +139,7 @@ in {
       '';
       serviceConfig = {
         Type = "notify";
-        ExecStart = "${uwsgi}/bin/uwsgi --uid ${cfg.user} --gid ${cfg.group} --json ${buildCfg "server" cfg.instance}/server.json";
+        ExecStart = "${cfg.package}/bin/uwsgi --uid ${cfg.user} --gid ${cfg.group} --json ${buildCfg "server" cfg.instance}/server.json";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
         NotifyAccess = "main";
@@ -156,5 +157,9 @@ in {
     users.groups = optionalAttrs (cfg.group == "uwsgi") {
       uwsgi.gid = config.ids.gids.uwsgi;
     };
+
+    services.uwsgi.package = pkgs.uwsgi.override {
+      inherit (cfg) plugins;
+    };
   };
 }
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 3f6921e0f4d..fe9c4df1416 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -122,6 +122,7 @@ in
   i3wm = handleTest ./i3wm.nix {};
   icingaweb2 = handleTest ./icingaweb2.nix {};
   iftop = handleTest ./iftop.nix {};
+  ihatemoney = handleTest ./ihatemoney.nix {};
   incron = handleTest ./incron.nix {};
   influxdb = handleTest ./influxdb.nix {};
   initrd-network-ssh = handleTest ./initrd-network-ssh {};
diff --git a/nixos/tests/ihatemoney.nix b/nixos/tests/ihatemoney.nix
new file mode 100644
index 00000000000..14db17fe5e6
--- /dev/null
+++ b/nixos/tests/ihatemoney.nix
@@ -0,0 +1,52 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+let
+  inherit (import ../lib/testing.nix { inherit system pkgs; }) makeTest;
+in
+map (
+  backend: makeTest {
+    name = "ihatemoney-${backend}";
+    machine = { lib, ... }: {
+      services.ihatemoney = {
+        enable = true;
+        enablePublicProjectCreation = true;
+        inherit backend;
+        uwsgiConfig = {
+          http = ":8000";
+        };
+      };
+      boot.cleanTmpDir = true;
+      # ihatemoney needs a local smtp server otherwise project creation just crashes
+      services.opensmtpd = {
+        enable = true;
+        serverConfiguration = ''
+          listen on lo
+          action foo relay
+          match from any for any action foo
+        '';
+      };
+    };
+    testScript = ''
+      $machine->waitForOpenPort(8000);
+      $machine->waitForUnit("uwsgi.service");
+      my $return = $machine->succeed("curl -X POST http://localhost:8000/api/projects -d 'name=yay&id=yay&password=yay&contact_email=yay\@example.com'");
+      die "wrong project id $return" unless "\"yay\"\n" eq $return;
+      my $timestamp = $machine->succeed("stat --printf %Y /var/lib/ihatemoney/secret_key");
+      my $owner = $machine->succeed("stat --printf %U:%G /var/lib/ihatemoney/secret_key");
+      die "wrong ownership for the secret key: $owner, is uwsgi running as the right user ?" unless $owner eq "ihatemoney:ihatemoney";
+      $machine->shutdown();
+      $machine->start();
+      $machine->waitForOpenPort(8000);
+      $machine->waitForUnit("uwsgi.service");
+      # check that the database is really persistent
+      print $machine->succeed("curl --basic -u yay:yay http://localhost:8000/api/projects/yay");
+      # check that the secret key is really persistent
+      my $timestamp2 = $machine->succeed("stat --printf %Y /var/lib/ihatemoney/secret_key");
+      die unless $timestamp eq $timestamp2;
+      $machine->succeed("curl http://localhost:8000 | grep ihatemoney");
+    '';
+  }
+) [ "sqlite" "postgresql" ]
diff --git a/pkgs/development/python-modules/ihatemoney/default.nix b/pkgs/development/python-modules/ihatemoney/default.nix
new file mode 100644
index 00000000000..e37dfe80e58
--- /dev/null
+++ b/pkgs/development/python-modules/ihatemoney/default.nix
@@ -0,0 +1,91 @@
+{ buildPythonPackage, lib, fetchFromGitHub, nixosTests
+, alembic
+, aniso8601
+, Babel
+, blinker
+, click
+, dnspython
+, email_validator
+, flask
+, flask-babel
+, flask-cors
+, flask_mail
+, flask_migrate
+, flask-restful
+, flask_script
+, flask_sqlalchemy
+, flask_wtf
+, idna
+, itsdangerous
+, jinja2
+, Mako
+, markupsafe
+, python-dateutil
+, pytz
+, six
+, sqlalchemy
+, werkzeug
+, wtforms
+, psycopg2 # optional, for postgresql support
+, flask_testing
+}:
+
+buildPythonPackage rec {
+  pname = "ihatemoney";
+  version = "4.1";
+
+  src = fetchFromGitHub {
+    owner = "spiral-project";
+    repo = pname;
+    rev = version;
+    sha256 = "1ai7v2i2rvswzv21nwyq51fvp8lr2x2cl3n34p11br06kc1pcmin";
+  };
+
+  propagatedBuildInputs = [
+    alembic
+    aniso8601
+    Babel
+    blinker
+    click
+    dnspython
+    email_validator
+    flask
+    flask-babel
+    flask-cors
+    flask_mail
+    flask_migrate
+    flask-restful
+    flask_script
+    flask_sqlalchemy
+    flask_wtf
+    idna
+    itsdangerous
+    jinja2
+    Mako
+    markupsafe
+    python-dateutil
+    pytz
+    six
+    sqlalchemy
+    werkzeug
+    wtforms
+    psycopg2
+  ];
+
+  checkInputs = [
+    flask_testing
+  ];
+
+  passthru.tests = {
+    inherit (nixosTests) ihatemoney;
+  };
+  meta = with lib; {
+    homepage = "https://ihatemoney.org";
+    description = "A simple shared budget manager web application";
+    platforms = platforms.linux;
+    license = licenses.beerware;
+    maintainers = [ maintainers.symphorien ];
+  };
+}
+
+
diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix
index 7cbae0956e7..ada5daa000b 100644
--- a/pkgs/top-level/python-packages.nix
+++ b/pkgs/top-level/python-packages.nix
@@ -761,6 +761,8 @@ in {
 
   i3ipc = callPackage ../development/python-modules/i3ipc { };
 
+  ihatemoney = callPackage ../development/python-modules/ihatemoney { };
+
   imutils = callPackage ../development/python-modules/imutils { };
 
   inotify-simple = callPackage ../development/python-modules/inotify-simple { };