1 { config, pkgs, lib, ... }:
6 cfg = config.services.invoiceplane;
9 webserver = config.services.${cfg.webserver};
11 invoiceplane-config = hostName: cfg: pkgs.writeText "ipconfig.php" ''
12 IP_URL=http://${hostName}
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
26 ENCRYPTION_CIPHER=AES-256
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."
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;
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();";
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}
76 siteOpts = { lib, name, ... }:
80 enable = mkEnableOption "InvoicePlane web application";
84 default = "/var/lib/invoiceplane/${name}";
86 This directory is used for uploads of attachments and cache.
87 The directory passed here is automatically created and permissions
95 default = "localhost";
96 description = "Database host address.";
102 description = "Database host port.";
107 default = "invoiceplane";
108 description = "Database name.";
113 default = "invoiceplane";
114 description = "Database user.";
117 passwordFile = mkOption {
118 type = types.nullOr types.path;
120 example = "/run/keys/invoiceplane-dbpassword";
122 A file containing the password corresponding to
123 {option}`database.user`.
127 createLocally = mkOption {
130 description = "Create the database and database user locally.";
134 invoiceTemplates = mkOption {
135 type = types.listOf types.path;
138 List of path(s) to respective template(s) which are copied from the 'invoice_templates/pdf' directory.
141 These templates need to be packaged before use, see example.
144 example = literalExpression ''
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";
155 # Installing simply means copying template php file to the output directory
158 cp invoiceplane-vtdirektmarketing/vtdirektmarketing.php $out/
161 # And then pass this package to the template list like this:
162 in [ template-vtdirektmarketing ]
166 poolConfig = mkOption {
167 type = with types; attrsOf (oneOf [ str int bool ]);
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;
177 Options for the InvoicePlane PHP pool. See the documentation on `php-fpm.conf`
178 for details on configuration directives.
182 settings = mkOption {
183 type = types.attrsOf types.anything;
186 Structural InvoicePlane configuration. Refer to
187 <https://github.com/InvoicePlane/InvoicePlane/blob/master/ipconfig.php.example>
188 for details and supported values.
190 example = literalExpression ''
192 SETUP_COMPLETED = true;
193 DISABLE_SETUP = true;
194 IP_URL = "https://invoice.example.com";
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.
212 description = "Cron key taken from the administration page.";
223 services.invoiceplane = mkOption {
224 type = types.submodule {
226 options.sites = mkOption {
227 type = types.attrsOf (types.submodule siteOpts);
229 description = "Specification of one or more WordPress sites to serve";
232 options.webserver = mkOption {
233 type = types.enum [ "caddy" "nginx" ];
237 Which webserver to use for virtual host management.
242 description = "InvoicePlane configuration.";
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'';
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.'';
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.'';
262 services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
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"; };
274 phpPackage = pkgs.php81;
275 pools = mapAttrs' (hostName: cfg: (
276 nameValuePair "invoiceplane-${hostName}" {
278 group = webserver.group;
280 "listen.owner" = webserver.user;
281 "listen.group" = webserver.group;
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} - -"
303 systemd.services.invoiceplane-config = {
304 serviceConfig.Type = "oneshot";
305 script = concatStrings (mapAttrsToList (hostName: cfg:
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"
313 wantedBy = [ "multi-user.target" ];
316 users.users.${user} = {
317 group = webserver.group;
324 # Cron service implementation
326 systemd.timers = mapAttrs' (hostName: cfg: (
327 nameValuePair "invoiceplane-cron-${hostName}" (mkIf cfg.cron.enable {
328 wantedBy = [ "timers.target" ];
331 OnUnitActiveSec = "5m";
332 Unit = "invoiceplane-cron-${hostName}.service";
338 mapAttrs' (hostName: cfg: (
339 nameValuePair "invoiceplane-cron-${hostName}" (mkIf cfg.cron.enable {
343 ExecStart = "${pkgs.curl}/bin/curl --header 'Host: ${hostName}' http://localhost/invoices/cron/recur/${cfg.cron.key}";
350 (mkIf (cfg.webserver == "caddy") {
353 virtualHosts = mapAttrs' (hostName: cfg: (
354 nameValuePair "http://${hostName}" {
356 root * ${pkg hostName cfg}
358 php_fastcgi unix/${config.services.phpfpm.pools."invoiceplane-${hostName}".socket}
365 (mkIf (cfg.webserver == "nginx") {
368 virtualHosts = mapAttrs' (hostName: cfg: (
369 nameValuePair hostName {
370 root = pkg hostName cfg;
372 index index.php index.html index.htm;
374 if (!-e $request_filename){
375 rewrite ^(.*)$ /index.php break;
380 "/setup".extraConfig = ''
381 rewrite ^(.*)$ http://${hostName}/ redirect;
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;