summary refs log tree commit diff
path: root/nixos/modules/services/web-apps/ihatemoney/default.nix
blob: 68769ac8c03161933ae04b3cc71891d5277e772a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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;
        };
      };
    };
  }