grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / invoiceplane.nix
blob9a9f180b2102162066044b2dbefccb54734ae078
1 { config, pkgs, lib, ... }:
3 with lib;
5 let
6   cfg = config.services.invoiceplane;
7   eachSite = cfg.sites;
8   user = "invoiceplane";
9   webserver = config.services.${cfg.webserver};
11   invoiceplane-config = hostName: cfg: pkgs.writeText "ipconfig.php" ''
12     IP_URL=http://${hostName}
13     ENABLE_DEBUG=false
14     DISABLE_SETUP=false
15     REMOVE_INDEXPHP=false
16     DB_HOSTNAME=${cfg.database.host}
17     DB_USERNAME=${cfg.database.user}
18     # NOTE: file_get_contents adds newline at the end of returned string
19     DB_PASSWORD=${optionalString (cfg.database.passwordFile != null) "trim(file_get_contents('${cfg.database.passwordFile}'), \"\\r\\n\")"}
20     DB_DATABASE=${cfg.database.name}
21     DB_PORT=${toString cfg.database.port}
22     SESS_EXPIRATION=864000
23     ENABLE_INVOICE_DELETION=false
24     DISABLE_READ_ONLY=false
25     ENCRYPTION_KEY=
26     ENCRYPTION_CIPHER=AES-256
27     SETUP_COMPLETED=false
28     REMOVE_INDEXPHP=true
29   '';
31   mkPhpValue = v:
32     if isString v then escapeShellArg v
33     # NOTE: If any value contains a , (comma) this will not get escaped
34     else if isList v && any lib.strings.isCoercibleToString v then escapeShellArg (concatMapStringsSep "," toString v)
35     else if isInt v then toString v
36     else if isBool v then boolToString v
37     else abort "The Invoiceplane config value ${lib.generators.toPretty {} v} can not be encoded."
38   ;
40   extraConfig = hostName: cfg: let
41     settings = mapAttrsToList (k: v: "${k}=${mkPhpValue v}") cfg.settings;
42   in pkgs.writeText "extraConfig.php" (concatStringsSep "\n" settings);
44   pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
45     pname = "invoiceplane-${hostName}";
46     version = src.version;
47     src = pkgs.invoiceplane;
49     postPatch = ''
50       # Patch index.php file to load additional config file
51       substituteInPlace index.php \
52         --replace-fail "require('vendor/autoload.php');" "require('vendor/autoload.php'); \$dotenv = Dotenv\Dotenv::createImmutable(__DIR__, 'extraConfig.php'); \$dotenv->load();";
53     '';
55     installPhase = ''
56       mkdir -p $out
57       cp -r * $out/
59       # symlink uploads and log directories
60       rm -r $out/uploads $out/application/logs $out/vendor/mpdf/mpdf/tmp
61       ln -sf ${cfg.stateDir}/uploads $out/
62       ln -sf ${cfg.stateDir}/logs $out/application/
63       ln -sf ${cfg.stateDir}/tmp $out/vendor/mpdf/mpdf/
65       # symlink the InvoicePlane config
66       ln -s ${cfg.stateDir}/ipconfig.php $out/ipconfig.php
68       # symlink the extraConfig file
69       ln -s ${extraConfig hostName cfg} $out/extraConfig.php
71       # symlink additional templates
72       ${concatMapStringsSep "\n" (template: "cp -r ${template}/. $out/application/views/invoice_templates/pdf/") cfg.invoiceTemplates}
73     '';
74   };
76   siteOpts = { lib, name, ... }:
77     {
78       options = {
80         enable = mkEnableOption "InvoicePlane web application";
82         stateDir = mkOption {
83           type = types.path;
84           default = "/var/lib/invoiceplane/${name}";
85           description = ''
86             This directory is used for uploads of attachments and cache.
87             The directory passed here is automatically created and permissions
88             adjusted as required.
89           '';
90         };
92         database = {
93           host = mkOption {
94             type = types.str;
95             default = "localhost";
96             description = "Database host address.";
97           };
99           port = mkOption {
100             type = types.port;
101             default = 3306;
102             description = "Database host port.";
103           };
105           name = mkOption {
106             type = types.str;
107             default = "invoiceplane";
108             description = "Database name.";
109           };
111           user = mkOption {
112             type = types.str;
113             default = "invoiceplane";
114             description = "Database user.";
115           };
117           passwordFile = mkOption {
118             type = types.nullOr types.path;
119             default = null;
120             example = "/run/keys/invoiceplane-dbpassword";
121             description = ''
122               A file containing the password corresponding to
123               {option}`database.user`.
124             '';
125           };
127           createLocally = mkOption {
128             type = types.bool;
129             default = true;
130             description = "Create the database and database user locally.";
131           };
132         };
134         invoiceTemplates = mkOption {
135           type = types.listOf types.path;
136           default = [];
137           description = ''
138             List of path(s) to respective template(s) which are copied from the 'invoice_templates/pdf' directory.
140             ::: {.note}
141             These templates need to be packaged before use, see example.
142             :::
143           '';
144           example = literalExpression ''
145             let
146               # Let's package an example template
147               template-vtdirektmarketing = pkgs.stdenv.mkDerivation {
148                 name = "vtdirektmarketing";
149                 # Download the template from a public repository
150                 src = pkgs.fetchgit {
151                   url = "https://git.project-insanity.org/onny/invoiceplane-vtdirektmarketing.git";
152                   sha256 = "1hh0q7wzsh8v8x03i82p6qrgbxr4v5fb05xylyrpp975l8axyg2z";
153                 };
154                 sourceRoot = ".";
155                 # Installing simply means copying template php file to the output directory
156                 installPhase = ""
157                   mkdir -p $out
158                   cp invoiceplane-vtdirektmarketing/vtdirektmarketing.php $out/
159                 "";
160               };
161             # And then pass this package to the template list like this:
162             in [ template-vtdirektmarketing ]
163           '';
164         };
166         poolConfig = mkOption {
167           type = with types; attrsOf (oneOf [ str int bool ]);
168           default = {
169             "pm" = "dynamic";
170             "pm.max_children" = 32;
171             "pm.start_servers" = 2;
172             "pm.min_spare_servers" = 2;
173             "pm.max_spare_servers" = 4;
174             "pm.max_requests" = 500;
175           };
176           description = ''
177             Options for the InvoicePlane PHP pool. See the documentation on `php-fpm.conf`
178             for details on configuration directives.
179           '';
180         };
182         settings = mkOption {
183           type = types.attrsOf types.anything;
184           default = {};
185           description = ''
186             Structural InvoicePlane configuration. Refer to
187             <https://github.com/InvoicePlane/InvoicePlane/blob/master/ipconfig.php.example>
188             for details and supported values.
189           '';
190           example = literalExpression ''
191             {
192               SETUP_COMPLETED = true;
193               DISABLE_SETUP = true;
194               IP_URL = "https://invoice.example.com";
195             }
196           '';
197         };
199         cron = {
200           enable = mkOption {
201             type = types.bool;
202             default = false;
203             description = ''
204               Enable cron service which periodically runs Invoiceplane tasks.
205               Requires key taken from the administration page. Refer to
206               <https://wiki.invoiceplane.com/en/1.0/modules/recurring-invoices>
207               on how to configure it.
208             '';
209           };
210           key = mkOption {
211             type = types.str;
212             description = "Cron key taken from the administration page.";
213           };
214         };
216       };
218     };
221   # interface
222   options = {
223     services.invoiceplane = mkOption {
224       type = types.submodule {
226         options.sites = mkOption {
227           type = types.attrsOf (types.submodule siteOpts);
228           default = {};
229           description = "Specification of one or more WordPress sites to serve";
230         };
232         options.webserver = mkOption {
233           type = types.enum [ "caddy" "nginx" ];
234           default = "caddy";
235           example = "nginx";
236           description = ''
237             Which webserver to use for virtual host management.
238           '';
239         };
240       };
241       default = {};
242       description = "InvoicePlane configuration.";
243     };
245   };
247   # implementation
248   config = mkIf (eachSite != {}) (mkMerge [{
250     assertions = flatten (mapAttrsToList (hostName: cfg: [
251       { assertion = cfg.database.createLocally -> cfg.database.user == user;
252         message = ''services.invoiceplane.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned'';
253       }
254       { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
255         message = ''services.invoiceplane.sites."${hostName}".database.passwordFile cannot be specified if services.invoiceplane.sites."${hostName}".database.createLocally is set to true.'';
256       }
257       { assertion = cfg.cron.enable -> cfg.cron.key != null;
258         message = ''services.invoiceplane.sites."${hostName}".cron.key must be set in order to use cron service.'';
259       }
260     ]) eachSite);
262     services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
263       enable = true;
264       package = mkDefault pkgs.mariadb;
265       ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite;
266       ensureUsers = mapAttrsToList (hostName: cfg:
267         { name = cfg.database.user;
268           ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
269         }
270       ) eachSite;
271     };
273     services.phpfpm = {
274       phpPackage = pkgs.php81;
275       pools = mapAttrs' (hostName: cfg: (
276         nameValuePair "invoiceplane-${hostName}" {
277           inherit user;
278           group = webserver.group;
279           settings = {
280             "listen.owner" = webserver.user;
281             "listen.group" = webserver.group;
282           } // cfg.poolConfig;
283         }
284       )) eachSite;
285     };
287   }
289   {
291     systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
292       "d ${cfg.stateDir} 0750 ${user} ${webserver.group} - -"
293       "f ${cfg.stateDir}/ipconfig.php 0750 ${user} ${webserver.group} - -"
294       "d ${cfg.stateDir}/logs 0750 ${user} ${webserver.group} - -"
295       "d ${cfg.stateDir}/uploads 0750 ${user} ${webserver.group} - -"
296       "d ${cfg.stateDir}/uploads/archive 0750 ${user} ${webserver.group} - -"
297       "d ${cfg.stateDir}/uploads/customer_files 0750 ${user} ${webserver.group} - -"
298       "d ${cfg.stateDir}/uploads/temp 0750 ${user} ${webserver.group} - -"
299       "d ${cfg.stateDir}/uploads/temp/mpdf 0750 ${user} ${webserver.group} - -"
300       "d ${cfg.stateDir}/tmp 0750 ${user} ${webserver.group} - -"
301     ]) eachSite);
303     systemd.services.invoiceplane-config = {
304       serviceConfig.Type = "oneshot";
305       script = concatStrings (mapAttrsToList (hostName: cfg:
306         ''
307           mkdir -p ${cfg.stateDir}/logs \
308                    ${cfg.stateDir}/uploads
309           if ! grep -q IP_URL "${cfg.stateDir}/ipconfig.php"; then
310             cp "${invoiceplane-config hostName cfg}" "${cfg.stateDir}/ipconfig.php"
311           fi
312         '') eachSite);
313       wantedBy = [ "multi-user.target" ];
314     };
316     users.users.${user} = {
317       group = webserver.group;
318       isSystemUser = true;
319     };
321   }
322   {
324     # Cron service implementation
326     systemd.timers = mapAttrs' (hostName: cfg: (
327       nameValuePair "invoiceplane-cron-${hostName}" (mkIf cfg.cron.enable {
328         wantedBy = [ "timers.target" ];
329         timerConfig = {
330           OnBootSec = "5m";
331           OnUnitActiveSec = "5m";
332           Unit = "invoiceplane-cron-${hostName}.service";
333         };
334       })
335     )) eachSite;
337     systemd.services =
338       mapAttrs' (hostName: cfg: (
339         nameValuePair "invoiceplane-cron-${hostName}" (mkIf cfg.cron.enable {
340           serviceConfig = {
341             Type = "oneshot";
342             User = user;
343             ExecStart = "${pkgs.curl}/bin/curl --header 'Host: ${hostName}' http://localhost/invoices/cron/recur/${cfg.cron.key}";
344           };
345         })
346     )) eachSite;
348   }
350   (mkIf (cfg.webserver == "caddy") {
351     services.caddy = {
352       enable = true;
353       virtualHosts = mapAttrs' (hostName: cfg: (
354         nameValuePair "http://${hostName}" {
355           extraConfig = ''
356             root * ${pkg hostName cfg}
357             file_server
358             php_fastcgi unix/${config.services.phpfpm.pools."invoiceplane-${hostName}".socket}
359           '';
360         }
361       )) eachSite;
362     };
363   })
365   (mkIf (cfg.webserver == "nginx") {
366     services.nginx = {
367       enable = true;
368       virtualHosts = mapAttrs' (hostName: cfg: (
369         nameValuePair hostName {
370           root = pkg hostName cfg;
371           extraConfig = ''
372             index index.php index.html index.htm;
374             if (!-e $request_filename){
375               rewrite ^(.*)$ /index.php break;
376             }
377           '';
379           locations = {
380             "/setup".extraConfig = ''
381               rewrite ^(.*)$ http://${hostName}/ redirect;
382             '';
384             "~ .php$" = {
385               extraConfig = ''
386                 fastcgi_split_path_info ^(.+\.php)(/.+)$;
387                 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
388                 fastcgi_pass unix:${config.services.phpfpm.pools."invoiceplane-${hostName}".socket};
389                 include ${config.services.nginx.package}/conf/fastcgi_params;
390                 include ${config.services.nginx.package}/conf/fastcgi.conf;
391               '';
392             };
393           };
394         }
395       )) eachSite;
396     };
397   })
399   ]);