grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / firefly-iii.nix
blob338f0490932022a6b62a32d13c6a765cb34271c9
1 { pkgs, config, lib, ... }:
3 let
4   inherit (lib) optionalString mkDefault mkIf mkOption mkEnableOption literalExpression;
5   inherit (lib.types) nullOr attrsOf oneOf str int bool path package enum submodule;
6   inherit (lib.strings) concatLines removePrefix toShellVars removeSuffix hasSuffix;
7   inherit (lib.attrsets) mapAttrsToList attrValues genAttrs filterAttrs mapAttrs' nameValuePair;
8   inherit (builtins) isInt isString toString typeOf;
10   cfg = config.services.firefly-iii;
12   user = cfg.user;
13   group = cfg.group;
15   defaultUser = "firefly-iii";
16   defaultGroup = "firefly-iii";
18   artisan = "${cfg.package}/artisan";
20   env-file-values = mapAttrs' (n: v: nameValuePair (removeSuffix "_FILE" n) v)
21     (filterAttrs (n: v: hasSuffix "_FILE" n) cfg.settings);
22   env-nonfile-values = filterAttrs (n: v: ! hasSuffix "_FILE" n) cfg.settings;
24   fileenv-func = ''
25     set -a
26     ${toShellVars env-nonfile-values}
27     ${concatLines (mapAttrsToList (n: v: "${n}=\"$(< ${v})\"") env-file-values)}
28     set +a
29   '';
31   firefly-iii-maintenance = pkgs.writeShellScript "firefly-iii-maintenance.sh" ''
32     ${fileenv-func}
34     ${optionalString (cfg.settings.DB_CONNECTION == "sqlite")
35       "touch ${cfg.dataDir}/storage/database/database.sqlite"}
36     ${artisan} cache:clear
37     ${artisan} package:discover
38     ${artisan} firefly-iii:upgrade-database
39     ${artisan} firefly-iii:laravel-passport-keys
40     ${artisan} view:cache
41     ${artisan} route:cache
42     ${artisan} config:cache
43   '';
45   commonServiceConfig = {
46     Type = "oneshot";
47     User = user;
48     Group = group;
49     StateDirectory = "firefly-iii";
50     ReadWritePaths = [cfg.dataDir];
51     WorkingDirectory = cfg.package;
52     PrivateTmp = true;
53     PrivateDevices = true;
54     CapabilityBoundingSet = "";
55     AmbientCapabilities = "";
56     ProtectSystem = "strict";
57     ProtectKernelTunables = true;
58     ProtectKernelModules = true;
59     ProtectControlGroups = true;
60     ProtectClock = true;
61     ProtectHostname = true;
62     ProtectHome = "tmpfs";
63     ProtectKernelLogs = true;
64     ProtectProc = "invisible";
65     ProcSubset = "pid";
66     PrivateNetwork = false;
67     RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
68     SystemCallArchitectures = "native";
69     SystemCallFilter = [
70       "@system-service @resources"
71       "~@obsolete @privileged"
72     ];
73     RestrictSUIDSGID = true;
74     RemoveIPC = true;
75     NoNewPrivileges = true;
76     RestrictRealtime = true;
77     RestrictNamespaces = true;
78     LockPersonality = true;
79     PrivateUsers = true;
80   };
82 in {
84   options.services.firefly-iii = {
86     enable = mkEnableOption "Firefly III: A free and open source personal finance manager";
88     user = mkOption {
89       type = str;
90       default = defaultUser;
91       description = "User account under which firefly-iii runs.";
92     };
94     group = mkOption {
95       type = str;
96       default = if cfg.enableNginx then "nginx" else defaultGroup;
97       defaultText = "If `services.firefly-iii.enableNginx` is true then `nginx` else ${defaultGroup}";
98       description = ''
99         Group under which firefly-iii runs. It is best to set this to the group
100         of whatever webserver is being used as the frontend.
101       '';
102     };
104     dataDir = mkOption {
105       type = path;
106       default = "/var/lib/firefly-iii";
107       description = ''
108         The place where firefly-iii stores its state.
109       '';
110     };
112     package = mkOption {
113       type = package;
114       default = pkgs.firefly-iii;
115       defaultText = literalExpression "pkgs.firefly-iii";
116       description = ''
117         The firefly-iii package served by php-fpm and the webserver of choice.
118         This option can be used to point the webserver to the correct root. It
119         may also be used to set the package to a different version, say a
120         development version.
121       '';
122       apply = firefly-iii : firefly-iii.override (prev: {
123         dataDir = cfg.dataDir;
124       });
125     };
127     enableNginx = mkOption {
128       type = bool;
129       default = false;
130       description = ''
131         Whether to enable nginx or not. If enabled, an nginx virtual host will
132         be created for access to firefly-iii. If not enabled, then you may use
133         `''${config.services.firefly-iii.package}` as your document root in
134         whichever webserver you wish to setup.
135       '';
136     };
138     virtualHost = mkOption {
139       type = str;
140       default = "localhost";
141       description = ''
142         The hostname at which you wish firefly-iii to be served. If you have
143         enabled nginx using `services.firefly-iii.enableNginx` then this will
144         be used.
145       '';
146     };
148     poolConfig = mkOption {
149       type = attrsOf (oneOf [ str int bool ]);
150       default = {
151         "pm" = "dynamic";
152         "pm.max_children" = 32;
153         "pm.start_servers" = 2;
154         "pm.min_spare_servers" = 2;
155         "pm.max_spare_servers" = 4;
156         "pm.max_requests" = 500;
157       };
158       description = ''
159         Options for the Firefly III PHP pool. See the documentation on <literal>php-fpm.conf</literal>
160         for details on configuration directives.
161       '';
162     };
164     settings = mkOption {
165       default = {};
166       description = ''
167         Options for firefly-iii configuration. Refer to
168         <https://github.com/firefly-iii/firefly-iii/blob/main/.env.example> for
169         details on supported values. All <option>_FILE values supported by
170         upstream are supported here.
172         APP_URL will be the same as `services.firefly-iii.virtualHost` if the
173         former is unset in `services.firefly-iii.settings`.
174       '';
175       example = literalExpression ''
176         {
177           APP_ENV = "production";
178           APP_KEY_FILE = "/var/secrets/firefly-iii-app-key.txt";
179           SITE_OWNER = "mail@example.com";
180           DB_CONNECTION = "mysql";
181           DB_HOST = "db";
182           DB_PORT = 3306;
183           DB_DATABASE = "firefly";
184           DB_USERNAME = "firefly";
185           DB_PASSWORD_FILE = "/var/secrets/firefly-iii-mysql-password.txt;
186         }
187       '';
188       type = submodule {
189         freeformType = attrsOf (oneOf [str int bool]);
190         options = {
191           DB_CONNECTION = mkOption {
192             type = enum [ "sqlite" "pgsql" "mysql" ];
193             default = "sqlite";
194             example = "pgsql";
195             description = ''
196               The type of database you wish to use. Can be one of "sqlite",
197               "mysql" or "pgsql".
198             '';
199           };
200           APP_ENV = mkOption {
201             type = enum [ "local" "production" "testing" ];
202             default = "local";
203             example = "production";
204             description = ''
205               The app environment. It is recommended to keep this at "local".
206               Possible values are "local", "production" and "testing"
207             '';
208           };
209           DB_PORT = mkOption {
210             type = nullOr int;
211             default = if cfg.settings.DB_CONNECTION == "pgsql" then 5432
212                       else if cfg.settings.DB_CONNECTION == "mysql" then 3306
213                       else null;
214             defaultText = ''
215               `null` if DB_CONNECTION is "sqlite", `3306` if "mysql", `5432` if "pgsql"
216             '';
217             description = ''
218               The port your database is listening at. sqlite does not require
219               this value to be filled.
220             '';
221           };
222           DB_HOST = mkOption {
223             type = str;
224             default = if cfg.settings.DB_CONNECTION == "pgsql" then "/run/postgresql"
225                       else "localhost";
226             defaultText = ''
227               "localhost" if DB_CONNECTION is "sqlite" or "mysql", "/run/postgresql" if "pgsql".
228             '';
229             description = ''
230               The machine which hosts your database. This is left at the
231               default value for "mysql" because we use the "DB_SOCKET" option
232               to connect to a unix socket instead. "pgsql" requires that the
233               unix socket location be specified here instead of at "DB_SOCKET".
234               This option does not affect "sqlite".
235             '';
236           };
237           APP_KEY_FILE = mkOption {
238             type = path;
239             description = ''
240               The path to your appkey. The file should contain a 32 character
241               random app key. This may be set using `echo "base64:$(head -c 32
242               /dev/urandom | base64)" > /path/to/key-file`.
243             '';
244           };
245           APP_URL = mkOption {
246             type = str;
247             default = if cfg.virtualHost == "localhost" then "http://${cfg.virtualHost}"
248                       else "https://${cfg.virtualHost}";
249             defaultText = ''
250               http(s)://''${config.services.firefly-iii.virtualHost}
251             '';
252             description = ''
253               The APP_URL used by firefly-iii internally. Please make sure this
254               URL matches the external URL of your Firefly III installation. It
255               is used to validate specific requests and to generate URLs in
256               emails.
257             '';
258           };
259         };
260       };
261     };
262   };
264   config = mkIf cfg.enable {
266     services.phpfpm.pools.firefly-iii = {
267       inherit user group;
268       phpPackage = cfg.package.phpPackage;
269       phpOptions = ''
270         log_errors = on
271       '';
272       settings = {
273         "listen.mode" = "0660";
274         "listen.owner" = user;
275         "listen.group" = group;
276         "clear_env" = "no";
277       } // cfg.poolConfig;
278     };
280     systemd.services.firefly-iii-setup = {
281       after = [ "postgresql.service" "mysql.service" ];
282       requiredBy = [ "phpfpm-firefly-iii.service" ];
283       before = [ "phpfpm-firefly-iii.service" ];
284       serviceConfig = {
285         ExecStart = firefly-iii-maintenance;
286         RemainAfterExit = true;
287       } // commonServiceConfig;
288       unitConfig.JoinsNamespaceOf = "phpfpm-firefly-iii.service";
289       restartTriggers = [ cfg.package ];
290     };
292     systemd.services.firefly-iii-cron = {
293       after = [ "firefly-iii-setup.service" "postgresql.service" "mysql.service" ];
294       wants = [ "firefly-iii-setup.service" ];
295       description = "Daily Firefly III cron job";
296       serviceConfig = {
297         ExecStart = "${artisan} firefly-iii:cron";
298       } // commonServiceConfig;
299     };
301     systemd.timers.firefly-iii-cron = {
302       description = "Trigger Firefly Cron";
303       timerConfig = {
304         OnCalendar = "Daily";
305         RandomizedDelaySec = "1800s";
306         Persistent = true;
307       };
308       wantedBy = [ "timers.target" ];
309       restartTriggers = [ cfg.package ];
310     };
312     services.nginx = mkIf cfg.enableNginx {
313       enable = true;
314       recommendedTlsSettings = mkDefault true;
315       recommendedOptimisation = mkDefault true;
316       recommendedGzipSettings = mkDefault true;
317       virtualHosts.${cfg.virtualHost} = {
318         root = "${cfg.package}/public";
319         locations = {
320           "/" = {
321             tryFiles = "$uri $uri/ /index.php?$query_string";
322             index = "index.php";
323             extraConfig = ''
324               sendfile off;
325             '';
326           };
327           "~ \.php$" = {
328             extraConfig = ''
329               include ${config.services.nginx.package}/conf/fastcgi_params ;
330               fastcgi_param SCRIPT_FILENAME $request_filename;
331               fastcgi_param modHeadersAvailable true; #Avoid sending the security headers twice
332               fastcgi_pass unix:${config.services.phpfpm.pools.firefly-iii.socket};
333             '';
334           };
335         };
336       };
337     };
339     systemd.tmpfiles.settings."10-firefly-iii" = genAttrs [
340       "${cfg.dataDir}/storage"
341       "${cfg.dataDir}/storage/app"
342       "${cfg.dataDir}/storage/database"
343       "${cfg.dataDir}/storage/export"
344       "${cfg.dataDir}/storage/framework"
345       "${cfg.dataDir}/storage/framework/cache"
346       "${cfg.dataDir}/storage/framework/sessions"
347       "${cfg.dataDir}/storage/framework/views"
348       "${cfg.dataDir}/storage/logs"
349       "${cfg.dataDir}/storage/upload"
350       "${cfg.dataDir}/cache"
351     ] (n: {
352       d = {
353         group = group;
354         mode = "0700";
355         user = user;
356       };
357     }) // {
358       "${cfg.dataDir}".d = {
359         group = group;
360         mode = "0710";
361         user = user;
362       };
363     };
365     users = {
366       users = mkIf (user == defaultUser) {
367         ${defaultUser} = {
368           description = "Firefly-iii service user";
369           inherit group;
370           isSystemUser = true;
371           home = cfg.dataDir;
372         };
373       };
374       groups = mkIf (group == defaultGroup) {
375         ${defaultGroup} = {};
376       };
377     };
378   };