diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 1abf87dfcc63..a6c1d7c5d66c 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 000000000000..68769ac8c031
--- /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 postgresql is selected, then a database called
+ ${db} 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 ihatemoney generate_password_hash";
+ };
+ 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 0c727cf44aee..3481b5e60403 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 3f6921e0f4dd..fe9c4df1416f 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 000000000000..14db17fe5e67
--- /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 000000000000..e37dfe80e580
--- /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 7cbae0956e79..ada5daa000b9 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 { };