invoice-ninja.nix: a bunch of updates and changes

This commit is contained in:
Andrew Bryant 2024-12-27 15:51:15 -05:00
parent f866421659
commit 1b5a64e29a

View File

@ -1,24 +1,55 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
options,
...
}:
let let
cfg = config.services.invoice-ninja; cfg = config.services.invoice-ninja;
user = cfg.user; user = cfg.user;
group = cfg.group; group = cfg.group;
testing = pkgs.callPackage ./default.nix { inherit lib; php = pkgs.php; fetchFromGitHub = pkgs.fetchFromGitHub; }; invoice-ninja = pkgs.callPackage ./default.nix {
invoice-ninja = testing.override { inherit (cfg) dataDir runtimeDir; }; inherit (cfg) dataDir runtimeDir;
};
configFormat = pkgs.formats.keyValue { };
configFile = pkgs.writeText "invoice-ninja-env" (lib.generators.toKeyValue { } cfg.settings); configFile = pkgs.writeText "invoice-ninja-env" (lib.generators.toKeyValue { } cfg.settings);
# PHP environment # PHP environment
phpPackage = cfg.phpPackage.buildEnv { phpPackage = cfg.phpPackage.buildEnv {
extensions = { enabled, all }: enabled ++ (with all; extensions =
[ bcmath ctype curl fileinfo gd gmp iconv mbstring mysqli openssl pdo tokenizer zip ] { enabled, all }:
(
with all;
enabled
++ [
bcmath
ctype
curl
fileinfo
gd
gmp
iconv
imagick
intl
mbstring
mysqli
openssl
pdo
soap
tokenizer
zip
]
++ lib.optional cfg.redis.createLocally redis
); );
extraConfig = "memory_limit = 1024M"; extraConfig = "memory_limit = 1024M";
}; };
# Chromium is required for PDF invoice generation # Chromium is required for PDF invoice generation
extraPrograms = with pkgs; [ chromium ]; chromium = pkgs.chromium;
extraPrograms = [ chromium ];
# Management script # Management script
invoice-ninja-manage = pkgs.writeShellScriptBin "invoice-ninja-manage" '' invoice-ninja-manage = pkgs.writeShellScriptBin "invoice-ninja-manage" ''
@ -44,11 +75,9 @@ in
description = '' description = ''
User account under which Invoice Ninja runs. User account under which Invoice Ninja runs.
::: {.note}
If left as the default value this user will automatically be created If left as the default value this user will automatically be created
on system activation, otherwise you are responsible for on system activation, otherwise you are responsible for
ensuring the user exists before the Invoice Ninja application starts. ensuring the user exists before the Invoice Ninja application starts.
:::
''; '';
}; };
@ -58,14 +87,41 @@ in
description = '' description = ''
Group account under which Invoice Ninja runs. Group account under which Invoice Ninja runs.
::: {.note}
If left as the default value this group will automatically be created If left as the default value this group will automatically be created
on system activation, otherwise you are responsible for on system activation, otherwise you are responsible for
ensuring the group exists before the Invoice Ninja application starts. ensuring the group exists before the Invoice Ninja application starts.
:::
''; '';
}; };
msmtp.accounts.invoice-ninja = lib.mkOption {
type = lib.types.attrs;
default = { };
example = {
host = "smtp.example";
port = 25;
auth = true;
tls = true;
tls_starttls = true;
user = "realperson";
passwordeval = "cat /secrets/password.txt";
};
description = ''
Define the msmtp configuration for an invoice-ninja account which
will be used by Invoice Ninja to send email message when
`config.services.invoice-ninja.settings.MAIL_MAILER` is `sendmail`.
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`).
'';
};
redis.createLocally = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable a local Redis server for Invoice Ninja.";
};
dataDir = lib.mkOption { dataDir = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "/var/lib/invoice-ninja"; default = "/var/lib/invoice-ninja";
@ -84,27 +140,28 @@ in
''; '';
}; };
schedulerInterval = lib.mkOption { hostname = lib.mkOption {
type = lib.types.str;
default = "1d";
description = "How often the Invoice Ninja cron task should run.";
};
domain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "localhost"; default = "localhost";
description = '' description = ''
FQDN for the Invoice Ninja instance. FQDN for the Invoice Ninja instance.
:: note
The default value is useful for local development and
disables HTTPS. Set to anything else to enable HTTPS.
::
''; '';
}; };
phpfpm.settings = lib.mkOption { phpfpm.settings = lib.mkOption {
type = with lib.types; attrsOf (oneOf [ int str bool ]); type =
with lib.types;
attrsOf (oneOf [
int
str
bool
]);
default = { default = {
"listen.owner" = user;
"listen.group" = group;
"listen.mode" = "0660";
"pm" = "dynamic"; "pm" = "dynamic";
"pm.start_servers" = "2"; "pm.start_servers" = "2";
"pm.min_spare_servers" = "2"; "pm.min_spare_servers" = "2";
@ -116,8 +173,6 @@ in
"php_admin_value[error_log]" = "stderr"; "php_admin_value[error_log]" = "stderr";
"php_admin_flag[log_errors]" = true; "php_admin_flag[log_errors]" = true;
"catch_workers_output" = true;
}; };
description = '' description = ''
@ -134,54 +189,227 @@ in
}; };
settings = lib.mkOption { settings = lib.mkOption {
type = with lib.types; (attrsOf (oneOf [ bool int str ])); default = { };
description = '' description = ''
.env settings for Invoice Ninja. .env settings for Invoice Ninja.
Secrets should use `secretFile` option instead. Secrets should use `secretFile` option instead.
''; '';
example = lib.literalExpression ''
{
EXTENDED_LOGGING = true;
SESSION_DRIVER = "file";
MAIL_MAILER = "sendmail";
}
'';
type = lib.types.submodule {
freeformType = configFormat.type;
options = {
APP_NAME = lib.mkOption {
type = lib.types.str;
default = "Invoice Ninja";
description = "Your application name - used in client portal title banner";
};
APP_DEBUG = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Puts Invoice Ninja in debug mode for additional logging. You should only set to `true` if your
debugging.
'';
};
APP_ENV = lib.mkOption {
type = lib.types.enum [
"development"
"local"
"production"
];
default = "production";
description = ''
Environment Invoice Ninja is set to run in. Leave set to the default unless your working on
Invoice Ninja code or this module definition.
'';
};
EXTENDED_LOGGING = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Pushes additional logging to the log channel";
};
LOG_CHANNEL = lib.mkOption {
type = lib.types.enum [
"invoiceninja"
"null"
"stack"
];
default = "stack";
description = "Where we send logs to.";
};
MAIL_MAILER = lib.mkOption {
type = lib.types.enum [
""
"log"
"sendmail"
"mailgun"
];
default = "";
description = "Controls the method used by Invoice Ninja to send mail.";
};
MAIL_SENDMAIL_PATH = lib.mkOption {
type = lib.types.str;
default = ''"/run/wrappers/bin/sendmail -t -a invoice-ninja"'';
description = ''
Path to sendmail along with arguments for Invoice Ninja to use when using sendmail
as mail transport agent.
:: note
The default value will work with the `msmtp.accounts.invoice-ninja` setting. Only
change if you know what your doing.
::
'';
};
MAIL_FROM_NAME = lib.mkOption {
type = lib.types.str;
default = "";
example = "Real Person";
description = "Set the 'To' email header name attribute.";
};
MAIL_FROM_EMAIL = lib.mkOption {
type = lib.types.str;
default = "";
example = "example@email.com";
description = "Set the 'To' email header address attribute.";
};
ERROR_EMAIL = lib.mkOption {
type = lib.types.str;
default = "";
example = "example@email.com";
description = "Email address Invoice Ninja will send errors.";
};
SESSION_DRIVER = lib.mkOption {
type = lib.types.enum [
"database"
"redis"
"file"
];
default =
if cfg.redis.createLocally then "redis" else (if cfg.database.createLocally then "database" else "file");
defaultText = lib.literalExpression ''
if config.services.invoice-ninja.redis.enable
then "redis"
else (if config.services.invoie-ninja.database.createLocally then "database" else "file")
'';
description = "Where session data is stored for Invoice Ninja.";
};
SESSION_LIFETIME = lib.mkOption {
type = lib.types.int;
default = 120;
description = ''
Here you may specify the number of minutes that you wish the session to be allowed to remain
idle before it expires.
'';
};
QUEUE_CONNECTION = lib.mkOption {
type = lib.types.enum [
"database"
"redis"
"sync"
];
default =
if cfg.redis.createLocally then "redis" else (if cfg.database.createLocally then "database" else "sync");
defaultText = lib.literalExpression ''
if config.services.invoice-ninja.redis.enable
then "redis"
else (if config.services.invoie-ninja.database.createLocally then "database" else "sync")
'';
description = "Where Invoice Ninja will store queued jobs.";
};
CACHE_DRIVER = lib.mkOption {
type = lib.types.enum [
"file"
"redis"
];
default = if cfg.redis.createLocally then "redis" else "file";
defaultText = lib.literalExpression ''if config.services.invoice-ninja.redis.enable then "redis" else "file"'';
description = "Laravel cache driver for Invoice Ninja to use.";
};
};
};
};
adminAccount = {
createAdmin = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
When set to `true`, an admin account will be created for Invoice Ninja. If set to `false`
Invoice Ninja will run a setup wizard on first use.
'';
};
email = lib.mkOption {
type = lib.types.str;
default = "example@email.com";
description = "Email address of the first (admin) account for this Invoice Ninja installation";
};
passwordFile = lib.mkOption {
type = lib.types.path;
description = "Password of the first (admin) account for this Invoice Ninja installation";
};
}; };
database = { database = {
createLocally = lib.mkOption { createLocally = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;
description = "a local database using UNIX socket authentication"; description = "Installs a local MariaDB server to use with Invoice Ninja.";
}; };
name = lib.mkOption { name = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "invoiceninja"; default = "invoiceninja";
description = "Database name for Invoice Ninja."; description = "Name of the database to use for Invoice Ninja.";
}; };
}; };
enableACME = lib.mkOption { maxUploadSize = lib.mkOption {
type = lib.types.bool; type = lib.types.str;
default = false; default = "8M";
description = '' description = "Maximum allowed upload size to Invoice Ninja.";
Whether an ACME certificate should be used to secure connections to the server.
'';
}; };
nginx = lib.mkOption { proxy = {
type = lib.types.nullOr (lib.types.submodule server = lib.mkOption {
(import <nixpkgs/nixos/modules/services/web-servers/nginx/vhost-options.nix> { type = lib.types.enum [
inherit config lib; "caddy"
})); "nginx"
default = null; "none"
example = '' ];
{ default = "nginx";
enableACME = true; example = "caddy";
forceHttps = true;
}
'';
description = '' description = ''
With this option, you can customize an nginx virtual host which already has sensible defaults Choose the proxy server to serve Invoice Ninja. Setting this to
for Invoice Ninja. Set to {} if you do not need any customization to the virtual host. `none` results in no proxy server being installed.
If enabled, then by default, the {option}`serverName` is `''${domain}`. If this is set to
null (the default), no nginx virtualHost will be configured.
''; '';
}; };
caddyConfig = lib.mkOption {
type = lib.types.submodule (
import (../. + "/web-servers/caddy/vhost-options.nix") { cfg = config.services.caddy; }
);
default = { };
description = ''
Extra configuration for the Caddy virtual host of Invoice Ninja.
Leave this option at the default to use the default configuration
'';
};
nginxConfig = lib.mkOption {
type = lib.types.submodule (
(import (../. + "/web-servers/nginx/vhost-options.nix") { inherit config lib; })
);
default = { };
description = ''
Extra configuration for the Nginx virtual host of Invoice Ninja.
Leave this option at the default to use the default configuration
'';
};
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
@ -196,35 +424,24 @@ in
environment.systemPackages = [ invoice-ninja-manage ] ++ extraPrograms; environment.systemPackages = [ invoice-ninja-manage ] ++ extraPrograms;
services.invoice-ninja.settings = programs.msmtp = lib.mkIf (cfg.settings.MAIL_MAILER == "sendmail") {
let inherit (cfg.msmtp) accounts;
app_http_url = "http://${cfg.domain}"; enable = true;
app_https_url = "https://${cfg.domain}"; };
react_http_url = "http://${cfg.domain}:3001";
react_https_url = "https://${cfg.domain}:3001"; services.invoice-ninja.settings = lib.mkMerge [
chromium = lib.lists.findSingle (x: x == pkgs.chromium) "none" "multiple" extraPrograms;
in
lib.mkMerge [
({ ({
APP_NAME = lib.mkDefault "\"Invoice Ninja\""; APP_URL = lib.mkDefault (
APP_ENV = lib.mkDefault "production"; if (cfg.hostname == "localhost") then ("http://" + cfg.hostname) else ("https://" + cfg.hostname)
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}");
DB_CONNECTION = lib.mkDefault "mysql";
MULTI_DB_ENABLED = lib.mkDefault false;
DEMO_MODE = lib.mkDefault false; DEMO_MODE = lib.mkDefault false;
BROADCAST_DRIVER = lib.mkDefault "log"; BROADCAST_DRIVER = lib.mkDefault "log";
LOG_CHANNEL = lib.mkDefault "stack"; REQUIRE_HTTPS = lib.mkDefault (if (cfg.hostname != "localhost") then true else false);
CACHE_DRIVER = lib.mkDefault "file"; TRUSTED_PROXIES = lib.mkDefault "*";
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);
TRUSTED_PROXIES = lib.mkDefault "127.0.0.1";
NINJA_ENVIRONMENT = lib.mkDefault "selfhost"; NINJA_ENVIRONMENT = lib.mkDefault "selfhost";
PDF_GENERATOR = lib.mkDefault "snappdf"; PDF_GENERATOR = lib.mkDefault "snappdf";
SNAPPDF_CHROMIUM_PATH = lib.mkDefault "${chromium}/bin/chromium"; SNAPPDF_CHROMIUM_PATH = lib.mkDefault "${chromium}/bin/chromium";
PRECONFIGURED_INSTALL = lib.mkDefault true;
}) })
(lib.mkIf (cfg.database.createLocally) { (lib.mkIf (cfg.database.createLocally) {
DB_CONNECTION = lib.mkDefault "mysql"; DB_CONNECTION = lib.mkDefault "mysql";
@ -233,79 +450,115 @@ in
DB_DATABASE = lib.mkDefault cfg.database.name; DB_DATABASE = lib.mkDefault cfg.database.name;
DB_USERNAME = lib.mkDefault user; DB_USERNAME = lib.mkDefault user;
}) })
(lib.mkIf cfg.redis.createLocally {
REDIS_HOST = lib.mkDefault config.services.redis.servers.invoice-ninja.bind;
REDIS_PORT = lib.mkDefault config.services.redis.servers.invoice-ninja.port;
})
]; ];
services.phpfpm.pools.invoiceninja = { services.phpfpm.pools.invoice-ninja = {
inherit user group phpPackage; 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}
upload_max_filesize = ${cfg.maxUploadSize}
max_execution_time = 600;
'';
}; };
users.users."${config.services.nginx.user}" = lib.mkIf (cfg.nginx != null) { extraGroups = [ cfg.group ]; }; services.redis.servers.invoice-ninja = lib.mkIf cfg.redis.createLocally {
services.nginx = lib.mkIf (cfg.nginx != null) { enable = true;
port = 6379;
};
users.users."${config.services.nginx.user}" = lib.mkIf (cfg.proxy.server == "nginx") {
extraGroups = [ cfg.group ];
};
services.nginx = lib.mkIf (cfg.proxy.server == "nginx") {
enable = true; enable = true;
clientMaxBodySize = "20m";
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = true; recommendedTlsSettings = true;
recommendedGzipSettings = true;
recommendedProxySettings = true;
recommendedOptimisation = true;
appendHttpConfig = '' clientMaxBodySize = cfg.maxUploadSize;
# 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 virtualHosts."${cfg.hostname}" = lib.mkMerge [
add_header 'Referrer-Policy' 'origin-when-cross-origin'; cfg.proxy.nginxConfig
# 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; root = lib.mkForce "${invoice-ninja}/public";
forceSSL = lib.mkDefault (if cfg.enableACME then true else false); addSSL = lib.mkForce true;
root = lib.mkForce "${invoice-ninja}/public/"; enableACME = lib.mkForce true;
locations."= /index.php" = { locations = {
extraConfig = '' "/".tryFiles = "$uri $uri/ /index.php?$query_string";
fastcgi_pass unix:${config.services.phpfpm.pools.invoiceninja.socket}; "/".extraConfig = ''
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) { if (!-e $request_filename) {
rewrite ^(.+)$ /index.php?q=$1 last; rewrite ^(.+)$ /index.php?q=$1 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;
''; '';
}; "~ \\.php$".extraConfig = "return 403;";
locations."~ /\\.ht" = { "= /index.php".extraConfig = ''
extraConfig = '' fastcgi_pass unix:${config.services.phpfpm.pools.invoice-ninja.socket};
deny all;
''; '';
"~ /\\.ht".extraConfig = "deny all;";
}; };
extraConfig = '' extraConfig = ''
index index.php index.html index.htm; index index.html index.htm index.php;
error_page 404 /index.php; error_page 404 /index.php;
''; '';
} }
(lib.mkIf (cfg.hostname != "localhost") {
forceSSL = lib.mkDefault true;
enableACME = lib.mkDefault true;
})
];
};
users.users."${config.services.caddy.user}" = lib.mkIf (cfg.proxy.server == "caddy") {
extraGroups = [ cfg.group ];
};
services.caddy = lib.mkIf (cfg.proxy.server == "caddy") {
enable = true;
globalConfig = lib.mkIf (cfg.hostname == "localhost") ''
auto_https disable_redirects
'';
virtualHosts."${cfg.hostname}" = lib.mkMerge [
cfg.proxy.caddyConfig
{
hostName = lib.mkForce cfg.hostname;
extraConfig = ''
encode zstd gzip
root * ${invoice-ninja}/public
php_fastcgi unix/${config.services.phpfpm.pools.invoice-ninja.socket}
try_files {path} /index.html
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "*"
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-Expose-Headers "X-APP-VERSION,X-MINIMUM-CLIENT-VERSION,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE"
Access-Control-Allow-Credentials false
}
file_server
'';
}
]; ];
}; };
@ -313,48 +566,56 @@ in
enable = lib.mkDefault true; enable = lib.mkDefault true;
package = lib.mkDefault pkgs.mariadb; package = lib.mkDefault pkgs.mariadb;
ensureDatabases = [ cfg.database.name ]; ensureDatabases = [ cfg.database.name ];
ensureUsers = [{ ensureUsers = [
{
name = user; name = user;
ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; ensurePermissions = {
}]; "${cfg.database.name}.*" = "ALL PRIVILEGES";
};
}
];
}; };
systemd.services.phpfpm-invoice-ninja.after = [ "invoice-ninja-data-setup.service" ]; systemd.services.phpfpm-invoice-ninja.after = [ "invoice-ninja-data-setup.service" ];
systemd.services.phpfpm-invoice-ninja.requires = [ "invoice-ninja-data-setup.service" ] systemd.services.phpfpm-invoice-ninja.requires = [
++ lib.optional cfg.database.createLocally "mysql.service"; "invoice-ninja-data-setup.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-invoice-ninja.path = extraPrograms;
systemd.timers.invoice-ninja-cron = { systemd.services.invoice-ninja-queue-worker = {
description = "Invoice Ninja periodic tasks timer"; description = "Invoice Ninja periodic tasks";
after = [ "invoice-ninja-data-setup.service" ]; after = [ "invoice-ninja-data-setup.service" ];
requires = [ "phpfpm-invoice-ninja.service" ]; requires = [ "phpfpm-invoice-ninja.service" ];
wantedBy = [ "timers.target" ]; wantedBy = [ "multi-user.target" ];
reloadTriggers = [ invoice-ninja ];
timerConfig = { reload = "${invoice-ninja-manage}/bin/invoice-ninja-manage queue:restart";
OnBootSec = cfg.schedulerInterval;
OnUnitActiveSec = cfg.schedulerInterval;
};
};
systemd.services.invoice-ninja-cron = {
description = "Invoice Ninja periodic tasks";
serviceConfig = { serviceConfig = {
ExecStart = "${invoice-ninja-manage}/bin/invoice-ninja-manage schedule:run"; ExecStart = "${invoice-ninja-manage}/bin/invoice-ninja-manage queue:work ${
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/invoice-ninja") "invoice-ninja";
Restart = "always";
}; };
}; };
systemd.services.invoice-ninja-data-setup = { systemd.services.invoice-ninja-data-setup = {
description = description = "Invoice Ninja setup: migrations, environment file update, cache reload, data changes";
"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";
requires = 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 = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
@ -362,7 +623,10 @@ in
Group = group; Group = group;
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/invoice-ninja") "invoice-ninja"; StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/invoice-ninja") "invoice-ninja";
StateDirectoryMode = "0750"; StateDirectoryMode = "0750";
LoadCredential = "env-secrets:${cfg.secretFile}"; LoadCredential = [
"env-secrets:${cfg.secretFile}"
"admin-pass:${cfg.adminAccount.passwordFile}"
];
UMask = "077"; UMask = "077";
}; };
@ -376,37 +640,34 @@ in
# Concatenate non-secret .env and secret .env # Concatenate non-secret .env and secret .env
rm -f ${cfg.dataDir}/.env rm -f ${cfg.dataDir}/.env
cp --no-preserve=all ${configFile} ${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 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. # Necessary for cities.json and static images.
mkdir -p ${cfg.dataDir}/storage
rsync -av --no-perms ${invoice-ninja}/storage-static/ ${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 # Link the static vendor/bin (package provided) to the runtime vendor/bin
chmod -R g+rX ${cfg.dataDir}/storage/app/public # 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 ${invoice-ninja}/bootstrap-static/app.php ${cfg.runtimeDir}/app.php
# Link the static public/images (package provided) to the runtime public/public/images # Perform the first migration
mkdir -p ${cfg.dataDir}/public/images [[ ! -f ${cfg.dataDir}/.initial-migration ]] && invoice-ninja-manage migrate --force && touch ${cfg.dataDir}/.initial-migration
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 # Seed database with records
mkdir -p ${cfg.dataDir}/public/react # Necessary for languages, currencies, countries, etc.
rsync -av --no-perms ${invoice-ninja}/public/react-static/ ${cfg.dataDir}/public/react [[ ! -f ${cfg.dataDir}/.db-seeded ]] && invoice-ninja-manage db:seed --force && touch ${cfg.dataDir}/.db-seeded
chmod -R +w ${cfg.dataDir}/public # Create Invoice Ninja admin account
chmod -R g+rwX ${cfg.dataDir}/public [[ (! -f ${cfg.dataDir}/.admin-created) && (${
if cfg.adminAccount.createAdmin then "true" else "false"
# Link the static vite.config.ts (package provided to the runtime vite.config.ts } == "true") ]] \
rsync -av --no-perms ${invoice-ninja}/vite.config.ts.static ${cfg.dataDir}/vite.config.ts && invoice-ninja-manage ninja:create-account --email=${cfg.adminAccount.email} --password=$(cat $CREDENTIALS_DIRECTORY/admin-pass) \
chmod +w ${cfg.dataDir}/vite.config.ts && touch ${cfg.dataDir}/.admin-created
chmod g+rw ${cfg.dataDir}/vite.config.ts
invoice-ninja-manage route:cache invoice-ninja-manage route:cache
invoice-ninja-manage view:cache invoice-ninja-manage view:cache
@ -414,11 +675,40 @@ in
''; '';
}; };
systemd.tmpfiles.rules = [ systemd.tmpfiles.settings."10-invoice-ninja" =
# Cache must live across multiple systemd units runtimes. lib.attrsets.genAttrs
"d ${cfg.runtimeDir}/ 0700 ${user} ${group} - -" [
"d ${cfg.runtimeDir}/cache 0700 ${user} ${group} - -" 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/testing"
"${cfg.dataDir}/storage/framework/views"
"${cfg.dataDir}/storage/logs"
"${cfg.dataDir}/vendor"
"${cfg.dataDir}/vendor/bin"
]
(n: {
d = {
user = user;
group = group;
mode = "0770";
};
})
// lib.attrsets.genAttrs
[
cfg.runtimeDir
"${cfg.runtimeDir}/cache"
]
(n: {
d = {
user = user;
group = group;
mode = "0750";
};
});
}; };
} }