at 23.11-pre 12 kB view raw
1{ config, pkgs, lib, ... }: 2 3with lib; 4 5let 6 cfg = config.services.invoiceplane; 7 eachSite = cfg.sites; 8 user = "invoiceplane"; 9 webserver = config.services.${cfg.webserver}; 10 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=${if cfg.database.passwordFile == null then "" else "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 ''; 30 31 extraConfig = hostName: cfg: pkgs.writeText "extraConfig.php" '' 32 ${toString cfg.extraConfig} 33 ''; 34 35 pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec { 36 pname = "invoiceplane-${hostName}"; 37 version = src.version; 38 src = pkgs.invoiceplane; 39 40 postPhase = '' 41 # Patch index.php file to load additional config file 42 substituteInPlace index.php \ 43 --replace "require('vendor/autoload.php');" "require('vendor/autoload.php'); \$dotenv = Dotenv\Dotenv::createImmutable(__DIR__, 'extraConfig.php'); \$dotenv->load();"; 44 ''; 45 46 installPhase = '' 47 mkdir -p $out 48 cp -r * $out/ 49 50 # symlink uploads and log directories 51 rm -r $out/uploads $out/application/logs $out/vendor/mpdf/mpdf/tmp 52 ln -sf ${cfg.stateDir}/uploads $out/ 53 ln -sf ${cfg.stateDir}/logs $out/application/ 54 ln -sf ${cfg.stateDir}/tmp $out/vendor/mpdf/mpdf/ 55 56 # symlink the InvoicePlane config 57 ln -s ${cfg.stateDir}/ipconfig.php $out/ipconfig.php 58 59 # symlink the extraConfig file 60 ln -s ${extraConfig hostName cfg} $out/extraConfig.php 61 62 # symlink additional templates 63 ${concatMapStringsSep "\n" (template: "cp -r ${template}/. $out/application/views/invoice_templates/pdf/") cfg.invoiceTemplates} 64 ''; 65 }; 66 67 siteOpts = { lib, name, ... }: 68 { 69 options = { 70 71 enable = mkEnableOption (lib.mdDoc "InvoicePlane web application"); 72 73 stateDir = mkOption { 74 type = types.path; 75 default = "/var/lib/invoiceplane/${name}"; 76 description = lib.mdDoc '' 77 This directory is used for uploads of attachments and cache. 78 The directory passed here is automatically created and permissions 79 adjusted as required. 80 ''; 81 }; 82 83 database = { 84 host = mkOption { 85 type = types.str; 86 default = "localhost"; 87 description = lib.mdDoc "Database host address."; 88 }; 89 90 port = mkOption { 91 type = types.port; 92 default = 3306; 93 description = lib.mdDoc "Database host port."; 94 }; 95 96 name = mkOption { 97 type = types.str; 98 default = "invoiceplane"; 99 description = lib.mdDoc "Database name."; 100 }; 101 102 user = mkOption { 103 type = types.str; 104 default = "invoiceplane"; 105 description = lib.mdDoc "Database user."; 106 }; 107 108 passwordFile = mkOption { 109 type = types.nullOr types.path; 110 default = null; 111 example = "/run/keys/invoiceplane-dbpassword"; 112 description = lib.mdDoc '' 113 A file containing the password corresponding to 114 {option}`database.user`. 115 ''; 116 }; 117 118 createLocally = mkOption { 119 type = types.bool; 120 default = true; 121 description = lib.mdDoc "Create the database and database user locally."; 122 }; 123 }; 124 125 invoiceTemplates = mkOption { 126 type = types.listOf types.path; 127 default = []; 128 description = lib.mdDoc '' 129 List of path(s) to respective template(s) which are copied from the 'invoice_templates/pdf' directory. 130 131 ::: {.note} 132 These templates need to be packaged before use, see example. 133 ::: 134 ''; 135 example = literalExpression '' 136 let 137 # Let's package an example template 138 template-vtdirektmarketing = pkgs.stdenv.mkDerivation { 139 name = "vtdirektmarketing"; 140 # Download the template from a public repository 141 src = pkgs.fetchgit { 142 url = "https://git.project-insanity.org/onny/invoiceplane-vtdirektmarketing.git"; 143 sha256 = "1hh0q7wzsh8v8x03i82p6qrgbxr4v5fb05xylyrpp975l8axyg2z"; 144 }; 145 sourceRoot = "."; 146 # Installing simply means copying template php file to the output directory 147 installPhase = "" 148 mkdir -p $out 149 cp invoiceplane-vtdirektmarketing/vtdirektmarketing.php $out/ 150 ""; 151 }; 152 # And then pass this package to the template list like this: 153 in [ template-vtdirektmarketing ] 154 ''; 155 }; 156 157 poolConfig = mkOption { 158 type = with types; attrsOf (oneOf [ str int bool ]); 159 default = { 160 "pm" = "dynamic"; 161 "pm.max_children" = 32; 162 "pm.start_servers" = 2; 163 "pm.min_spare_servers" = 2; 164 "pm.max_spare_servers" = 4; 165 "pm.max_requests" = 500; 166 }; 167 description = lib.mdDoc '' 168 Options for the InvoicePlane PHP pool. See the documentation on `php-fpm.conf` 169 for details on configuration directives. 170 ''; 171 }; 172 173 extraConfig = mkOption { 174 type = types.nullOr types.lines; 175 default = null; 176 example = '' 177 SETUP_COMPLETED=true 178 DISABLE_SETUP=true 179 IP_URL=https://invoice.example.com 180 ''; 181 description = lib.mdDoc '' 182 InvoicePlane configuration. Refer to 183 <https://github.com/InvoicePlane/InvoicePlane/blob/master/ipconfig.php.example> 184 for details on supported values. 185 ''; 186 }; 187 188 cron = { 189 190 enable = mkOption { 191 type = types.bool; 192 default = false; 193 description = lib.mdDoc '' 194 Enable cron service which periodically runs Invoiceplane tasks. 195 Requires key taken from the administration page. Refer to 196 <https://wiki.invoiceplane.com/en/1.0/modules/recurring-invoices> 197 on how to configure it. 198 ''; 199 }; 200 201 key = mkOption { 202 type = types.str; 203 description = lib.mdDoc "Cron key taken from the administration page."; 204 }; 205 206 }; 207 208 }; 209 210 }; 211in 212{ 213 # interface 214 options = { 215 services.invoiceplane = mkOption { 216 type = types.submodule { 217 218 options.sites = mkOption { 219 type = types.attrsOf (types.submodule siteOpts); 220 default = {}; 221 description = lib.mdDoc "Specification of one or more WordPress sites to serve"; 222 }; 223 224 options.webserver = mkOption { 225 type = types.enum [ "caddy" ]; 226 default = "caddy"; 227 description = lib.mdDoc '' 228 Which webserver to use for virtual host management. Currently only 229 caddy is supported. 230 ''; 231 }; 232 }; 233 default = {}; 234 description = lib.mdDoc "InvoicePlane configuration."; 235 }; 236 237 }; 238 239 # implementation 240 config = mkIf (eachSite != {}) (mkMerge [{ 241 242 assertions = flatten (mapAttrsToList (hostName: cfg: 243 [{ assertion = cfg.database.createLocally -> cfg.database.user == user; 244 message = ''services.invoiceplane.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned''; 245 } 246 { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null; 247 message = ''services.invoiceplane.sites."${hostName}".database.passwordFile cannot be specified if services.invoiceplane.sites."${hostName}".database.createLocally is set to true.''; 248 } 249 { assertion = cfg.cron.enable -> cfg.cron.key != null; 250 message = ''services.invoiceplane.sites."${hostName}".cron.key must be set in order to use cron service.''; 251 } 252 ]) eachSite); 253 254 services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) { 255 enable = true; 256 package = mkDefault pkgs.mariadb; 257 ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite; 258 ensureUsers = mapAttrsToList (hostName: cfg: 259 { name = cfg.database.user; 260 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; 261 } 262 ) eachSite; 263 }; 264 265 services.phpfpm = { 266 phpPackage = pkgs.php81; 267 pools = mapAttrs' (hostName: cfg: ( 268 nameValuePair "invoiceplane-${hostName}" { 269 inherit user; 270 group = webserver.group; 271 settings = { 272 "listen.owner" = webserver.user; 273 "listen.group" = webserver.group; 274 } // cfg.poolConfig; 275 } 276 )) eachSite; 277 }; 278 279 } 280 281 { 282 283 systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [ 284 "d ${cfg.stateDir} 0750 ${user} ${webserver.group} - -" 285 "f ${cfg.stateDir}/ipconfig.php 0750 ${user} ${webserver.group} - -" 286 "d ${cfg.stateDir}/logs 0750 ${user} ${webserver.group} - -" 287 "d ${cfg.stateDir}/uploads 0750 ${user} ${webserver.group} - -" 288 "d ${cfg.stateDir}/uploads/archive 0750 ${user} ${webserver.group} - -" 289 "d ${cfg.stateDir}/uploads/customer_files 0750 ${user} ${webserver.group} - -" 290 "d ${cfg.stateDir}/uploads/temp 0750 ${user} ${webserver.group} - -" 291 "d ${cfg.stateDir}/uploads/temp/mpdf 0750 ${user} ${webserver.group} - -" 292 "d ${cfg.stateDir}/tmp 0750 ${user} ${webserver.group} - -" 293 ]) eachSite); 294 295 systemd.services.invoiceplane-config = { 296 serviceConfig.Type = "oneshot"; 297 script = concatStrings (mapAttrsToList (hostName: cfg: 298 '' 299 mkdir -p ${cfg.stateDir}/logs \ 300 ${cfg.stateDir}/uploads 301 if ! grep -q IP_URL "${cfg.stateDir}/ipconfig.php"; then 302 cp "${invoiceplane-config hostName cfg}" "${cfg.stateDir}/ipconfig.php" 303 fi 304 '') eachSite); 305 wantedBy = [ "multi-user.target" ]; 306 }; 307 308 users.users.${user} = { 309 group = webserver.group; 310 isSystemUser = true; 311 }; 312 313 } 314 { 315 316 # Cron service implementation 317 318 systemd.timers = mapAttrs' (hostName: cfg: ( 319 nameValuePair "invoiceplane-cron-${hostName}" (mkIf cfg.cron.enable { 320 wantedBy = [ "timers.target" ]; 321 timerConfig = { 322 OnBootSec = "5m"; 323 OnUnitActiveSec = "5m"; 324 Unit = "invoiceplane-cron-${hostName}.service"; 325 }; 326 }) 327 )) eachSite; 328 329 systemd.services = 330 mapAttrs' (hostName: cfg: ( 331 nameValuePair "invoiceplane-cron-${hostName}" (mkIf cfg.cron.enable { 332 serviceConfig = { 333 Type = "oneshot"; 334 User = user; 335 ExecStart = "${pkgs.curl}/bin/curl --header 'Host: ${hostName}' http://localhost/invoices/cron/recur/${cfg.cron.key}"; 336 }; 337 }) 338 )) eachSite; 339 340 } 341 342 (mkIf (cfg.webserver == "caddy") { 343 services.caddy = { 344 enable = true; 345 virtualHosts = mapAttrs' (hostName: cfg: ( 346 nameValuePair "http://${hostName}" { 347 extraConfig = '' 348 root * ${pkg hostName cfg} 349 file_server 350 php_fastcgi unix/${config.services.phpfpm.pools."invoiceplane-${hostName}".socket} 351 ''; 352 } 353 )) eachSite; 354 }; 355 }) 356 357 ]); 358}