From 60640a502387e76b27a258624dc9138cf9552f55 Mon Sep 17 00:00:00 2001 From: awkawb Date: Fri, 4 Oct 2024 09:56:33 -0400 Subject: [PATCH 01/10] Work on Invoice Ninja v5.10.29 * Module now uses caddy instead of nginx * Module now has adminEmail and adminPassword options --- Makefile | 10 +- config-filesystems.patch | 13 +++ default.nix | 26 ++---- invoice-ninja.nix | 192 ++++++++++++++++----------------------- tests/test-config.nix | 2 +- 5 files changed, 103 insertions(+), 140 deletions(-) create mode 100644 config-filesystems.patch diff --git a/Makefile b/Makefile index 3f6b092..1a5f31b 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,7 @@ # Since all targets are phony, all targets should be listed here # One target per line -.PHONY: boot-graphical-vm \ - boot-vm \ +.PHONY: boot-vm \ build-vm \ clean \ format-nix-files \ @@ -29,8 +28,8 @@ format-nix-files: ## Format nix files using the nixpkgs-fmt tool find . -name "*.nix" -exec nixpkgs-fmt {} \; clean: ## Clean build artifacts and shutdown running virtual machines - rm result - rm nixos.qcow2 + rm result > /dev/null 2>&1 + rm nixos.qcow2 > /dev/null 2>&1 pkill qemu exit 0 @@ -46,6 +45,3 @@ boot-vm: ## Run virtual machine in current terminal -nographic; \ reset -boot-graphical-vm: ## Run virtual machine in QEMU window - ./result/bin/run-nixos-vm - diff --git a/config-filesystems.patch b/config-filesystems.patch new file mode 100644 index 0000000..5e15a0c --- /dev/null +++ b/config-filesystems.patch @@ -0,0 +1,13 @@ +diff --git a/config/filesystems.php b/config/filesystems.php +index a104af7a81..a4c87ba3ff 100644 +--- a/config/filesystems.php ++++ b/config/filesystems.php +@@ -37,7 +37,7 @@ return [ + 'root' => base_path(), + 'permissions' => [ + 'file' => [ +- 'public' => 0664, ++ 'public' => 0444, + 'private' => 0600, + ], + 'dir' => [ diff --git a/default.nix b/default.nix index 6defb92..fa54cdb 100644 --- a/default.nix +++ b/default.nix @@ -8,25 +8,23 @@ }: let - # Helper script to generate an APP_KEY for .env - generate-invoice-ninja-app-key = writers.writeBashBin "generate-laravel-key" '' - echo "APP_KEY=base64:$(${openssl}/bin/openssl rand -base64 32)" - ''; + configFilesystemsPatch = "./config-filesystems.patch"; in php.buildComposerProject (finalAttrs: { pname = "invoice-ninja"; - version = "5.10.3"; + version = "5.10.29"; src = fetchFromGitHub { owner = "invoiceninja"; repo = "invoiceninja"; rev = "v${finalAttrs.version}"; - hash = "sha256-QL6L+yT1yRQUTTGYGjaC4zbvzgw4ozgJSP2bYJCf014="; + hash = "sha256-nhLt3DXW0q07ZhDq23mHwbVmqHZor+p925/yrKXum54="; }; - vendorHash = "sha256-LGNBgaWWX2a8w9uE3+fVtBDqgbcv69FNnka4HjZKqsQ="; + vendorHash = "sha256-NVvx1aKhbC5XuXt2+gS2c3ulNWoCKrYNnEleBuAcftQ="; - propagatedBuildInput = [ generate-invoice-ninja-app-key ]; + # Patch sources to allow more restrictive permissions + patches = [ ./config-filesystems.patch ]; # Upstream composer.json has invalid license, webpatser/laravel-countries package is pointing # to commit-ref, and php required in require and require-dev @@ -36,21 +34,17 @@ php.buildComposerProject (finalAttrs: { mv "$out/share/php/${finalAttrs.pname}"/* $out rm -R $out/bootstrap/cache + rm -rf $out/public/storage + # Move static contents for the NixOS module to pick it up, if needed. mv $out/bootstrap $out/bootstrap-static mv $out/storage $out/storage-static - mv $out/vite.config.ts $out/vite.config.ts.static - mv $out/public/images $out/public/images-static - mv $out/public/react $out/public/react-static - ln -s ${dataDir}/public/images $out/public/ - ln -s ${dataDir}/public/react $out/public/ - ln -s ${dataDir}/vite.config.ts $out/ + # Link NixOS module files to derivation output ln -s ${dataDir}/.env $out/.env ln -s ${dataDir}/storage $out/ ln -s ${runtimeDir} $out/bootstrap - - chmod +x $out/artisan + ln -s ${dataDir}/storage/app/public $out/public/storage ''; meta = { diff --git a/invoice-ninja.nix b/invoice-ninja.nix index 2ca693b..9e515c6 100644 --- a/invoice-ninja.nix +++ b/invoice-ninja.nix @@ -11,7 +11,7 @@ let # PHP environment phpPackage = cfg.phpPackage.buildEnv { extensions = { enabled, all }: enabled ++ (with all; - [ bcmath ctype curl fileinfo gd gmp iconv mbstring mysqli openssl pdo tokenizer zip ] + [ bcmath ctype curl fileinfo gd gmp iconv imagick mbstring mysqli openssl pdo tokenizer zip ] ); extraConfig = "memory_limit = 1024M"; @@ -141,6 +141,18 @@ in ''; }; + adminEmail = lib.mkOption { + type = lib.types.str; + default = "example@email.com"; + description = "Email address of the first (admin) account for this Invoice Ninja installation"; + }; + + adminPassword = lib.mkOption { + type = lib.types.str; + default = "example"; + description = "Password of the first (admin) account for this Invoice Ninja installation"; + }; + database = { createLocally = lib.mkOption { type = lib.types.bool; @@ -155,31 +167,12 @@ in }; }; - enableACME = lib.mkOption { + caddy = lib.mkOption { type = lib.types.bool; - default = false; + default = true; description = '' - Whether an ACME certificate should be used to secure connections to the server. - ''; - }; - - nginx = lib.mkOption { - type = lib.types.nullOr (lib.types.submodule - (import { - inherit config lib; - })); - default = null; - example = '' - { - enableACME = true; - forceHttps = true; - } - ''; - description = '' - With this option, you can customize an nginx virtual host which already has sensible defaults - for Invoice Ninja. Set to {} if you do not need any customization to the virtual host. - If enabled, then by default, the {option}`serverName` is `''${domain}`. If this is set to - null (the default), no nginx virtualHost will be configured. + Whether to enable a preconfigured Caddy server to serve + Invoice Ninja. ''; }; }; @@ -225,6 +218,7 @@ in NINJA_ENVIRONMENT = lib.mkDefault "selfhost"; PDF_GENERATOR = lib.mkDefault "snappdf"; SNAPPDF_CHROMIUM_PATH = lib.mkDefault "${chromium}/bin/chromium"; + PRECONFIGURED_INSTALL = lib.mkDefault true; }) (lib.mkIf (cfg.database.createLocally) { DB_CONNECTION = lib.mkDefault "mysql"; @@ -235,78 +229,30 @@ in }) ]; - services.phpfpm.pools.invoiceninja = { + services.phpfpm.pools.invoice-ninja = { inherit user group phpPackage; inherit (cfg.phpfpm) settings; }; - users.users."${config.services.nginx.user}" = lib.mkIf (cfg.nginx != null) { extraGroups = [ cfg.group ]; }; - services.nginx = lib.mkIf (cfg.nginx != null) { + users.users."${config.services.caddy.user}" = lib.mkIf (cfg.caddy != false) { extraGroups = [ cfg.group ]; }; + services.caddy = + let + caddyDomain = if (cfg.domain == "localhost") then ":80" else cfg.domain; + in + lib.mkIf (cfg.caddy != false) { enable = true; - clientMaxBodySize = "20m"; + globalConfig = lib.mkIf (cfg.domain == "localhost") '' + auto_https disable_redirects + ''; - recommendedGzipSettings = true; - recommendedOptimisation = true; - recommendedProxySettings = true; - recommendedTlsSettings = true; - - appendHttpConfig = '' - # Add HSTS header with preloading to HTTPS requests. - # Adding this header to HTTP requests is discouraged - map $scheme $hsts_header { - https "max-age=31536000; includeSubdomains; preload"; - } - add_header Strict-Transport-Security $hsts_header; - - # Minimize information leaked to other domains - add_header 'Referrer-Policy' 'origin-when-cross-origin'; - - # Disable embedding as a frame - add_header X-Frame-Options DENY; - - # Prevent injection of code in other mime types (XSS Attacks) - add_header X-Content-Type-Options nosniff; - - # This might create errors - proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; - ''; - - virtualHosts."${cfg.domain}" = lib.mkMerge [ - cfg.nginx - { - inherit (cfg) enableACME; - forceSSL = lib.mkDefault (if cfg.enableACME then true else false); - root = lib.mkForce "${invoice-ninja}/public/"; - locations."= /index.php" = { - extraConfig = '' - fastcgi_pass unix:${config.services.phpfpm.pools.invoiceninja.socket}; - fastcgi_index index.php; - ''; - }; - locations."~ \\.php$" = { - return = 403; - }; - locations."/" = { - tryFiles = "$uri $uri/ /index.php?$query_string"; - extraConfig = '' - # Add your rewrite rule for non-existent files - if (!-e $request_filename) { - rewrite ^(.+)$ /index.php?q=$1 last; - } - ''; - }; - locations."~ /\\.ht" = { - extraConfig = '' - deny all; - ''; - }; - extraConfig = '' - index index.php index.html index.htm; - error_page 404 /index.php; - ''; - } - ]; + virtualHosts."${caddyDomain}".extraConfig = '' + encode zstd gzip + root * ${invoice-ninja}/public/ + php_fastcgi unix/${config.services.phpfpm.pools.invoice-ninja.socket} + try_files {path} {path}/ /public/{path} /index.php?{query} + file_server + ''; }; services.mysql = lib.mkIf (cfg.database.createLocally) { @@ -354,7 +300,7 @@ in wantedBy = [ "multi-user.target" ]; after = lib.optional cfg.database.createLocally "mysql.service"; requires = lib.optional cfg.database.createLocally "mysql.service"; - path = with pkgs; [ bash invoice-ninja-manage rsync ] ++ extraPrograms; + path = with pkgs; [ bash invoice-ninja-manage rsync config.services.mysql.package ] ++ extraPrograms; serviceConfig = { Type = "oneshot"; @@ -376,37 +322,30 @@ in # Concatenate non-secret .env and secret .env rm -f ${cfg.dataDir}/.env cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env - # echo -e '\n' >> ${cfg.dataDir}/.env + echo -e '\n' >> ${cfg.dataDir}/.env cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env # Link the static storage (package provided) to the runtime storage # Necessary for cities.json and static images. - mkdir -p ${cfg.dataDir}/storage rsync -av --no-perms ${invoice-ninja}/storage-static/ ${cfg.dataDir}/storage - chmod -R +w ${cfg.dataDir}/storage - - chmod g+x ${cfg.dataDir}/storage ${cfg.dataDir}/storage/app - chmod -R g+rX ${cfg.dataDir}/storage/app/public - + # Link the app.php in the runtime folder. # We cannot link the cache folder only because bootstrap folder needs to be writeable. ln -sf ${invoice-ninja}/bootstrap-static/app.php ${cfg.runtimeDir}/app.php - # Link the static public/images (package provided) to the runtime public/public/images - mkdir -p ${cfg.dataDir}/public/images - rsync -av --no-perms ${invoice-ninja}/public/images-static/ ${cfg.dataDir}/public/images - - # Link the static public/react (package provided) to the runtime public/public/react - mkdir -p ${cfg.dataDir}/public/react - rsync -av --no-perms ${invoice-ninja}/public/react-static/ ${cfg.dataDir}/public/react + # Perform the first migration + [[ ! -f ${cfg.dataDir}/.initial-migration ]] && invoice-ninja-manage migrate --force && touch ${cfg.dataDir}/.initial-migration - chmod -R +w ${cfg.dataDir}/public - chmod -R g+rwX ${cfg.dataDir}/public - - # Link the static vite.config.ts (package provided to the runtime vite.config.ts - rsync -av --no-perms ${invoice-ninja}/vite.config.ts.static ${cfg.dataDir}/vite.config.ts - chmod +w ${cfg.dataDir}/vite.config.ts - chmod g+rw ${cfg.dataDir}/vite.config.ts + # Create Invoice Ninja admin account + [[ ! -f ${cfg.dataDir}/.admin-created ]] \ + && invoice-ninja-manage ninja:create-account --email=${cfg.adminEmail} --password=${cfg.adminPassword} \ + && touch ${cfg.dataDir}/.admin-created + + # Recent releases make the React interface default + # Currently this is broken so we switch back to the Flutter interface + [[ ! -f ${cfg.dataDir}/.react-disabled ]] \ + && mysql -D ${cfg.database.name} -e 'UPDATE accounts SET set_react_as_default_ap = 0;' \ + && touch ${cfg.dataDir}/.react-disabled invoice-ninja-manage route:cache invoice-ninja-manage view:cache @@ -414,11 +353,32 @@ in ''; }; - systemd.tmpfiles.rules = [ - # Cache must live across multiple systemd units runtimes. - "d ${cfg.runtimeDir}/ 0700 ${user} ${group} - -" - "d ${cfg.runtimeDir}/cache 0700 ${user} ${group} - -" - ]; + systemd.tmpfiles.settings."10-invoice-ninja" = lib.attrsets.genAttrs [ + cfg.dataDir + "${cfg.dataDir}/storage" + "${cfg.dataDir}/storage/app" + "${cfg.dataDir}/storage/app/public" + "${cfg.dataDir}/storage/framework" + "${cfg.dataDir}/storage/framework/cache" + "${cfg.dataDir}/storage/framework/sessions" + "${cfg.dataDir}/storage/framework/views" + "${cfg.dataDir}/storage/logs" + ] (n: { + d = { + user = user; + group = group; + mode = "0770"; + }; + }) // lib.attrsets.genAttrs [ + cfg.runtimeDir + "${cfg.runtimeDir}/cache" + ] (n: { + d = { + user = user; + group = group; + mode = "0770"; + }; + }); }; } diff --git a/tests/test-config.nix b/tests/test-config.nix index 6e98c4c..f5bbd32 100644 --- a/tests/test-config.nix +++ b/tests/test-config.nix @@ -19,7 +19,7 @@ services.invoice-ninja = { enable = true; database.createLocally = true; - nginx = { }; + caddy = true; secretFile = ./test-secrets.env; }; From 1abeb7dc5a339c96285050c54323d949b795e59f Mon Sep 17 00:00:00 2001 From: awkawb Date: Sun, 6 Oct 2024 13:30:32 -0400 Subject: [PATCH 02/10] Added Nginx as a proxy server option * Nginx is the default proxy server --- default.nix | 5 +- invoice-ninja.nix | 170 ++++++++++++++++++++++++++++++++---------- tests/test-config.nix | 1 - 3 files changed, 132 insertions(+), 44 deletions(-) diff --git a/default.nix b/default.nix index fa54cdb..6cf7ace 100644 --- a/default.nix +++ b/default.nix @@ -7,9 +7,6 @@ , runtimeDir ? "/run/invoice-ninja" }: -let - configFilesystemsPatch = "./config-filesystems.patch"; -in php.buildComposerProject (finalAttrs: { pname = "invoice-ninja"; version = "5.10.29"; @@ -24,7 +21,7 @@ php.buildComposerProject (finalAttrs: { vendorHash = "sha256-NVvx1aKhbC5XuXt2+gS2c3ulNWoCKrYNnEleBuAcftQ="; # Patch sources to allow more restrictive permissions - patches = [ ./config-filesystems.patch ]; + # patches = [ ./config-filesystems.patch ]; # Upstream composer.json has invalid license, webpatser/laravel-countries package is pointing # to commit-ref, and php required in require and require-dev diff --git a/invoice-ninja.nix b/invoice-ninja.nix index 9e515c6..acb4925 100644 --- a/invoice-ninja.nix +++ b/invoice-ninja.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ config +, lib +, pkgs +, modulesPath +, ... }: + let cfg = config.services.invoice-ninja; @@ -90,7 +95,7 @@ in description = "How often the Invoice Ninja cron task should run."; }; - domain = lib.mkOption { + hostName = lib.mkOption { type = lib.types.str; default = "localhost"; description = '' @@ -157,7 +162,7 @@ in createLocally = lib.mkOption { type = lib.types.bool; default = true; - description = "a local database using UNIX socket authentication"; + description = "A local database using UNIX socket authentication"; }; name = lib.mkOption { @@ -167,17 +172,66 @@ in }; }; - caddy = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Whether to enable a preconfigured Caddy server to serve - Invoice Ninja. - ''; + maxUploadSize = lib.mkOption { + type = lib.types.str; + default = "8M"; + description = "Maximum allowed upload size to Invoice Ninja."; + }; + + webserver = { + caddy = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to enable the Caddy server to serve Invoice Ninja."; + }; + + config = lib.mkOption { + type = lib.types.submodule ( + (import (modulesPath + "/services/web-servers/caddy/vhost-options.nix") { cfg = config.services.caddy; }) { + inherit lib; config = cfg; name = (if (cfg.hostName == "localhost") then ":80" else cfg.hostName); + } + ); + default = { }; + description = '' + Extra configuration for the Caddy virtual host of Invoice Ninja. + Set to `{ }` to use the default configuration + ''; + }; + }; + + nginx = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to enable Nginx server to serve Invoice Ninja. + ''; + }; + + config = lib.mkOption { + type = lib.types.submodule ( + (import (modulesPath + "/services/web-servers/nginx/vhost-options.nix") { inherit config lib; }) + ); + default = { }; + description = '' + Extra configuration for the Nginx virtual host of Invoice Ninja. + Set to `{ }` to use the default configuration + ''; + }; + }; }; }; config = lib.mkIf cfg.enable { + # FIXME Caddy and Nginx should be mutually exclusive + assertions = [ + { + assertion = (cfg.webserver.nginx.enable || cfg.webserver.caddy.enable); + message = "Either the Caddy or Nginx webserver needs to be enabled."; + } + ]; + users.users.invoiceninja = lib.mkIf (cfg.user == "invoiceninja") { isSystemUser = true; home = cfg.dataDir; @@ -191,10 +245,10 @@ in services.invoice-ninja.settings = let - app_http_url = "http://${cfg.domain}"; - app_https_url = "https://${cfg.domain}"; - react_http_url = "http://${cfg.domain}:3001"; - react_https_url = "https://${cfg.domain}:3001"; + app_http_url = "http://${cfg.hostName}"; + app_https_url = "https://${cfg.hostName}"; + react_http_url = "http://${cfg.hostName}:3001"; + react_https_url = "https://${cfg.hostName}:3001"; chromium = lib.lists.findSingle (x: x == pkgs.chromium) "none" "multiple" extraPrograms; in lib.mkMerge [ @@ -202,8 +256,8 @@ in APP_NAME = lib.mkDefault "\"Invoice Ninja\""; APP_ENV = lib.mkDefault "production"; APP_DEBUG = lib.mkDefault false; - APP_URL = lib.mkDefault (if (cfg.domain != "localhost") then "${app_https_url}" else "${app_http_url}"); - REACT_URL = lib.mkDefault (if (cfg.domain != "localhost") then "${react_https_url}" else "${react_http_url}"); + APP_URL = lib.mkDefault (if (cfg.hostName != "localhost") then "${app_https_url}" else "${app_http_url}"); + REACT_URL = lib.mkDefault (if (cfg.hostName != "localhost") then "${react_https_url}" else "${react_http_url}"); DB_CONNECTION = lib.mkDefault "mysql"; MULTI_DB_ENABLED = lib.mkDefault false; DEMO_MODE = lib.mkDefault false; @@ -213,7 +267,7 @@ in QUEUE_CONNECTION = lib.mkDefault "database"; SESSION_DRIVER = lib.mkDefault "file"; SESSION_LIFETIME = lib.mkDefault "120"; - REQUIRE_HTTPS = lib.mkDefault (if (cfg.domain != "localhost") then true else false); + REQUIRE_HTTPS = lib.mkDefault (if (cfg.hostName != "localhost") then true else false); TRUSTED_PROXIES = lib.mkDefault "127.0.0.1"; NINJA_ENVIRONMENT = lib.mkDefault "selfhost"; PDF_GENERATOR = lib.mkDefault "snappdf"; @@ -232,27 +286,72 @@ in services.phpfpm.pools.invoice-ninja = { inherit user group phpPackage; inherit (cfg.phpfpm) settings; + + phpOptions = '' + post_max_size = ${cfg.maxUploadSize} + upload_max_filesize = ${cfg.maxUploadSize} + max_execution_time = 600; + ''; }; - users.users."${config.services.caddy.user}" = lib.mkIf (cfg.caddy != false) { extraGroups = [ cfg.group ]; }; - services.caddy = - let - caddyDomain = if (cfg.domain == "localhost") then ":80" else cfg.domain; - in - lib.mkIf (cfg.caddy != false) { - enable = true; + users.users."${config.services.nginx.user}" = lib.mkIf (cfg.webserver.nginx.enable == true) { extraGroups = [ cfg.group ]; }; + services.nginx = lib.mkIf ((cfg.webserver.nginx.enable == true) && (cfg.webserver.caddy.enable == false)) { + inherit (cfg.webserver.nginx) enable; - globalConfig = lib.mkIf (cfg.domain == "localhost") '' + recommendedTlsSettings = true; + recommendedGzipSettings = true; + recommendedProxySettings = true; + recommendedOptimisation = true; + + clientMaxBodySize = cfg.maxUploadSize; + + virtualHosts."${cfg.hostName}" = lib.mkMerge [ + cfg.webserver.nginx.config + { + root = lib.mkForce "${invoice-ninja}/public"; + locations = { + "/" = { + tryFiles = "$uri $uri/ /index.php?$query_string"; + extraConfig = '' + if (!-e $request_filename) { + rewrite ^(.+)$ /index.php?q=$1 last; + } + ''; + }; + "~ \\.php$".extraConfig = '' + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:${config.services.phpfpm.pools.invoice-ninja.socket}; + fastcgi_index index.php; + ''; + "~ /\\.ht".extraConfig = "deny all;"; + }; + extraConfig = '' + index index.html index.htm index.php; + error_page 404 /index.php; + ''; + } + ]; + }; + + users.users."${config.services.caddy.user}" = lib.mkIf (cfg.webserver.caddy.enable == true) { extraGroups = [ cfg.group ]; }; + services.caddy = lib.mkIf ((cfg.webserver.caddy.enable == true) && (cfg.webserver.nginx.enable == false)) { + inherit (cfg.webserver.caddy) enable; + + globalConfig = lib.mkIf (cfg.hostName == "localhost") '' auto_https disable_redirects ''; - virtualHosts."${caddyDomain}".extraConfig = '' - encode zstd gzip - root * ${invoice-ninja}/public/ - php_fastcgi unix/${config.services.phpfpm.pools.invoice-ninja.socket} - try_files {path} {path}/ /public/{path} /index.php?{query} - file_server - ''; + virtualHosts."${cfg.hostName}" = lib.mkMerge [ + cfg.webserver.caddy.config + { + extraConfig = '' + encode zstd gzip + root * ${invoice-ninja}/public + php_fastcgi unix/${config.services.phpfpm.pools.invoice-ninja.socket} + file_server + ''; + } + ]; }; services.mysql = lib.mkIf (cfg.database.createLocally) { @@ -363,13 +462,6 @@ in "${cfg.dataDir}/storage/framework/sessions" "${cfg.dataDir}/storage/framework/views" "${cfg.dataDir}/storage/logs" - ] (n: { - d = { - user = user; - group = group; - mode = "0770"; - }; - }) // lib.attrsets.genAttrs [ cfg.runtimeDir "${cfg.runtimeDir}/cache" ] (n: { diff --git a/tests/test-config.nix b/tests/test-config.nix index f5bbd32..107caee 100644 --- a/tests/test-config.nix +++ b/tests/test-config.nix @@ -19,7 +19,6 @@ services.invoice-ninja = { enable = true; database.createLocally = true; - caddy = true; secretFile = ./test-secrets.env; }; From 5f862fe8d369464ced43c3ed1e75452142cccb21 Mon Sep 17 00:00:00 2001 From: awkawb Date: Sun, 6 Oct 2024 21:48:09 -0400 Subject: [PATCH 03/10] Updated Invoice Ninja to v5.10.31 * Changed php environment packages * Added patch to switch back to Flutter * Added patch to fix storage permissions --- default.nix | 16 ++++-- disable-react-for-admin.patch | 13 +++++ ...ystems.patch => fix-base-permissions.patch | 8 +-- fix-storage-permissions.patch | 32 ++++++++++++ invoice-ninja.nix | 50 +++++++++++-------- tests/test-config.nix | 2 +- 6 files changed, 92 insertions(+), 29 deletions(-) create mode 100644 disable-react-for-admin.patch rename config-filesystems.patch => fix-base-permissions.patch (65%) create mode 100644 fix-storage-permissions.patch diff --git a/default.nix b/default.nix index 6cf7ace..99ae22a 100644 --- a/default.nix +++ b/default.nix @@ -9,19 +9,25 @@ php.buildComposerProject (finalAttrs: { pname = "invoice-ninja"; - version = "5.10.29"; + version = "5.10.31"; src = fetchFromGitHub { owner = "invoiceninja"; repo = "invoiceninja"; rev = "v${finalAttrs.version}"; - hash = "sha256-nhLt3DXW0q07ZhDq23mHwbVmqHZor+p925/yrKXum54="; + hash = "sha256-n3SpyXGUjw+qHEtnrclzIIdQ09UZBI+Rj7MXeUJVk6c="; }; - vendorHash = "sha256-NVvx1aKhbC5XuXt2+gS2c3ulNWoCKrYNnEleBuAcftQ="; + vendorHash = "sha256-krGeVT6GyvkFkgeviA06dRneApEHDK61Tm+hXYTVYCM="; - # Patch sources to allow more restrictive permissions - # patches = [ ./config-filesystems.patch ]; + # Patch sources for more restrictive permissions + patches = [ + ./fix-storage-permissions.patch + ./disable-react-for-admin.patch + + # FIXME this patch should fix "Health Check" file permissions errors + #./fix-base-permissions.patch + ]; # Upstream composer.json has invalid license, webpatser/laravel-countries package is pointing # to commit-ref, and php required in require and require-dev diff --git a/disable-react-for-admin.patch b/disable-react-for-admin.patch new file mode 100644 index 0000000..d3f16f0 --- /dev/null +++ b/disable-react-for-admin.patch @@ -0,0 +1,13 @@ +diff --git a/app/Console/Commands/CreateAccount.php b/app/Console/Commands/CreateAccount.php +index 228f8e8283..1ff3c54a61 100644 +--- a/app/Console/Commands/CreateAccount.php ++++ b/app/Console/Commands/CreateAccount.php +@@ -79,7 +79,7 @@ class CreateAccount extends Command + $company->save(); + + $account->default_company_id = $company->id; +- $account->set_react_as_default_ap = true; ++ $account->set_react_as_default_ap = false; + $account->save(); + + $email = $this->option('email') ?? 'admin@example.com'; diff --git a/config-filesystems.patch b/fix-base-permissions.patch similarity index 65% rename from config-filesystems.patch rename to fix-base-permissions.patch index 5e15a0c..5254613 100644 --- a/config-filesystems.patch +++ b/fix-base-permissions.patch @@ -1,13 +1,15 @@ diff --git a/config/filesystems.php b/config/filesystems.php -index a104af7a81..a4c87ba3ff 100644 +index a104af7a81..3582c519a1 100644 --- a/config/filesystems.php +++ b/config/filesystems.php -@@ -37,7 +37,7 @@ return [ +@@ -37,8 +37,8 @@ return [ 'root' => base_path(), 'permissions' => [ 'file' => [ - 'public' => 0664, +- 'private' => 0600, + 'public' => 0444, - 'private' => 0600, ++ 'private' => 0400, ], 'dir' => [ + 'public' => 0775, diff --git a/fix-storage-permissions.patch b/fix-storage-permissions.patch new file mode 100644 index 0000000..de32b03 --- /dev/null +++ b/fix-storage-permissions.patch @@ -0,0 +1,32 @@ +diff --git a/config/filesystems.php b/config/filesystems.php +index a104af7a81..5294147710 100644 +--- a/config/filesystems.php ++++ b/config/filesystems.php +@@ -53,11 +53,11 @@ return [ + 'root' => storage_path('app'), + 'permissions' => [ + 'file' => [ +- 'public' => 0664, ++ 'public' => 0660, + 'private' => 0600, + ], + 'dir' => [ +- 'public' => 0775, ++ 'public' => 0770, + 'private' => 0700, + ], + ], +@@ -71,11 +71,11 @@ return [ + 'visibility' => 'public', + 'permissions' => [ + 'file' => [ +- 'public' => 0664, ++ 'public' => 0660, + 'private' => 0600, + ], + 'dir' => [ +- 'public' => 0775, ++ 'public' => 0770, + 'private' => 0700, + ], + ], diff --git a/invoice-ninja.nix b/invoice-ninja.nix index acb4925..a65365c 100644 --- a/invoice-ninja.nix +++ b/invoice-ninja.nix @@ -16,7 +16,7 @@ let # PHP environment phpPackage = cfg.phpPackage.buildEnv { extensions = { enabled, all }: enabled ++ (with all; - [ bcmath ctype curl fileinfo gd gmp iconv imagick mbstring mysqli openssl pdo tokenizer zip ] + [ bcmath ctype curl fileinfo gd gmp iconv imagick intl mbstring mysqli openssl pdo soap tokenizer zip ] ); extraConfig = "memory_limit = 1024M"; @@ -245,10 +245,13 @@ in services.invoice-ninja.settings = let - app_http_url = "http://${cfg.hostName}"; - app_https_url = "https://${cfg.hostName}"; - react_http_url = "http://${cfg.hostName}:3001"; - react_https_url = "https://${cfg.hostName}:3001"; + url = ({ hostName, react ? false }: + if (hostName == "localhost") + then + (if (react == true) then ("http://" + hostName + ":3001") else ("http://" + hostName)) + else + (if (react == true) then ("https://" + hostName + ":3001") else ("https://" + hostName)) + ); chromium = lib.lists.findSingle (x: x == pkgs.chromium) "none" "multiple" extraPrograms; in lib.mkMerge [ @@ -256,8 +259,8 @@ in APP_NAME = lib.mkDefault "\"Invoice Ninja\""; APP_ENV = lib.mkDefault "production"; APP_DEBUG = lib.mkDefault false; - APP_URL = lib.mkDefault (if (cfg.hostName != "localhost") then "${app_https_url}" else "${app_http_url}"); - REACT_URL = lib.mkDefault (if (cfg.hostName != "localhost") then "${react_https_url}" else "${react_http_url}"); + APP_URL = lib.mkDefault (url { hostName = cfg.hostName; }); + REACT_URL = lib.mkDefault (url { hostName = cfg.hostName; react = true; }); DB_CONNECTION = lib.mkDefault "mysql"; MULTI_DB_ENABLED = lib.mkDefault false; DEMO_MODE = lib.mkDefault false; @@ -310,6 +313,11 @@ in { root = lib.mkForce "${invoice-ninja}/public"; locations = { + "= /index.php".extraConfig = '' + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:${config.services.phpfpm.pools.invoice-ninja.socket}; + fastcgi_index index.php; + ''; "/" = { tryFiles = "$uri $uri/ /index.php?$query_string"; extraConfig = '' @@ -318,11 +326,7 @@ in } ''; }; - "~ \\.php$".extraConfig = '' - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass unix:${config.services.phpfpm.pools.invoice-ninja.socket}; - fastcgi_index index.php; - ''; + "~ \\.php$".extraConfig = "return 403;"; "~ /\\.ht".extraConfig = "deny all;"; }; extraConfig = '' @@ -435,17 +439,15 @@ in # Perform the first migration [[ ! -f ${cfg.dataDir}/.initial-migration ]] && invoice-ninja-manage migrate --force && touch ${cfg.dataDir}/.initial-migration + # Seed database with records + # Necessary for languages, currencies, countries, etc. + invoice-ninja-manage db:seed --force + # Create Invoice Ninja admin account [[ ! -f ${cfg.dataDir}/.admin-created ]] \ && invoice-ninja-manage ninja:create-account --email=${cfg.adminEmail} --password=${cfg.adminPassword} \ && touch ${cfg.dataDir}/.admin-created - # Recent releases make the React interface default - # Currently this is broken so we switch back to the Flutter interface - [[ ! -f ${cfg.dataDir}/.react-disabled ]] \ - && mysql -D ${cfg.database.name} -e 'UPDATE accounts SET set_react_as_default_ap = 0;' \ - && touch ${cfg.dataDir}/.react-disabled - invoice-ninja-manage route:cache invoice-ninja-manage view:cache invoice-ninja-manage config:cache @@ -460,16 +462,24 @@ in "${cfg.dataDir}/storage/framework" "${cfg.dataDir}/storage/framework/cache" "${cfg.dataDir}/storage/framework/sessions" + "${cfg.dataDir}/storage/framework/testing" "${cfg.dataDir}/storage/framework/views" "${cfg.dataDir}/storage/logs" - cfg.runtimeDir - "${cfg.runtimeDir}/cache" ] (n: { d = { user = user; group = group; mode = "0770"; }; + }) // lib.attrsets.genAttrs [ + cfg.runtimeDir + "${cfg.runtimeDir}/cache" + ] (n: { + d = { + user = user; + group = group; + mode = "0750"; + }; }); }; } diff --git a/tests/test-config.nix b/tests/test-config.nix index 107caee..6eef017 100644 --- a/tests/test-config.nix +++ b/tests/test-config.nix @@ -13,7 +13,7 @@ users.users.test = { isNormalUser = true; extraGroups = [ "wheel" ]; - initialPassword = "testing"; + initialPassword = "test"; }; services.invoice-ninja = { From d7ee5fa774a55d8f26b7112fdf9e0b8665480418 Mon Sep 17 00:00:00 2001 From: awkawb Date: Sun, 6 Oct 2024 22:33:57 -0400 Subject: [PATCH 04/10] Changed phpfpm setting refactoring * moved listen.owner from config to implemetation * moved listen.group from config to implementation * moved catch_workers_output from config to implementation --- invoice-ninja.nix | 74 +++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/invoice-ninja.nix b/invoice-ninja.nix index a65365c..a9debe7 100644 --- a/invoice-ninja.nix +++ b/invoice-ninja.nix @@ -106,10 +106,6 @@ in phpfpm.settings = lib.mkOption { type = with lib.types; attrsOf (oneOf [ int str bool ]); default = { - "listen.owner" = user; - "listen.group" = group; - "listen.mode" = "0660"; - "pm" = "dynamic"; "pm.start_servers" = "2"; "pm.min_spare_servers" = "2"; @@ -121,8 +117,6 @@ in "php_admin_value[error_log]" = "stderr"; "php_admin_flag[log_errors]" = true; - - "catch_workers_output" = true; }; description = '' @@ -288,7 +282,13 @@ in services.phpfpm.pools.invoice-ninja = { inherit user group phpPackage; - inherit (cfg.phpfpm) settings; + + settings = { + "listen.owner" = user; + "listen.group" = group; + "listen.mode" = "0660"; + "catch_workers_output" = true; + } // cfg.phpfpm.settings; phpOptions = '' post_max_size = ${cfg.maxUploadSize} @@ -416,41 +416,41 @@ in }; script = '' - # Before running any PHP program, cleanup the code cache. - # It's necessary if you upgrade the application otherwise you might - # try to import non-existent modules. - rm -f ${cfg.runtimeDir}/app.php - rm -rf ${cfg.runtimeDir}/cache/* + # Before running any PHP program, cleanup the code cache. + # It's necessary if you upgrade the application otherwise you might + # try to import non-existent modules. + rm -f ${cfg.runtimeDir}/app.php + rm -rf ${cfg.runtimeDir}/cache/* - # Concatenate non-secret .env and secret .env - rm -f ${cfg.dataDir}/.env - cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env - echo -e '\n' >> ${cfg.dataDir}/.env - cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env + # Concatenate non-secret .env and secret .env + rm -f ${cfg.dataDir}/.env + cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env + echo -e '\n' >> ${cfg.dataDir}/.env + cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env - # Link the static storage (package provided) to the runtime storage - # Necessary for cities.json and static images. - rsync -av --no-perms ${invoice-ninja}/storage-static/ ${cfg.dataDir}/storage - - # Link the app.php in the runtime folder. - # We cannot link the cache folder only because bootstrap folder needs to be writeable. - ln -sf ${invoice-ninja}/bootstrap-static/app.php ${cfg.runtimeDir}/app.php + # Link the static storage (package provided) to the runtime storage + # Necessary for cities.json and static images. + rsync -av --no-perms ${invoice-ninja}/storage-static/ ${cfg.dataDir}/storage + + # Link the app.php in the runtime folder. + # We cannot link the cache folder only because bootstrap folder needs to be writeable. + ln -sf ${invoice-ninja}/bootstrap-static/app.php ${cfg.runtimeDir}/app.php - # Perform the first migration - [[ ! -f ${cfg.dataDir}/.initial-migration ]] && invoice-ninja-manage migrate --force && touch ${cfg.dataDir}/.initial-migration + # Perform the first migration + [[ ! -f ${cfg.dataDir}/.initial-migration ]] && invoice-ninja-manage migrate --force && touch ${cfg.dataDir}/.initial-migration - # Seed database with records - # Necessary for languages, currencies, countries, etc. - invoice-ninja-manage db:seed --force + # Seed database with records + # Necessary for languages, currencies, countries, etc. + invoice-ninja-manage db:seed --force - # Create Invoice Ninja admin account - [[ ! -f ${cfg.dataDir}/.admin-created ]] \ - && invoice-ninja-manage ninja:create-account --email=${cfg.adminEmail} --password=${cfg.adminPassword} \ - && touch ${cfg.dataDir}/.admin-created - - invoice-ninja-manage route:cache - invoice-ninja-manage view:cache - invoice-ninja-manage config:cache + # Create Invoice Ninja admin account + [[ ! -f ${cfg.dataDir}/.admin-created ]] \ + && invoice-ninja-manage ninja:create-account --email=${cfg.adminEmail} --password=${cfg.adminPassword} \ + && touch ${cfg.dataDir}/.admin-created + + invoice-ninja-manage route:cache + invoice-ninja-manage view:cache + invoice-ninja-manage config:cache ''; }; From eb0326f1d4828a723d7f866b1d0eb35300a43b9c Mon Sep 17 00:00:00 2001 From: awkawb Date: Thu, 10 Oct 2024 18:10:22 -0400 Subject: [PATCH 05/10] Changes to nixos module * removed package call * changed systemd queue worker name * changed interval option name * changed assertions * changed app environment file defaults * changed app data setup systemd job to only run database seeder on initial setup --- invoice-ninja.nix | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/invoice-ninja.nix b/invoice-ninja.nix index a9debe7..8f7b0c3 100644 --- a/invoice-ninja.nix +++ b/invoice-ninja.nix @@ -9,8 +9,7 @@ let cfg = config.services.invoice-ninja; user = cfg.user; group = cfg.group; - testing = pkgs.callPackage ./default.nix { inherit lib; php = pkgs.php; fetchFromGitHub = pkgs.fetchFromGitHub; }; - invoice-ninja = testing.override { inherit (cfg) dataDir runtimeDir; }; + invoice-ninja = pkgs.callPackage ./default.nix { inherit (cfg) dataDir runtimeDir; }; configFile = pkgs.writeText "invoice-ninja-env" (lib.generators.toKeyValue { } cfg.settings); # PHP environment @@ -89,10 +88,10 @@ in ''; }; - schedulerInterval = lib.mkOption { + queueWorkerInterval = lib.mkOption { type = lib.types.str; - default = "1d"; - description = "How often the Invoice Ninja cron task should run."; + default = "5m"; + description = "How often the Invoice Ninja worker task should run."; }; hostName = lib.mkOption { @@ -198,9 +197,7 @@ in enable = lib.mkOption { type = lib.types.bool; default = true; - description = '' - Whether to enable Nginx server to serve Invoice Ninja. - ''; + description = "Whether to enable Nginx server to serve Invoice Ninja."; }; config = lib.mkOption { @@ -221,8 +218,12 @@ in # FIXME Caddy and Nginx should be mutually exclusive assertions = [ { - assertion = (cfg.webserver.nginx.enable || cfg.webserver.caddy.enable); - message = "Either the Caddy or Nginx webserver needs to be enabled."; + assertion = ((cfg.webserver.nginx.enable -> !cfg.webserver.caddy.enable) + && (cfg.webserver.caddy.enable -> !cfg.webserver.nginx.enable)); + message = '' + Both Nginx and Caddy webservers cannot be enable together. Check your configuration + and ensure you only enabled one. + ''; } ]; @@ -258,7 +259,7 @@ in DB_CONNECTION = lib.mkDefault "mysql"; MULTI_DB_ENABLED = lib.mkDefault false; DEMO_MODE = lib.mkDefault false; - BROADCAST_DRIVER = lib.mkDefault "log"; + BROADCAST_DRIVER = lib.mkDefault "pusher"; LOG_CHANNEL = lib.mkDefault "stack"; CACHE_DRIVER = lib.mkDefault "file"; QUEUE_CONNECTION = lib.mkDefault "database"; @@ -267,6 +268,7 @@ in REQUIRE_HTTPS = lib.mkDefault (if (cfg.hostName != "localhost") then true else false); TRUSTED_PROXIES = lib.mkDefault "127.0.0.1"; NINJA_ENVIRONMENT = lib.mkDefault "selfhost"; + LOCAL_DOWNLOAD= lib.mkDefault false; PDF_GENERATOR = lib.mkDefault "snappdf"; SNAPPDF_CHROMIUM_PATH = lib.mkDefault "${chromium}/bin/chromium"; PRECONFIGURED_INSTALL = lib.mkDefault true; @@ -298,7 +300,7 @@ in }; users.users."${config.services.nginx.user}" = lib.mkIf (cfg.webserver.nginx.enable == true) { extraGroups = [ cfg.group ]; }; - services.nginx = lib.mkIf ((cfg.webserver.nginx.enable == true) && (cfg.webserver.caddy.enable == false)) { + services.nginx = lib.mkIf (cfg.webserver.nginx.enable == true) { inherit (cfg.webserver.nginx) enable; recommendedTlsSettings = true; @@ -338,7 +340,7 @@ in }; users.users."${config.services.caddy.user}" = lib.mkIf (cfg.webserver.caddy.enable == true) { extraGroups = [ cfg.group ]; }; - services.caddy = lib.mkIf ((cfg.webserver.caddy.enable == true) && (cfg.webserver.nginx.enable == false)) { + services.caddy = lib.mkIf (cfg.webserver.caddy.enable == true) { inherit (cfg.webserver.caddy) enable; globalConfig = lib.mkIf (cfg.hostName == "localhost") '' @@ -374,7 +376,7 @@ in # Ensure chromium is available systemd.services.phpfpm-invoice-ninja.path = extraPrograms; - systemd.timers.invoice-ninja-cron = { + systemd.timers.invoice-ninja-worker = { description = "Invoice Ninja periodic tasks timer"; after = [ "invoice-ninja-data-setup.service" ]; requires = [ "phpfpm-invoice-ninja.service" ]; @@ -386,11 +388,11 @@ in }; }; - systemd.services.invoice-ninja-cron = { + systemd.services.invoice-ninja-worker = { description = "Invoice Ninja periodic tasks"; serviceConfig = { - ExecStart = "${invoice-ninja-manage}/bin/invoice-ninja-manage schedule:run"; + ExecStart = "${invoice-ninja-manage}/bin/invoice-ninja-manage queue:work --stop-when-empty"; User = user; Group = group; StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/invoice-ninja") "invoice-ninja"; @@ -441,7 +443,7 @@ in # Seed database with records # Necessary for languages, currencies, countries, etc. - invoice-ninja-manage db:seed --force + [[ ! -f ${cfg.dataDir}/.db-seeded ]] && invoice-ninja-manage db:seed --force && touch ${cfg.dataDir}/.db-seeded # Create Invoice Ninja admin account [[ ! -f ${cfg.dataDir}/.admin-created ]] \ From e6f752704fb5d6089f8f981730f7ca433dbcb47b Mon Sep 17 00:00:00 2001 From: awkawb Date: Fri, 11 Oct 2024 14:41:19 -0400 Subject: [PATCH 06/10] Updated Invoice Ninja derivation to v5.10.34 --- default.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/default.nix b/default.nix index 99ae22a..8490cc3 100644 --- a/default.nix +++ b/default.nix @@ -9,16 +9,16 @@ php.buildComposerProject (finalAttrs: { pname = "invoice-ninja"; - version = "5.10.31"; + version = "5.10.34"; src = fetchFromGitHub { owner = "invoiceninja"; repo = "invoiceninja"; rev = "v${finalAttrs.version}"; - hash = "sha256-n3SpyXGUjw+qHEtnrclzIIdQ09UZBI+Rj7MXeUJVk6c="; + hash = "sha256-2lR2lT8TbDcJ0m3C2ZwdbICPJGSlF9U7I+RC8MxvtOY="; }; - vendorHash = "sha256-krGeVT6GyvkFkgeviA06dRneApEHDK61Tm+hXYTVYCM="; + vendorHash = "sha256-Z9gEjZqn8Ew2EnUNyD1dfjt0y+Y+5VjH/q1oFErrorg="; # Patch sources for more restrictive permissions patches = [ From 048ca07f4b81dda8764d473ff5313ff51108fa84 Mon Sep 17 00:00:00 2001 From: awkawb Date: Fri, 11 Oct 2024 14:41:42 -0400 Subject: [PATCH 07/10] Work on Laravel queue --- invoice-ninja.nix | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/invoice-ninja.nix b/invoice-ninja.nix index 8f7b0c3..bfa19c7 100644 --- a/invoice-ninja.nix +++ b/invoice-ninja.nix @@ -88,12 +88,6 @@ in ''; }; - queueWorkerInterval = lib.mkOption { - type = lib.types.str; - default = "5m"; - description = "How often the Invoice Ninja worker task should run."; - }; - hostName = lib.mkOption { type = lib.types.str; default = "localhost"; @@ -376,23 +370,16 @@ in # Ensure chromium is available systemd.services.phpfpm-invoice-ninja.path = extraPrograms; - systemd.timers.invoice-ninja-worker = { - description = "Invoice Ninja periodic tasks timer"; - after = [ "invoice-ninja-data-setup.service" ]; - requires = [ "phpfpm-invoice-ninja.service" ]; - wantedBy = [ "timers.target" ]; - - timerConfig = { - OnBootSec = cfg.schedulerInterval; - OnUnitActiveSec = cfg.schedulerInterval; - }; - }; - systemd.services.invoice-ninja-worker = { description = "Invoice Ninja periodic tasks"; + after = [ "invoice-ninja-data-setup.service" ]; + requires = [ "phpfpm-invoice-ninja.service" ]; + wantedBy = [ "multi-user.target" ]; + reloadTriggers = [ invoice-ninja ]; + reload = "${invoice-ninja-manage}/bin/invoice-ninja-manage queue:restart"; serviceConfig = { - ExecStart = "${invoice-ninja-manage}/bin/invoice-ninja-manage queue:work --stop-when-empty"; + ExecStart = "${invoice-ninja-manage}/bin/invoice-ninja-manage queue:work"; User = user; Group = group; StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/invoice-ninja") "invoice-ninja"; From 91a0d4e71a89242b5204b6c18b63b1cf07372e85 Mon Sep 17 00:00:00 2001 From: awkawb Date: Sun, 13 Oct 2024 13:08:04 -0400 Subject: [PATCH 08/10] Work on sendmail to send emails --- invoice-ninja.nix | 58 +++++++++++++++++++++++++++++++++++++++++++ tests/test-config.nix | 21 ++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/invoice-ninja.nix b/invoice-ninja.nix index bfa19c7..38c8706 100644 --- a/invoice-ninja.nix +++ b/invoice-ninja.nix @@ -33,6 +33,18 @@ let fi $sudo ${phpPackage}/bin/php artisan "$@" ''; + + invoice-ninja-msmtp = pkgs.writeShellScriptBin "msmtp" '' + sudo=exec + if [[ "$USER" != ${user} ]]; then + sudo='exec /run/wrappers/bin/sudo -u ${user}' + fi + $sudo ${pkgs.msmtp}/bin/msmtp --auth on \ + --tls=${if cfg.msmtp.tls then "on" else "off"} --tls-starttls=off \ + --host=${cfg.msmtp.host} --port=${toString cfg.msmtp.port} \ + --user=${cfg.msmtp.username} --passwordeval="${cfg.msmtp.passwordeval}" \ + --from=${cfg.msmtp.from} "$1" + ''; in { options.services.invoice-ninja = { @@ -70,6 +82,13 @@ in ''; }; + mailMailer = lib.mkOption { + type = lib.types.enum [ "sendmail" "smtp" ]; + default = "sendmail"; + example = "smtp"; + description = "Controls the method used by Invoice Ninja to send mail."; + }; + dataDir = lib.mkOption { type = lib.types.str; default = "/var/lib/invoice-ninja"; @@ -165,6 +184,43 @@ in description = "Maximum allowed upload size to Invoice Ninja."; }; + msmtp = { + tls = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable SSL/TLS encryption"; + }; + from = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Email address message will come from."; + }; + host = lib.mkOption { + type = lib.types.str; + default = ""; + description = "SMTP host used to send mail."; + }; + port = lib.mkOption { + type = lib.types.int; + default = 25; + description = "Port used to connect to SMTP host."; + }; + username = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Username used to authenticate to SMTP host"; + }; + passwordeval = lib.mkOption { + type = lib.types.str; + default = ""; + example = "cat /secrets/msmtp_password"; + description = '' + A shell command to read the password from a secret file to avoid having it written in + the world-readable nix store. The password file must end with a newline (`\n`). + ''; + }; + }; + webserver = { caddy = { enable = lib.mkOption { @@ -250,6 +306,8 @@ in APP_DEBUG = lib.mkDefault false; APP_URL = lib.mkDefault (url { hostName = cfg.hostName; }); REACT_URL = lib.mkDefault (url { hostName = cfg.hostName; react = true; }); + MAIL_MAILER = lib.mkDefault cfg.mailMailer; + MAIL_SENDMAIL_PATH = lib.mkDefault (if (cfg.mailMailer == "sendmail") then "${invoice-ninja-msmtp}/bin/msmtp" else ""); DB_CONNECTION = lib.mkDefault "mysql"; MULTI_DB_ENABLED = lib.mkDefault false; DEMO_MODE = lib.mkDefault false; diff --git a/tests/test-config.nix b/tests/test-config.nix index 6eef017..64f2a3f 100644 --- a/tests/test-config.nix +++ b/tests/test-config.nix @@ -10,6 +10,17 @@ nixpkgs.config.allowUnfree = true; + environment.etc."msmtp-password" = { + enable = true; + user = "invoiceninja"; + group = "invoiceninja"; + mode = "0440"; + text = '' + 3t5h638t3a7y7275 + + ''; + }; + users.users.test = { isNormalUser = true; extraGroups = [ "wheel" ]; @@ -19,6 +30,16 @@ services.invoice-ninja = { enable = true; database.createLocally = true; + webserver.caddy.enable = true; + webserver.nginx.enable = false; + msmtp = { + tls = true; + from = "awkawb@awkawb.cloud"; + host = "smtp.fastmail.com"; + port = 465; + username = "awkawb@awkawb.cloud"; + passwordeval = "cat /etc/msmtp-password"; + }; secretFile = ./test-secrets.env; }; From 3a5c53299210c07d963a4c0f79f5a2095b0878f9 Mon Sep 17 00:00:00 2001 From: awkawb Date: Mon, 14 Oct 2024 16:26:08 -0400 Subject: [PATCH 09/10] Updated NixOS module to support sending emails with msmtp --- invoice-ninja.nix | 104 ++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 54 deletions(-) diff --git a/invoice-ninja.nix b/invoice-ninja.nix index 38c8706..5d7d0ad 100644 --- a/invoice-ninja.nix +++ b/invoice-ninja.nix @@ -33,18 +33,6 @@ let fi $sudo ${phpPackage}/bin/php artisan "$@" ''; - - invoice-ninja-msmtp = pkgs.writeShellScriptBin "msmtp" '' - sudo=exec - if [[ "$USER" != ${user} ]]; then - sudo='exec /run/wrappers/bin/sudo -u ${user}' - fi - $sudo ${pkgs.msmtp}/bin/msmtp --auth on \ - --tls=${if cfg.msmtp.tls then "on" else "off"} --tls-starttls=off \ - --host=${cfg.msmtp.host} --port=${toString cfg.msmtp.port} \ - --user=${cfg.msmtp.username} --passwordeval="${cfg.msmtp.passwordeval}" \ - --from=${cfg.msmtp.from} "$1" - ''; in { options.services.invoice-ninja = { @@ -82,11 +70,18 @@ in ''; }; - mailMailer = lib.mkOption { - type = lib.types.enum [ "sendmail" "smtp" ]; - default = "sendmail"; - example = "smtp"; - description = "Controls the method used by Invoice Ninja to send mail."; + mail = { + mailer = lib.mkOption { + type = lib.types.enum [ "sendmail" ]; + default = "sendmail"; + description = "Controls the method used by Invoice Ninja to send mail."; + }; + mailFromName = lib.mkOption { + type = lib.types.str; + default = ""; + example = "Someone"; + description = "Set the 'To' email header name attribute."; + }; }; dataDir = lib.mkOption { @@ -184,41 +179,27 @@ in description = "Maximum allowed upload size to Invoice Ninja."; }; - msmtp = { - tls = lib.mkOption { - type = lib.types.bool; - default = true; - description = "Enable SSL/TLS encryption"; - }; - from = lib.mkOption { - type = lib.types.str; - default = ""; - description = "Email address message will come from."; - }; - host = lib.mkOption { - type = lib.types.str; - default = ""; - description = "SMTP host used to send mail."; - }; - port = lib.mkOption { - type = lib.types.int; - default = 25; - description = "Port used to connect to SMTP host."; - }; - username = lib.mkOption { - type = lib.types.str; - default = ""; - description = "Username used to authenticate to SMTP host"; - }; - passwordeval = lib.mkOption { - type = lib.types.str; - default = ""; - example = "cat /secrets/msmtp_password"; - description = '' - A shell command to read the password from a secret file to avoid having it written in - the world-readable nix store. The password file must end with a newline (`\n`). - ''; + msmtp.accounts.invoice-ninja = lib.mkOption { + type = lib.types.attrs; + default = {}; + example = { + from = "someone@example.com"; + host = "smtp.example"; + port = 25; + auth = true; + tls = true; + tls_starttls = true; + user = "someone"; + passwordeval = "cat /secrets/password.txt"; }; + description = '' + Here we define the msmtp configuration for an invoice-ninja account which + will be used by Invoice Ninja to send email message. + + It is advised to use the `passwordeval` setting to read the password + from a secret file to avoid having it written in the world-readable + nix store. The password file must end with a newline (`\n`). + ''; }; webserver = { @@ -288,6 +269,11 @@ in environment.systemPackages = [ invoice-ninja-manage ] ++ extraPrograms; + programs.msmtp = { + inherit (cfg.msmtp) accounts; + enable = true; + }; + services.invoice-ninja.settings = let url = ({ hostName, react ? false }: @@ -300,14 +286,24 @@ in chromium = lib.lists.findSingle (x: x == pkgs.chromium) "none" "multiple" extraPrograms; in lib.mkMerge [ - ({ + (rec { APP_NAME = lib.mkDefault "\"Invoice Ninja\""; APP_ENV = lib.mkDefault "production"; APP_DEBUG = lib.mkDefault false; + EXPANDED_LOGGING = lib.mkDefault true; APP_URL = lib.mkDefault (url { hostName = cfg.hostName; }); REACT_URL = lib.mkDefault (url { hostName = cfg.hostName; react = true; }); - MAIL_MAILER = lib.mkDefault cfg.mailMailer; - MAIL_SENDMAIL_PATH = lib.mkDefault (if (cfg.mailMailer == "sendmail") then "${invoice-ninja-msmtp}/bin/msmtp" else ""); + MAIL_MAILER = lib.mkDefault cfg.mail.mailer; + MAIL_SENDMAIL_PATH = lib.mkDefault ( + if (cfg.mail.mailer == "sendmail") + then + ''"/run/wrappers/bin/sendmail -t -a invoice-ninja"'' + else + "" + ); + MAIL_FROM_ADDRESS = lib.mkDefault "${cfg.msmtp.accounts.invoice-ninja.from}"; + MAIL_FROM_NAME = lib.mkDefault ''"${cfg.mail.mailFromName}"''; + ERROR_EMAIL = lib.mkDefault "${cfg.msmtp.accounts.invoice-ninja.from}"; DB_CONNECTION = lib.mkDefault "mysql"; MULTI_DB_ENABLED = lib.mkDefault false; DEMO_MODE = lib.mkDefault false; From 449c52239a834c9f0133c0c3cc8c66a5b9a05708 Mon Sep 17 00:00:00 2001 From: awkawb Date: Mon, 14 Oct 2024 16:26:43 -0400 Subject: [PATCH 10/10] Updated test-config to use modules sendmail feature --- tests/test-config.nix | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test-config.nix b/tests/test-config.nix index 64f2a3f..2c1e2b3 100644 --- a/tests/test-config.nix +++ b/tests/test-config.nix @@ -32,12 +32,16 @@ database.createLocally = true; webserver.caddy.enable = true; webserver.nginx.enable = false; - msmtp = { + mail.mailFromName = "Andrew Bryant"; + adminEmail = "billing@allpawcare.com"; + msmtp.accounts.invoice-ninja = { + auth = true; tls = true; - from = "awkawb@awkawb.cloud"; + tls_starttls = false; + from = "billing@allpawcare.com"; host = "smtp.fastmail.com"; port = 465; - username = "awkawb@awkawb.cloud"; + user = "awkawb@awkawb.cloud"; passwordeval = "cat /etc/msmtp-password"; }; secretFile = ./test-secrets.env;