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
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;
};
configFormat = pkgs.formats.keyValue { };
configFile = pkgs.writeText "invoice-ninja-env" (lib.generators.toKeyValue { } cfg.settings);
# 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 ]
extensions =
{ 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";
};
# Chromium is required for PDF invoice generation
extraPrograms = with pkgs; [ chromium ];
chromium = pkgs.chromium;
extraPrograms = [ chromium ];
# Management script
invoice-ninja-manage = pkgs.writeShellScriptBin "invoice-ninja-manage" ''
@ -44,11 +75,9 @@ in
description = ''
User account under which Invoice Ninja runs.
::: {.note}
If left as the default value this user will automatically be created
on system activation, otherwise you are responsible for
ensuring the user exists before the Invoice Ninja application starts.
:::
'';
};
@ -58,14 +87,41 @@ in
description = ''
Group account under which Invoice Ninja runs.
::: {.note}
If left as the default value this group will automatically be created
on system activation, otherwise you are responsible for
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 {
type = lib.types.str;
default = "/var/lib/invoice-ninja";
@ -84,27 +140,28 @@ in
'';
};
schedulerInterval = lib.mkOption {
type = lib.types.str;
default = "1d";
description = "How often the Invoice Ninja cron task should run.";
};
domain = lib.mkOption {
hostname = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = ''
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 {
type = with lib.types; attrsOf (oneOf [ int str bool ]);
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";
@ -116,8 +173,6 @@ in
"php_admin_value[error_log]" = "stderr";
"php_admin_flag[log_errors]" = true;
"catch_workers_output" = true;
};
description = ''
@ -134,54 +189,227 @@ in
};
settings = lib.mkOption {
type = with lib.types; (attrsOf (oneOf [ bool int str ]));
default = { };
description = ''
.env settings for Invoice Ninja.
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 = {
createLocally = lib.mkOption {
type = lib.types.bool;
default = true;
description = "a local database using UNIX socket authentication";
description = "Installs a local MariaDB server to use with Invoice Ninja.";
};
name = lib.mkOption {
type = lib.types.str;
default = "invoiceninja";
description = "Database name for Invoice Ninja.";
description = "Name of the database to use for Invoice Ninja.";
};
};
enableACME = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether an ACME certificate should be used to secure connections to the server.
'';
maxUploadSize = lib.mkOption {
type = lib.types.str;
default = "8M";
description = "Maximum allowed upload size to Invoice Ninja.";
};
nginx = lib.mkOption {
type = lib.types.nullOr (lib.types.submodule
(import <nixpkgs/nixos/modules/services/web-servers/nginx/vhost-options.nix> {
inherit config lib;
}));
default = null;
example = ''
{
enableACME = true;
forceHttps = true;
}
'';
proxy = {
server = lib.mkOption {
type = lib.types.enum [
"caddy"
"nginx"
"none"
];
default = "nginx";
example = "caddy";
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.
Choose the proxy server to serve Invoice Ninja. Setting this to
`none` results in no proxy server being installed.
'';
};
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 {
@ -196,35 +424,24 @@ in
environment.systemPackages = [ invoice-ninja-manage ] ++ extraPrograms;
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";
chromium = lib.lists.findSingle (x: x == pkgs.chromium) "none" "multiple" extraPrograms;
in
lib.mkMerge [
programs.msmtp = lib.mkIf (cfg.settings.MAIL_MAILER == "sendmail") {
inherit (cfg.msmtp) accounts;
enable = true;
};
services.invoice-ninja.settings = lib.mkMerge [
({
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}");
DB_CONNECTION = lib.mkDefault "mysql";
MULTI_DB_ENABLED = lib.mkDefault false;
APP_URL = lib.mkDefault (
if (cfg.hostname == "localhost") then ("http://" + cfg.hostname) else ("https://" + cfg.hostname)
);
DEMO_MODE = lib.mkDefault false;
BROADCAST_DRIVER = lib.mkDefault "log";
LOG_CHANNEL = lib.mkDefault "stack";
CACHE_DRIVER = lib.mkDefault "file";
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";
REQUIRE_HTTPS = lib.mkDefault (if (cfg.hostname != "localhost") then true else false);
TRUSTED_PROXIES = lib.mkDefault "*";
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";
@ -233,79 +450,115 @@ in
DB_DATABASE = lib.mkDefault cfg.database.name;
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 (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.nginx = lib.mkIf (cfg.nginx != null) {
services.redis.servers.invoice-ninja = lib.mkIf cfg.redis.createLocally {
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;
clientMaxBodySize = "20m";
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
recommendedGzipSettings = true;
recommendedProxySettings = true;
recommendedOptimisation = 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;
clientMaxBodySize = cfg.maxUploadSize;
# 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
virtualHosts."${cfg.hostname}" = lib.mkMerge [
cfg.proxy.nginxConfig
{
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
root = lib.mkForce "${invoice-ninja}/public";
addSSL = lib.mkForce true;
enableACME = lib.mkForce true;
locations = {
"/".tryFiles = "$uri $uri/ /index.php?$query_string";
"/".extraConfig = ''
if (!-e $request_filename) {
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;
'';
};
locations."~ /\\.ht" = {
extraConfig = ''
deny all;
"~ \\.php$".extraConfig = "return 403;";
"= /index.php".extraConfig = ''
fastcgi_pass unix:${config.services.phpfpm.pools.invoice-ninja.socket};
'';
"~ /\\.ht".extraConfig = "deny all;";
};
extraConfig = ''
index index.php index.html index.htm;
index index.html index.htm 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;
package = lib.mkDefault pkgs.mariadb;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [{
ensureUsers = [
{
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.requires = [ "invoice-ninja-data-setup.service" ]
++ lib.optional cfg.database.createLocally "mysql.service";
systemd.services.phpfpm-invoice-ninja.requires = [
"invoice-ninja-data-setup.service"
] ++ lib.optional cfg.database.createLocally "mysql.service";
# Ensure chromium is available
systemd.services.phpfpm-invoice-ninja.path = extraPrograms;
systemd.timers.invoice-ninja-cron = {
description = "Invoice Ninja periodic tasks timer";
systemd.services.invoice-ninja-queue-worker = {
description = "Invoice Ninja periodic tasks";
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-cron = {
description = "Invoice Ninja periodic tasks";
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 schedule:run";
ExecStart = "${invoice-ninja-manage}/bin/invoice-ninja-manage queue:work ${
lib.strings.optionalString (cfg.settings.QUEUE_CONNECTION == "redis") "redis"
}";
User = user;
Group = group;
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/invoice-ninja") "invoice-ninja";
Restart = "always";
};
};
systemd.services.invoice-ninja-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" ];
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";
@ -362,7 +623,10 @@ in
Group = group;
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/invoice-ninja") "invoice-ninja";
StateDirectoryMode = "0750";
LoadCredential = "env-secrets:${cfg.secretFile}";
LoadCredential = [
"env-secrets:${cfg.secretFile}"
"admin-pass:${cfg.adminAccount.passwordFile}"
];
UMask = "077";
};
@ -376,37 +640,34 @@ 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 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.
# 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
# Perform the first migration
[[ ! -f ${cfg.dataDir}/.initial-migration ]] && invoice-ninja-manage migrate --force && touch ${cfg.dataDir}/.initial-migration
# 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
# Seed database with records
# Necessary for languages, currencies, countries, etc.
[[ ! -f ${cfg.dataDir}/.db-seeded ]] && invoice-ninja-manage db:seed --force && touch ${cfg.dataDir}/.db-seeded
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) && (${
if cfg.adminAccount.createAdmin then "true" else "false"
} == "true") ]] \
&& invoice-ninja-manage ninja:create-account --email=${cfg.adminAccount.email} --password=$(cat $CREDENTIALS_DIRECTORY/admin-pass) \
&& touch ${cfg.dataDir}/.admin-created
invoice-ninja-manage route:cache
invoice-ninja-manage view:cache
@ -414,11 +675,40 @@ 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/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";
};
});
};
}