24 Commits

Author SHA1 Message Date
awkawb b37f121f06 flake.nix: updated nixpkgs input to version 25.11 2026-05-25 19:11:06 -04:00
awkawb 878134dd4b nixos-module/invoiceninja.nix: update nginx to work with Invoice Ninja 2026-05-25 19:09:49 -04:00
awkawb 808b9a7fe9 Replaced Makefile with justfile 2026-05-25 19:02:53 -04:00
awkawb 6b0f38d59e tests/default.nix: updated system.stateVersion 2026-05-25 19:02:21 -04:00
awkawb fd776bb9b7 package.nix: updated to build Invoice Ninja with React frontend 2026-05-25 19:01:26 -04:00
awkawb a42d01edd3 package.nix: bumped version to 5.12.13 2025-08-13 18:34:59 -04:00
awkawb 1df11adbde Merge branch 'master' of gitea.awkawb.cloud:awkawb/invoiceninja-nixos 2025-03-01 11:11:58 -05:00
awkawb e8b208accb flake.lock: updated flake inputs 2025-02-24 17:32:32 -05:00
awkawb 295b299fdd package.nix: bumped version to 5.11.41 2025-02-24 17:29:51 -05:00
awkawb d898e26cb1 package.nix: bumped version to 5.11.24 2025-01-14 16:54:51 -05:00
awkawb 7b2072e5b0 package.nix: remove unneeded move and link statements 2025-01-02 16:55:02 -05:00
awkawb 611709cb62 nixos-module/invoiceninja.nix: changed nixos module definition for nginx to allow local development, removed unneeded link in data setup service 2025-01-02 16:53:58 -05:00
awkawb 730715014e tests/default.nix: modified to reflect package name change 2025-01-02 16:22:09 -05:00
awkawb 4bddba21da flake.nix: fixed attribute name 2025-01-01 16:15:10 -05:00
awkawb 8549d2f23d Makefile: build-vm now reflects the repo being a flake 2025-01-01 12:19:25 -05:00
awkawb 91d8d8a908 flake.nix: modified to reflect package name change, add nixos config for development 2025-01-01 12:17:55 -05:00
awkawb 2b98375f9b tests/default.nix: added stateVersion, added import of nixos module, fixed typo 2025-01-01 12:12:59 -05:00
awkawb 4cb4a1c05d Package is now 'invoiceninja', modified code to reflect name change, changed nixos module definition for caddy to allow local development 2025-01-01 12:11:18 -05:00
awkawb 14976fdae9 Added tests directory for testing in vm 2024-12-30 10:19:29 -05:00
awkawb 1e48f190da Removed unused files and directories 2024-12-28 16:19:55 -05:00
awkawb 85de24655f packages.nix: bumped version to 5.11.7 2024-12-28 16:11:25 -05:00
awkawb 9bb2d8f352 package.nix: bumped version to 5.11.6 2024-12-28 16:09:49 -05:00
awkawb e043ebbaea package.nix: bumped version to 5.11.5 2024-12-28 15:40:58 -05:00
awkawb 5178b6b7cb package.nix: bumped version to 5.11.4 2024-12-28 14:30:08 -05:00
15 changed files with 457 additions and 304 deletions
-11
View File
@@ -1,11 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2024.07.04] 2024-07-04
- Initial release
-47
View File
@@ -1,47 +0,0 @@
####
# Target definitions
####
# Disable echoing of target recipe commands
# Comment this for debugging
.SILENT:
# Run target recipes in one shell invocation
.ONESHELL:
# Since all targets are phony, all targets should be listed here
# One target per line
.PHONY: boot-vm \
build-vm \
clean \
format-nix-files \
help
# Show the help text
help:
egrep -h '\s##\s' $(MAKEFILE_LIST) \
| sort \
| awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
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 > /dev/null 2>&1
rm nixos.qcow2 > /dev/null 2>&1
pkill qemu
exit 0
build-vm: clean ## Build virtual machine for testing
nixos-rebuild build-vm \
-I nixpkgs=http://nixos.org/channels/nixos-24.05/nixexprs.tar.xz \
-I nixos-config=./tests/test-config.nix
boot-vm: ## Run virtual machine in current terminal
QEMU_KERNEL_PARAMS=console=ttyS0 \
QEMU_NET_OPTS=hostfwd=tcp::8080-:80 \
./result/bin/run-nixos-vm \
-nographic; \
reset
-13
View File
@@ -1,13 +0,0 @@
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';
Generated
+166
View File
@@ -0,0 +1,166 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"git-hooks-nix": {
"inputs": {
"flake-compat": [
"nix"
],
"gitignore": [
"nix"
],
"nixpkgs": [
"nix",
"nixpkgs"
],
"nixpkgs-stable": [
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1734279981,
"narHash": "sha256-NdaCraHPp8iYMWzdXAt5Nv6sA3MUzlCiGiR586TCwo0=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "aa9f40c906904ebd83da78e7f328cd8aeaeae785",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"git-hooks-nix": "git-hooks-nix",
"nixpkgs": "nixpkgs",
"nixpkgs-23-11": "nixpkgs-23-11",
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1740432616,
"narHash": "sha256-2yad3RqbLhCeMK01lpSLXmeZR7J74YeBg4yNAqhzLLk=",
"owner": "NixOS",
"repo": "nix",
"rev": "8384e41b7608b5ff50306d3dec6171483cf2d4cd",
"type": "github"
},
"original": {
"id": "nix",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1734359947,
"narHash": "sha256-1Noao/H+N8nFB4Beoy8fgwrcOQLVm9o4zKW1ODaqK9E=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "48d12d5e70ee91fe8481378e540433a7303dbf6a",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-23-11": {
"locked": {
"lastModified": 1717159533,
"narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
"type": "github"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1740339700,
"narHash": "sha256-cbrw7EgQhcdFnu6iS3vane53bEagZQy/xyIkDWpCgVE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "04ef94c4c1582fd485bbfdb8c4a8ba250e359195",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nix": "nix",
"nixpkgs": "nixpkgs_2"
}
}
},
"root": "root",
"version": 7
}
+9 -8
View File
@@ -1,7 +1,7 @@
{ {
description = "An Invoice Ninja package and a module which can be added to a NixOS configuration"; description = "An Invoice Ninja package and module.";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
outputs = outputs =
{ self, nixpkgs, nix, }: { self, nixpkgs, nix, }:
@@ -18,19 +18,20 @@
overlays = overlayList; overlays = overlayList;
} }
); );
in in
rec { rec {
nixosConfigurations.test = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./tests ];
};
# A Nixpkgs overlay that provides a 'Invoice Ninja' package. overlays.default = final: prev: { invoiceninja = final.callPackage ./package.nix { }; };
overlays.default = final: prev: { invoice-ninja = final.callPackage ./package.nix { }; };
packages = forEachSystem (system: { packages = forEachSystem (system: {
invoice-ninja = pkgsBySystem.${system}.invoice-ninja; invoiceninja = pkgsBySystem.${system}.invoiceninja;
default = pkgsBySystem.${system}.invoice-ninja; default = pkgsBySystem.${system}.invoiceninja;
}); });
nixosModules = import ./nixos-module { overlays = overlayList; }; nixosModules = import ./nixos-module { overlays = overlayList; };
}; };
} }
+33
View File
@@ -0,0 +1,33 @@
set quiet := true
# Passed to QEMU in boot-vm
export QEMU_KERNEL_PARAMS := "console=ttyS0"
export QEMU_NET_OPTS := "hostfwd=tcp::8080-:80"
export QEMU_OPTS := "-nographic"
[private]
_default:
just --list
[doc('Clean build artifacts and shutdown running virtual machines')]
[group('maintenance')]
clean:
#!/usr/bin/env bash
rm result > /dev/null 2>&1
rm nixos.qcow2 > /dev/null 2>&1
pkill qemu
exit 0
[doc('Build virtual machine for testing')]
[group('main')]
build-vm: clean
nom build ".#nixosConfigurations.test.config.system.build.vm"
[doc('Run virtual machine in current terminal')]
[group('main')]
boot-vm:
#!/usr/bin/env bash
[ -x result/bin/run-nixos-vm ] && \
./result/bin/run-nixos-vm
reset
+1 -1
View File
@@ -1,7 +1,7 @@
{ overlays }: { overlays }:
{ {
invoice-ninja = import ./invoice-ninja.nix; invoiceninja = import ./invoiceninja.nix;
overlayNixpkgsForThisInstance = overlayNixpkgsForThisInstance =
{ pkgs, ... }: { { pkgs, ... }: {
nixpkgs = { nixpkgs = {
@@ -1,21 +1,20 @@
{ { config
config, , lib
lib, , modulesPath
modulesPath, , options
options, , pkgs
pkgs, , ...
...
}: }:
let let
cfg = config.services.invoice-ninja; cfg = config.services.invoiceninja;
user = cfg.user; user = cfg.user;
group = cfg.group; group = cfg.group;
invoice-ninja = pkgs.callPackage ../package.nix { invoiceninja = pkgs.callPackage ../package.nix {
inherit (cfg) dataDir runtimeDir; inherit (cfg) dataDir runtimeDir;
}; };
configFormat = pkgs.formats.keyValue { }; configFormat = pkgs.formats.keyValue { };
configFile = pkgs.writeText "invoice-ninja-env" (lib.generators.toKeyValue { } cfg.settings); configFile = pkgs.writeText "invoiceninja-env" (lib.generators.toKeyValue { } cfg.settings);
# PHP environment # PHP environment
phpPackage = cfg.phpPackage.buildEnv { phpPackage = cfg.phpPackage.buildEnv {
@@ -53,8 +52,8 @@ let
extraPrograms = [ chromium ]; extraPrograms = [ chromium ];
# Management script # Management script
invoice-ninja-manage = pkgs.writeShellScriptBin "invoice-ninja-manage" '' invoiceninja-manage = pkgs.writeShellScriptBin "invoiceninja-manage" ''
cd ${invoice-ninja} cd ${invoiceninja}
sudo=exec sudo=exec
if [[ "$USER" != ${user} ]]; then if [[ "$USER" != ${user} ]]; then
sudo='exec /run/wrappers/bin/sudo -u ${user}' sudo='exec /run/wrappers/bin/sudo -u ${user}'
@@ -63,10 +62,10 @@ let
''; '';
in in
{ {
options.services.invoice-ninja = { options.services.invoiceninja = {
enable = lib.mkEnableOption "invoice-ninja"; enable = lib.mkEnableOption "invoiceninja";
package = lib.mkPackageOption pkgs "invoice-ninja" { }; package = lib.mkPackageOption pkgs "invoiceninja" { };
phpPackage = lib.mkPackageOption pkgs "php82" { }; phpPackage = lib.mkPackageOption pkgs "php82" { };
@@ -94,7 +93,7 @@ in
''; '';
}; };
msmtp.accounts.invoice-ninja = lib.mkOption { msmtp.accounts.invoiceninja = lib.mkOption {
type = lib.types.attrs; type = lib.types.attrs;
default = { }; default = { };
example = { example = {
@@ -107,9 +106,9 @@ in
passwordeval = "cat /secrets/password.txt"; passwordeval = "cat /secrets/password.txt";
}; };
description = '' description = ''
Define the msmtp configuration for an invoice-ninja account which Define the msmtp configuration for an invoiceninja account which
will be used by Invoice Ninja to send email message when will be used by Invoice Ninja to send email message when
`config.services.invoice-ninja.settings.MAIL_MAILER` is `sendmail`. `config.services.invoiceninja.settings.MAIL_MAILER` is `sendmail`.
It is advised to use the `passwordeval` setting to read the password 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 from a secret file to avoid having it written in the world-readable
@@ -125,18 +124,18 @@ in
dataDir = lib.mkOption { dataDir = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "/var/lib/invoice-ninja"; default = "/var/lib/invoiceninja";
description = '' description = ''
State directory of the `invoice-ninja` user which holds State directory of the `invoiceninja` user which holds
the application's state and data. the application's state and data.
''; '';
}; };
runtimeDir = lib.mkOption { runtimeDir = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "/run/invoice-ninja"; default = "/run/invoiceninja";
description = '' description = ''
Rutime directory of the `invoice-ninja` user which holds Rutime directory of the `invoiceninja` user which holds
the application's caches and temporary files. the application's caches and temporary files.
''; '';
}; };
@@ -207,7 +206,7 @@ in
options = { options = {
APP_NAME = lib.mkOption { APP_NAME = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "Invoice Ninja"; default = ''"Invoice Ninja"'';
description = "Your application name - used in client portal title banner"; description = "Your application name - used in client portal title banner";
}; };
APP_DEBUG = lib.mkOption { APP_DEBUG = lib.mkOption {
@@ -256,13 +255,13 @@ in
}; };
MAIL_SENDMAIL_PATH = lib.mkOption { MAIL_SENDMAIL_PATH = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = ''"/run/wrappers/bin/sendmail -t -a invoice-ninja"''; default = ''"/run/wrappers/bin/sendmail -t -a invoiceninja"'';
description = '' description = ''
Path to sendmail along with arguments for Invoice Ninja to use when using sendmail Path to sendmail along with arguments for Invoice Ninja to use when using sendmail
as mail transport agent. as mail transport agent.
:: note :: note
The default value will work with the `msmtp.accounts.invoice-ninja` setting. Only The default value will work with the `msmtp.accounts.invoiceninja` setting. Only
change if you know what your doing. change if you know what your doing.
:: ::
''; '';
@@ -294,7 +293,7 @@ in
default = default =
if cfg.redis.createLocally then "redis" else (if cfg.database.createLocally then "database" else "file"); if cfg.redis.createLocally then "redis" else (if cfg.database.createLocally then "database" else "file");
defaultText = lib.literalExpression '' defaultText = lib.literalExpression ''
if config.services.invoice-ninja.redis.enable if config.services.invoiceninja.redis.enable
then "redis" then "redis"
else (if config.services.invoie-ninja.database.createLocally then "database" else "file") else (if config.services.invoie-ninja.database.createLocally then "database" else "file")
''; '';
@@ -317,7 +316,7 @@ in
default = default =
if cfg.redis.createLocally then "redis" else (if cfg.database.createLocally then "database" else "sync"); if cfg.redis.createLocally then "redis" else (if cfg.database.createLocally then "database" else "sync");
defaultText = lib.literalExpression '' defaultText = lib.literalExpression ''
if config.services.invoice-ninja.redis.enable if config.services.invoiceninja.redis.enable
then "redis" then "redis"
else (if config.services.invoie-ninja.database.createLocally then "database" else "sync") else (if config.services.invoie-ninja.database.createLocally then "database" else "sync")
''; '';
@@ -329,7 +328,7 @@ in
"redis" "redis"
]; ];
default = if cfg.redis.createLocally then "redis" else "file"; default = if cfg.redis.createLocally then "redis" else "file";
defaultText = lib.literalExpression ''if config.services.invoice-ninja.redis.enable then "redis" else "file"''; defaultText = lib.literalExpression ''if config.services.invoiceninja.redis.enable then "redis" else "file"'';
description = "Laravel cache driver for Invoice Ninja to use."; description = "Laravel cache driver for Invoice Ninja to use.";
}; };
}; };
@@ -337,7 +336,7 @@ in
}; };
adminAccount = { adminAccount = {
createAdmin = lib.mkOption { enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;
description = '' description = ''
@@ -347,7 +346,7 @@ in
}; };
email = lib.mkOption { email = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "example@email.com"; default = "admin@email.com";
description = "Email address of the first (admin) account for this Invoice Ninja installation"; description = "Email address of the first (admin) account for this Invoice Ninja installation";
}; };
passwordFile = lib.mkOption { passwordFile = lib.mkOption {
@@ -423,14 +422,14 @@ in
users.groups.invoiceninja = lib.mkIf (cfg.group == "invoiceninja") { }; users.groups.invoiceninja = lib.mkIf (cfg.group == "invoiceninja") { };
environment.systemPackages = [ invoice-ninja-manage ] ++ extraPrograms; environment.systemPackages = [ invoiceninja-manage ] ++ extraPrograms;
programs.msmtp = lib.mkIf (cfg.settings.MAIL_MAILER == "sendmail") { programs.msmtp = lib.mkIf (cfg.settings.MAIL_MAILER == "sendmail") {
inherit (cfg.msmtp) accounts; inherit (cfg.msmtp) accounts;
enable = true; enable = true;
}; };
services.invoice-ninja.settings = lib.mkMerge [ services.invoiceninja.settings = lib.mkMerge [
({ ({
APP_URL = lib.mkDefault ( APP_URL = lib.mkDefault (
if (cfg.hostname == "localhost") then ("http://" + cfg.hostname) else ("https://" + cfg.hostname) if (cfg.hostname == "localhost") then ("http://" + cfg.hostname) else ("https://" + cfg.hostname)
@@ -452,12 +451,12 @@ in
DB_USERNAME = lib.mkDefault user; DB_USERNAME = lib.mkDefault user;
}) })
(lib.mkIf cfg.redis.createLocally { (lib.mkIf cfg.redis.createLocally {
REDIS_HOST = lib.mkDefault config.services.redis.servers.invoice-ninja.bind; REDIS_HOST = lib.mkDefault config.services.redis.servers.invoiceninja.bind;
REDIS_PORT = lib.mkDefault config.services.redis.servers.invoice-ninja.port; REDIS_PORT = lib.mkDefault config.services.redis.servers.invoiceninja.port;
}) })
]; ];
services.phpfpm.pools.invoice-ninja = { services.phpfpm.pools.invoiceninja = {
inherit user group phpPackage; inherit user group phpPackage;
settings = { settings = {
@@ -474,7 +473,7 @@ in
''; '';
}; };
services.redis.servers.invoice-ninja = lib.mkIf cfg.redis.createLocally { services.redis.servers.invoiceninja = lib.mkIf cfg.redis.createLocally {
enable = true; enable = true;
port = 6379; port = 6379;
}; };
@@ -485,7 +484,7 @@ in
services.nginx = lib.mkIf (cfg.proxy.server == "nginx") { services.nginx = lib.mkIf (cfg.proxy.server == "nginx") {
enable = true; enable = true;
recommendedTlsSettings = true; recommendedTlsSettings = (if (cfg.hostname == "localhost") then false else true);
recommendedGzipSettings = true; recommendedGzipSettings = true;
recommendedProxySettings = true; recommendedProxySettings = true;
recommendedOptimisation = true; recommendedOptimisation = true;
@@ -495,32 +494,46 @@ in
virtualHosts."${cfg.hostname}" = lib.mkMerge [ virtualHosts."${cfg.hostname}" = lib.mkMerge [
cfg.proxy.nginxConfig cfg.proxy.nginxConfig
{ {
root = lib.mkForce "${invoice-ninja}/public"; root = lib.mkForce "${invoiceninja}/public";
addSSL = lib.mkForce true; addSSL = lib.mkForce (if (cfg.hostname == "localhost") then false else true);
enableACME = lib.mkForce true; enableACME = lib.mkForce (if (cfg.hostname == "localhost") then false else true);
locations = { locations = {
# Handle Laravel Routes
"/".tryFiles = "$uri $uri/ /index.php?$query_string"; "/".tryFiles = "$uri $uri/ /index.php?$query_string";
"/".extraConfig = ''
if (!-e $request_filename) { # PHP Processing
rewrite ^(.+)$ /index.php?q=$1 last; "~ \\.php$".extraConfig = ''
} include ${config.services.nginx.package}/conf/fastcgi_params;
add_header 'Access-Control-Allow-Origin' '*'; fastcgi_param SCRIPT_FILENAME $request_filename;
add_header 'Access-Control-Allow-Methods' '*'; fastcgi_split_path_info ^(.+\.php)(/.+)$;
add_header 'Access-Control-Max-Age' 0; fastcgi_pass unix:${config.services.phpfpm.pools.invoiceninja.socket};
add_header 'Content-Length' 0; fastcgi_index index.php;
add_header 'Access-Control-Allow-Headers' 'X-API-COMPANY-KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Disposition,Content-Type,Range,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE';
add_header 'Access-Control-Expose-Headers' 'X-APP-VERSION,X-MINIMUM-CLIENT-VERSION,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE';
add_header 'Access-Control-Allow-Credentials' false;
'';
"~ \\.php$".extraConfig = "return 403;";
"= /index.php".extraConfig = ''
fastcgi_pass unix:${config.services.phpfpm.pools.invoice-ninja.socket};
''; '';
# Security: Deny access to hidden files
"~ /\\.ht".extraConfig = "deny all;"; "~ /\\.ht".extraConfig = "deny all;";
# Static Files Caching
"~* \\.(jpg|jpeg|png|gif|ico|css|js)$".extraConfig = ''
expires 1y;
add_header Cache-Control "public, immutable";
'';
}; };
extraConfig = '' extraConfig = ''
index index.html index.htm index.php; index index.php index.html index.htm;
error_page 404 /index.php; error_page 404 /index.php;
if (!-e $request_filename) {
rewrite ^(.+)$ /index.php?q= last;
}
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' '*';
add_header 'Access-Control-Max-Age' 0;
add_header 'Content-Length' 0;
add_header 'Access-Control-Allow-Headers' 'X-API-COMPANY-KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Disposition,Content-Type,Range,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE';
add_header 'Access-Control-Expose-Headers' 'X-APP-VERSION,X-MINIMUM-CLIENT-VERSION,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE';
add_header 'Access-Control-Allow-Credentials' false;
''; '';
} }
(lib.mkIf (cfg.hostname != "localhost") { (lib.mkIf (cfg.hostname != "localhost") {
@@ -533,35 +546,42 @@ in
users.users."${config.services.caddy.user}" = lib.mkIf (cfg.proxy.server == "caddy") { users.users."${config.services.caddy.user}" = lib.mkIf (cfg.proxy.server == "caddy") {
extraGroups = [ cfg.group ]; extraGroups = [ cfg.group ];
}; };
services.caddy = lib.mkIf (cfg.proxy.server == "caddy") { services.caddy =
enable = true; let
proto_hostname = (
if (cfg.hostname == "localhost")
then (cfg.hostname + ":80")
else (cfg.hostname + ":443")
);
in
lib.mkIf (cfg.proxy.server == "caddy") {
enable = true;
globalConfig = lib.mkIf (cfg.hostname == "localhost") '' globalConfig = lib.mkIf (cfg.hostname == "localhost") ''
auto_https disable_redirects auto_https disable_redirects
''; '';
virtualHosts."${cfg.hostname}" = lib.mkMerge [ virtualHosts."${proto_hostname}" = lib.mkMerge [
cfg.proxy.caddyConfig cfg.proxy.caddyConfig
{ {
hostName = lib.mkForce cfg.hostname; hostName = lib.mkForce proto_hostname;
extraConfig = '' extraConfig = ''
encode zstd gzip encode zstd gzip
root * ${invoice-ninja}/public root ${invoiceninja}/public
php_fastcgi unix/${config.services.phpfpm.pools.invoice-ninja.socket} php_fastcgi unix/${config.services.phpfpm.pools.invoiceninja.socket}
try_files {path} /index.html header {
header { Access-Control-Allow-Origin "*"
Access-Control-Allow-Origin "*" Access-Control-Allow-Methods "*"
Access-Control-Allow-Methods "*" Access-Control-Max-Age "0"
Access-Control-Max-Age "0" Access-Control-Allow-Headers "X-API-COMPANY-KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Disposition,Content-Type,Range,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE"
Access-Control-Allow-Headers "X-API-COMPANY-KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Disposition,Content-Type,Range,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE" Access-Control-Expose-Headers "X-APP-VERSION,X-MINIMUM-CLIENT-VERSION,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE"
Access-Control-Expose-Headers "X-APP-VERSION,X-MINIMUM-CLIENT-VERSION,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE" Access-Control-Allow-Credentials false
Access-Control-Allow-Credentials false }
} file_server
file_server '';
''; }
} ];
]; };
};
services.mysql = lib.mkIf (cfg.database.createLocally) { services.mysql = lib.mkIf (cfg.database.createLocally) {
enable = lib.mkDefault true; enable = lib.mkDefault true;
@@ -577,33 +597,33 @@ in
]; ];
}; };
systemd.services.phpfpm-invoice-ninja.after = [ "invoice-ninja-data-setup.service" ]; systemd.services.phpfpm-invoiceninja.after = [ "invoiceninja-data-setup.service" ];
systemd.services.phpfpm-invoice-ninja.requires = [ systemd.services.phpfpm-invoiceninja.requires = [
"invoice-ninja-data-setup.service" "invoiceninja-data-setup.service"
] ++ lib.optional cfg.database.createLocally "mysql.service"; ] ++ lib.optional cfg.database.createLocally "mysql.service";
# Ensure chromium is available # Ensure chromium is available
systemd.services.phpfpm-invoice-ninja.path = extraPrograms; systemd.services.phpfpm-invoiceninja.path = extraPrograms;
systemd.services.invoice-ninja-queue-worker = { systemd.services.invoiceninja-queue-worker = {
description = "Invoice Ninja periodic tasks"; description = "Invoice Ninja periodic tasks";
after = [ "invoice-ninja-data-setup.service" ]; after = [ "invoiceninja-data-setup.service" ];
requires = [ "phpfpm-invoice-ninja.service" ]; requires = [ "phpfpm-invoiceninja.service" ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
reloadTriggers = [ invoice-ninja ]; reloadTriggers = [ invoiceninja ];
reload = "${invoice-ninja-manage}/bin/invoice-ninja-manage queue:restart"; reload = "${invoiceninja-manage}/bin/invoiceninja-manage queue:restart";
serviceConfig = { serviceConfig = {
ExecStart = "${invoice-ninja-manage}/bin/invoice-ninja-manage queue:work ${ ExecStart = "${invoiceninja-manage}/bin/invoiceninja-manage queue:work ${
lib.strings.optionalString (cfg.settings.QUEUE_CONNECTION == "redis") "redis" lib.strings.optionalString (cfg.settings.QUEUE_CONNECTION == "redis") "redis"
}"; }";
User = user; User = user;
Group = group; Group = group;
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/invoice-ninja") "invoice-ninja"; StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/invoiceninja") "invoiceninja";
Restart = "always"; Restart = "always";
}; };
}; };
systemd.services.invoice-ninja-data-setup = { systemd.services.invoiceninja-data-setup = {
description = "Invoice Ninja setup: migrations, environment file update, cache reload, data changes"; description = "Invoice Ninja setup: migrations, environment file update, cache reload, data changes";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = lib.optional cfg.database.createLocally "mysql.service"; after = lib.optional cfg.database.createLocally "mysql.service";
@@ -612,7 +632,7 @@ in
with pkgs; with pkgs;
[ [
bash bash
invoice-ninja-manage invoiceninja-manage
rsync rsync
config.services.mysql.package config.services.mysql.package
] ]
@@ -622,7 +642,7 @@ in
Type = "oneshot"; Type = "oneshot";
User = user; User = user;
Group = group; Group = group;
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/invoice-ninja") "invoice-ninja"; StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/invoiceninja") "invoiceninja";
StateDirectoryMode = "0750"; StateDirectoryMode = "0750";
LoadCredential = [ LoadCredential = [
"env-secrets:${cfg.secretFile}" "env-secrets:${cfg.secretFile}"
@@ -645,38 +665,33 @@ in
cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env
# Link the static storage (package provided) to the runtime storage # Link the static storage (package provided) to the runtime storage
# Necessary for cities.json and static images. rsync -av --no-perms ${invoiceninja}/storage-static/ ${cfg.dataDir}/storage
rsync -av --no-perms ${invoice-ninja}/storage-static/ ${cfg.dataDir}/storage
# Link the static vendor/bin (package provided) to the runtime vendor/bin
# Necessary for cities.json and static images.
rsync -av --no-perms ${invoice-ninja}/vendor/bin-static/ ${cfg.dataDir}/vendor/bin
# Link the app.php in the runtime folder. # Link the app.php in the runtime folder.
# We cannot link the cache folder only because bootstrap folder needs to be writeable. # 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 ln -sf ${invoiceninja}/bootstrap-static/app.php ${cfg.runtimeDir}/app.php
# Perform the first migration # Perform the first migration
[[ ! -f ${cfg.dataDir}/.initial-migration ]] && invoice-ninja-manage migrate --force && touch ${cfg.dataDir}/.initial-migration [[ ! -f ${cfg.dataDir}/.initial-migration ]] && invoiceninja-manage migrate --force && touch ${cfg.dataDir}/.initial-migration
# Seed database with records # Seed database with records
# Necessary for languages, currencies, countries, etc. # Necessary for languages, currencies, countries, etc.
[[ ! -f ${cfg.dataDir}/.db-seeded ]] && invoice-ninja-manage db:seed --force && touch ${cfg.dataDir}/.db-seeded [[ ! -f ${cfg.dataDir}/.db-seeded ]] && invoiceninja-manage db:seed --force && touch ${cfg.dataDir}/.db-seeded
# Create Invoice Ninja admin account # Create Invoice Ninja admin account
[[ (! -f ${cfg.dataDir}/.admin-created) && (${ [[ (! -f ${cfg.dataDir}/.admin-created) && (${
if cfg.adminAccount.createAdmin then "true" else "false" if cfg.adminAccount.enable then "true" else "false"
} == "true") ]] \ } == "true") ]] \
&& invoice-ninja-manage ninja:create-account --email=${cfg.adminAccount.email} --password=$(cat $CREDENTIALS_DIRECTORY/admin-pass) \ && invoiceninja-manage ninja:create-account --email=${cfg.adminAccount.email} --password=$(cat $CREDENTIALS_DIRECTORY/admin-pass) \
&& touch ${cfg.dataDir}/.admin-created && touch ${cfg.dataDir}/.admin-created
invoice-ninja-manage route:cache invoiceninja-manage route:cache
invoice-ninja-manage view:cache invoiceninja-manage view:cache
invoice-ninja-manage config:cache invoiceninja-manage config:cache
''; '';
}; };
systemd.tmpfiles.settings."10-invoice-ninja" = systemd.tmpfiles.settings."10-invoiceninja" =
lib.attrsets.genAttrs lib.attrsets.genAttrs
[ [
cfg.dataDir cfg.dataDir
+89 -32
View File
@@ -1,62 +1,119 @@
{ lib { buildNpmPackage
, php , npmHooks
, openssl
, writers
, fetchFromGitHub , fetchFromGitHub
, dataDir ? "/var/lib/invoice-ninja" , fetchNpmDeps
, runtimeDir ? "/run/invoice-ninja" , php82
, nodejs_22
, npm-lockfile-fix
, dataDir ? "/var/lib/invoiceninja"
, runtimeDir ? "/run/invoiceninja"
}: }:
php.buildComposerProject (finalAttrs: { let
pname = "invoice-ninja"; pname = "invoiceninja";
version = "5.11.3";
version = "5.13.19";
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "invoiceninja"; name = "${pname}";
repo = "invoiceninja"; owner = pname;
rev = "v${finalAttrs.version}"; repo = pname;
hash = "sha256-PsDHQAFy7fnz03zTdJtcUzOVKT6jjHv0DmVmyLAOwkc="; rev = "v${version}";
hash = "sha256-pvZNTiGGX6OqOEqcDrDqqpLD1Ohc/TA4nIRjm2jEp74=";
postFetch = ''
# add missing integrity fields to lockfile
${npm-lockfile-fix}/bin/npm-lockfile-fix $out/package-lock.json
'';
}; };
vendorHash = "sha256-RA7IkPXz1HdqQAyB/VIbYg3BmCnlJKLxIVtODIRmZxg="; uiVersion = "03.05.2026.1";
uiSrc = fetchFromGitHub {
name = "${pname}-ui";
owner = pname;
repo = "ui";
tag = uiVersion;
hash = "sha256-BTaWNHTE+9NvavUYs56DdJHmqXy36N5k/crj3rU2Npg=";
# Patch sources for more restrictive permissions postFetch = ''
patches = [ # add missing integrity fields to lockfile
./disable-react-for-admin.patch ${npm-lockfile-fix}/bin/npm-lockfile-fix $out/package-lock.json
'';
};
# React frontend
ui = buildNpmPackage {
pname = "${pname}-ui";
version = uiVersion;
src = uiSrc;
nodejs = nodejs_22;
npmDepsHash = "sha256-JA5TfXeg7iHVjQdjeU6SSD2JFSLISad8hPowaR1roQw=";
preConfigure = ''
sed -i 's/VITE_IS_TEST=true/VITE_IS_TEST=false/' .env.example
cp .env.example .env
cp ${src}/vite.config.ts.react ./vite.config.js
'';
installPhase = ''
runHook preInstall
mkdir $out
cp -a * $out
runHook postInstall
'';
};
in
php82.buildComposerProject (finalAttrs: {
inherit src version;
pname = "${pname}-composer";
nativeBuildInputs = [
nodejs_22
npmHooks.npmConfigHook
npmHooks.npmBuildHook
]; ];
vendorHash = "sha256-2CyWKlyCyoCt/WdY7Ta0oEXW2KIkGwhilHtzpBJ5Wnk=";
# Upstream composer.json has invalid license, webpatser/laravel-countries package is pointing # Upstream composer.json has invalid license, webpatser/laravel-countries package is pointing
# to commit-ref, and php required in require and require-dev # to commit-ref, and php required in require and require-dev
composerStrictValidation = false; composerStrictValidation = false;
CYPRESS_INSTALL_BINARY = 0;
npmDeps = fetchNpmDeps {
name = "${pname}-${version}-npm-deps";
inherit src;
hash = "sha256-WCadsQVj9eusYWJEOaEp5DIkVcbmPFGe6bmKi9NZDDg=";
};
preConfigure = ''
cp -r ${ui}/dist/* public/
cp public/index.html resources/views/react/index.blade.php
'';
postInstall = '' postInstall = ''
mv "$out/share/php/${finalAttrs.pname}"/* $out mv "$out/share/php/${finalAttrs.pname}"/* $out
rm -R $out/bootstrap/cache
# Remove JS/CSS build artifacts
rm -rf $out/node_modules
rm -r $out/bootstrap/cache
rm -rf $out/public/storage rm -rf $out/public/storage
# Move static contents for the NixOS module to pick it up, if needed. # Move static contents for the NixOS module to pick it up, if needed.
mv $out/bootstrap $out/bootstrap-static mv $out/bootstrap $out/bootstrap-static
mv $out/storage $out/storage-static mv $out/storage $out/storage-static
mv $out/vendor/bin $out/vendor/bin-static
# Link NixOS module files to derivation output # Link NixOS module files to derivation output
ln -s ${dataDir}/.env $out/.env ln -s ${dataDir}/.env $out/.env
ln -s ${dataDir}/storage $out/ ln -s ${dataDir}/storage $out/
ln -s ${dataDir}/storage/app/public $out/public/storage ln -s ${dataDir}/storage/app/public $out/public/storage
ln -s ${dataDir}/vendor/bin $out/vendor
ln -s ${runtimeDir} $out/bootstrap ln -s ${runtimeDir} $out/bootstrap
''; '';
meta = {
description = "Open-source, self-hosted invoicing application";
homepage = "https://www.invoiceninja.com/";
license = with lib.licenses; {
fullName = "Elastic License 2.0";
shortName = "Elastic-2.0";
free = false;
};
platforms = lib.platforms.all;
};
}) })
-26
View File
@@ -1,26 +0,0 @@
{ pkgs ? import <nixpkgs> { config.allowUnfree = true; }
, lib ? import <nixpkgs/lib>
}:
let
# Helper script to generate an APP_KEY for .env
generate-invoice-ninja-app-key = pkgs.writers.writeBashBin "generate-laravel-key" ''
echo "APP_KEY=base64:$(${pkgs.openssl}/bin/openssl rand -base64 32)"
'';
# Invoice Ninja derivation
# Add to buildInputs to test in nix-shell environment
invoice-ninja = pkgs.callPackage ./default.nix {
inherit lib;
php = pkgs.php;
openssl = pkgs.openssl;
fetchFromGitHub = pkgs.fetchFromGitHub;
};
in
pkgs.mkShell {
buildInputs = [
generate-invoice-ninja-app-key
pkgs.nixpkgs-fmt
];
}
+31
View File
@@ -0,0 +1,31 @@
{ pkgs, modulesPath, ... }:
{
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
../nixos-module/invoiceninja.nix
];
system.stateVersion = "25.11";
nixpkgs.config.allowUnfree = true;
users.users.test = {
isNormalUser = true;
extraGroups = [ "wheel" ];
initialPassword = "test";
};
services.invoiceninja = {
enable = true;
proxy.server = "nginx";
adminAccount.passwordFile = ./invoice_ninja_test_password;
secretFile = ./test.env;
settings.APP_DEBUG = true;
};
networking.firewall.enable = false;
services.resolved.enable = true;
}
+1
View File
@@ -0,0 +1 @@
password
-53
View File
@@ -1,53 +0,0 @@
{ modulesPath, ... }:
{
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
../invoice-ninja.nix
];
system.stateVersion = "24.05";
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" ];
initialPassword = "test";
};
services.invoice-ninja = {
enable = true;
database.createLocally = true;
webserver.caddy.enable = true;
webserver.nginx.enable = false;
mail.mailFromName = "Andrew Bryant";
adminEmail = "billing@allpawcare.com";
msmtp.accounts.invoice-ninja = {
auth = true;
tls = true;
tls_starttls = false;
from = "billing@allpawcare.com";
host = "smtp.fastmail.com";
port = 465;
user = "awkawb@awkawb.cloud";
passwordeval = "cat /etc/msmtp-password";
};
secretFile = ./test-secrets.env;
};
networking.firewall.enable = false;
services.resolved.enable = true;
}
-1
View File
@@ -1 +0,0 @@
test