482 lines
14 KiB
Nix
482 lines
14 KiB
Nix
{
|
|
config,
|
|
pkgs,
|
|
lib,
|
|
...
|
|
}:
|
|
|
|
let
|
|
inherit (lib)
|
|
any
|
|
attrValues
|
|
boolToString
|
|
concatMapStringsSep
|
|
concatStrings
|
|
concatStringsSep
|
|
escapeShellArg
|
|
flatten
|
|
isBool
|
|
isInt
|
|
isList
|
|
isString
|
|
literalExpression
|
|
mapAttrs'
|
|
mapAttrsToList
|
|
mkDefault
|
|
mkEnableOption
|
|
mkIf
|
|
mkMerge
|
|
mkOption
|
|
nameValuePair
|
|
optionalString
|
|
types
|
|
;
|
|
|
|
cfg = config.services.invoiceplane;
|
|
eachSite = cfg.sites;
|
|
user = "invoiceplane";
|
|
webserver = config.services.${cfg.webserver};
|
|
|
|
invoiceplane-config =
|
|
hostName: cfg:
|
|
pkgs.writeText "ipconfig.php" ''
|
|
IP_URL=http://${hostName}
|
|
ENABLE_DEBUG=false
|
|
DISABLE_SETUP=false
|
|
REMOVE_INDEXPHP=false
|
|
DB_HOSTNAME=${cfg.database.host}
|
|
DB_USERNAME=${cfg.database.user}
|
|
# NOTE: file_get_contents adds newline at the end of returned string
|
|
DB_PASSWORD=${
|
|
optionalString (
|
|
cfg.database.passwordFile != null
|
|
) "trim(file_get_contents('${cfg.database.passwordFile}'), \"\\r\\n\")"
|
|
}
|
|
DB_DATABASE=${cfg.database.name}
|
|
DB_PORT=${toString cfg.database.port}
|
|
SESS_EXPIRATION=864000
|
|
ENABLE_INVOICE_DELETION=false
|
|
DISABLE_READ_ONLY=false
|
|
ENCRYPTION_KEY=
|
|
ENCRYPTION_CIPHER=AES-256
|
|
SETUP_COMPLETED=false
|
|
REMOVE_INDEXPHP=true
|
|
'';
|
|
|
|
mkPhpValue =
|
|
v:
|
|
if isString v then
|
|
escapeShellArg v
|
|
# NOTE: If any value contains a , (comma) this will not get escaped
|
|
else if isList v && any lib.strings.isCoercibleToString v then
|
|
escapeShellArg (concatMapStringsSep "," toString v)
|
|
else if isInt v then
|
|
toString v
|
|
else if isBool v then
|
|
boolToString v
|
|
else
|
|
abort "The Invoiceplane config value ${lib.generators.toPretty { } v} can not be encoded.";
|
|
|
|
extraConfig =
|
|
hostName: cfg:
|
|
let
|
|
settings = mapAttrsToList (k: v: "${k}=${mkPhpValue v}") cfg.settings;
|
|
in
|
|
pkgs.writeText "extraConfig.php" (concatStringsSep "\n" settings);
|
|
|
|
pkg =
|
|
hostName: cfg:
|
|
pkgs.stdenv.mkDerivation rec {
|
|
pname = "invoiceplane-${hostName}";
|
|
version = src.version;
|
|
src = pkgs.invoiceplane;
|
|
|
|
postPatch = ''
|
|
# Patch index.php file to load additional config file
|
|
substituteInPlace index.php \
|
|
--replace-fail "require('vendor/autoload.php');" "require('vendor/autoload.php'); \$dotenv = Dotenv\Dotenv::createImmutable(__DIR__, 'extraConfig.php'); \$dotenv->load();";
|
|
'';
|
|
|
|
installPhase = ''
|
|
mkdir -p $out
|
|
cp -r * $out/
|
|
|
|
# symlink uploads and log directories
|
|
rm -r $out/uploads $out/application/logs $out/vendor/mpdf/mpdf/tmp
|
|
ln -sf ${cfg.stateDir}/uploads $out/
|
|
ln -sf ${cfg.stateDir}/logs $out/application/
|
|
ln -sf ${cfg.stateDir}/tmp $out/vendor/mpdf/mpdf/
|
|
|
|
# symlink the InvoicePlane config
|
|
ln -s ${cfg.stateDir}/ipconfig.php $out/ipconfig.php
|
|
|
|
# symlink the extraConfig file
|
|
ln -s ${extraConfig hostName cfg} $out/extraConfig.php
|
|
|
|
# symlink additional templates
|
|
${concatMapStringsSep "\n" (
|
|
template: "cp -r ${template}/. $out/application/views/invoice_templates/pdf/"
|
|
) cfg.invoiceTemplates}
|
|
'';
|
|
};
|
|
|
|
siteOpts =
|
|
{ name, ... }:
|
|
{
|
|
options = {
|
|
|
|
enable = mkEnableOption "InvoicePlane web application";
|
|
|
|
stateDir = mkOption {
|
|
type = types.path;
|
|
default = "/var/lib/invoiceplane/${name}";
|
|
description = ''
|
|
This directory is used for uploads of attachments and cache.
|
|
The directory passed here is automatically created and permissions
|
|
adjusted as required.
|
|
'';
|
|
};
|
|
|
|
database = {
|
|
host = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
description = "Database host address.";
|
|
};
|
|
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 3306;
|
|
description = "Database host port.";
|
|
};
|
|
|
|
name = mkOption {
|
|
type = types.str;
|
|
default = "invoiceplane";
|
|
description = "Database name.";
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "invoiceplane";
|
|
description = "Database user.";
|
|
};
|
|
|
|
passwordFile = mkOption {
|
|
type = types.nullOr types.path;
|
|
default = null;
|
|
example = "/run/keys/invoiceplane-dbpassword";
|
|
description = ''
|
|
A file containing the password corresponding to
|
|
{option}`database.user`.
|
|
'';
|
|
};
|
|
|
|
createLocally = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Create the database and database user locally.";
|
|
};
|
|
};
|
|
|
|
invoiceTemplates = mkOption {
|
|
type = types.listOf types.path;
|
|
default = [ ];
|
|
description = ''
|
|
List of path(s) to respective template(s) which are copied from the 'invoice_templates/pdf' directory.
|
|
|
|
::: {.note}
|
|
These templates need to be packaged before use, see example.
|
|
:::
|
|
'';
|
|
example = literalExpression ''
|
|
let
|
|
# Let's package an example template
|
|
template-vtdirektmarketing = pkgs.stdenv.mkDerivation {
|
|
name = "vtdirektmarketing";
|
|
# Download the template from a public repository
|
|
src = pkgs.fetchgit {
|
|
url = "https://git.project-insanity.org/onny/invoiceplane-vtdirektmarketing.git";
|
|
sha256 = "1hh0q7wzsh8v8x03i82p6qrgbxr4v5fb05xylyrpp975l8axyg2z";
|
|
};
|
|
sourceRoot = ".";
|
|
# Installing simply means copying template php file to the output directory
|
|
installPhase = ""
|
|
mkdir -p $out
|
|
cp invoiceplane-vtdirektmarketing/vtdirektmarketing.php $out/
|
|
"";
|
|
};
|
|
# And then pass this package to the template list like this:
|
|
in [ template-vtdirektmarketing ]
|
|
'';
|
|
};
|
|
|
|
poolConfig = mkOption {
|
|
type =
|
|
with types;
|
|
attrsOf (oneOf [
|
|
str
|
|
int
|
|
bool
|
|
]);
|
|
default = {
|
|
"pm" = "dynamic";
|
|
"pm.max_children" = 32;
|
|
"pm.start_servers" = 2;
|
|
"pm.min_spare_servers" = 2;
|
|
"pm.max_spare_servers" = 4;
|
|
"pm.max_requests" = 500;
|
|
};
|
|
description = ''
|
|
Options for the InvoicePlane PHP pool. See the documentation on `php-fpm.conf`
|
|
for details on configuration directives.
|
|
'';
|
|
};
|
|
|
|
settings = mkOption {
|
|
type = types.attrsOf types.anything;
|
|
default = { };
|
|
description = ''
|
|
Structural InvoicePlane configuration. Refer to
|
|
<https://github.com/InvoicePlane/InvoicePlane/blob/master/ipconfig.php.example>
|
|
for details and supported values.
|
|
'';
|
|
example = literalExpression ''
|
|
{
|
|
SETUP_COMPLETED = true;
|
|
DISABLE_SETUP = true;
|
|
IP_URL = "https://invoice.example.com";
|
|
}
|
|
'';
|
|
};
|
|
|
|
cron = {
|
|
enable = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = ''
|
|
Enable cron service which periodically runs Invoiceplane tasks.
|
|
Requires key taken from the administration page. Refer to
|
|
<https://wiki.invoiceplane.com/en/1.0/modules/recurring-invoices>
|
|
on how to configure it.
|
|
'';
|
|
};
|
|
key = mkOption {
|
|
type = types.str;
|
|
description = "Cron key taken from the administration page.";
|
|
};
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
in
|
|
{
|
|
disabledModules = [ "services/web-apps/invoiceplane.nix" ];
|
|
|
|
# interface
|
|
options = {
|
|
services.invoiceplane = mkOption {
|
|
type = types.submodule {
|
|
|
|
options.sites = mkOption {
|
|
type = types.attrsOf (types.submodule siteOpts);
|
|
default = { };
|
|
description = "Specification of one or more WordPress sites to serve";
|
|
};
|
|
|
|
options.webserver = mkOption {
|
|
type = types.enum [
|
|
"caddy"
|
|
"nginx"
|
|
];
|
|
default = "caddy";
|
|
example = "nginx";
|
|
description = ''
|
|
Which webserver to use for virtual host management.
|
|
'';
|
|
};
|
|
};
|
|
default = { };
|
|
description = "InvoicePlane configuration.";
|
|
};
|
|
|
|
};
|
|
|
|
# implementation
|
|
config = mkIf (eachSite != { }) (mkMerge [
|
|
{
|
|
|
|
assertions = flatten (
|
|
mapAttrsToList (hostName: cfg: [
|
|
{
|
|
assertion = cfg.database.createLocally -> cfg.database.user == user;
|
|
message = ''services.invoiceplane.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned'';
|
|
}
|
|
{
|
|
assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
|
|
message = ''services.invoiceplane.sites."${hostName}".database.passwordFile cannot be specified if services.invoiceplane.sites."${hostName}".database.createLocally is set to true.'';
|
|
}
|
|
{
|
|
assertion = cfg.cron.enable -> cfg.cron.key != null;
|
|
message = ''services.invoiceplane.sites."${hostName}".cron.key must be set in order to use cron service.'';
|
|
}
|
|
]) eachSite
|
|
);
|
|
|
|
services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
|
|
enable = true;
|
|
package = mkDefault pkgs.mariadb;
|
|
ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite;
|
|
ensureUsers = mapAttrsToList (hostName: cfg: {
|
|
name = cfg.database.user;
|
|
ensurePermissions = {
|
|
"${cfg.database.name}.*" = "ALL PRIVILEGES";
|
|
};
|
|
}) eachSite;
|
|
};
|
|
|
|
services.phpfpm = {
|
|
phpPackage = pkgs.php81;
|
|
pools = mapAttrs' (
|
|
hostName: cfg:
|
|
(nameValuePair "invoiceplane-${hostName}" {
|
|
inherit user;
|
|
group = webserver.group;
|
|
settings = {
|
|
"listen.owner" = webserver.user;
|
|
"listen.group" = webserver.group;
|
|
} // cfg.poolConfig;
|
|
})
|
|
) eachSite;
|
|
};
|
|
|
|
}
|
|
|
|
{
|
|
|
|
systemd.tmpfiles.rules = flatten (
|
|
mapAttrsToList (hostName: cfg: [
|
|
"d ${cfg.stateDir} 0750 ${user} ${webserver.group} - -"
|
|
"f ${cfg.stateDir}/ipconfig.php 0750 ${user} ${webserver.group} - -"
|
|
"d ${cfg.stateDir}/logs 0750 ${user} ${webserver.group} - -"
|
|
"d ${cfg.stateDir}/uploads 0750 ${user} ${webserver.group} - -"
|
|
"d ${cfg.stateDir}/uploads/archive 0750 ${user} ${webserver.group} - -"
|
|
"d ${cfg.stateDir}/uploads/customer_files 0750 ${user} ${webserver.group} - -"
|
|
"d ${cfg.stateDir}/uploads/temp 0750 ${user} ${webserver.group} - -"
|
|
"d ${cfg.stateDir}/uploads/temp/mpdf 0750 ${user} ${webserver.group} - -"
|
|
"d ${cfg.stateDir}/tmp 0750 ${user} ${webserver.group} - -"
|
|
]) eachSite
|
|
);
|
|
|
|
systemd.services.invoiceplane-config = {
|
|
serviceConfig.Type = "oneshot";
|
|
script = concatStrings (
|
|
mapAttrsToList (hostName: cfg: ''
|
|
mkdir -p ${cfg.stateDir}/logs \
|
|
${cfg.stateDir}/uploads
|
|
if ! grep -q IP_URL "${cfg.stateDir}/ipconfig.php"; then
|
|
cp "${invoiceplane-config hostName cfg}" "${cfg.stateDir}/ipconfig.php"
|
|
fi
|
|
'') eachSite
|
|
);
|
|
wantedBy = [ "multi-user.target" ];
|
|
};
|
|
|
|
users.users.${user} = {
|
|
group = webserver.group;
|
|
isSystemUser = true;
|
|
};
|
|
|
|
}
|
|
{
|
|
|
|
# Cron service implementation
|
|
|
|
systemd.timers = mapAttrs' (
|
|
hostName: cfg:
|
|
(nameValuePair "invoiceplane-cron-${hostName}" (
|
|
mkIf cfg.cron.enable {
|
|
wantedBy = [ "timers.target" ];
|
|
timerConfig = {
|
|
OnBootSec = "5m";
|
|
OnUnitActiveSec = "5m";
|
|
Unit = "invoiceplane-cron-${hostName}.service";
|
|
};
|
|
}
|
|
))
|
|
) eachSite;
|
|
|
|
systemd.services = mapAttrs' (
|
|
hostName: cfg:
|
|
(nameValuePair "invoiceplane-cron-${hostName}" (
|
|
mkIf cfg.cron.enable {
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = user;
|
|
ExecStart = "${pkgs.curl}/bin/curl --header 'Host: ${hostName}' http://localhost/invoices/cron/recur/${cfg.cron.key}";
|
|
};
|
|
}
|
|
))
|
|
) eachSite;
|
|
|
|
}
|
|
|
|
(mkIf (cfg.webserver == "caddy") {
|
|
services.caddy = {
|
|
enable = true;
|
|
virtualHosts = mapAttrs' (
|
|
hostName: cfg:
|
|
(nameValuePair "http://${hostName}" {
|
|
extraConfig = ''
|
|
root * ${pkg hostName cfg}
|
|
file_server
|
|
php_fastcgi unix/${config.services.phpfpm.pools."invoiceplane-${hostName}".socket}
|
|
'';
|
|
})
|
|
) eachSite;
|
|
};
|
|
})
|
|
|
|
(mkIf (cfg.webserver == "nginx") {
|
|
services.nginx = {
|
|
enable = true;
|
|
virtualHosts = mapAttrs' (
|
|
hostName: cfg:
|
|
(nameValuePair hostName {
|
|
root = pkg hostName cfg;
|
|
extraConfig = ''
|
|
index index.php index.html index.htm;
|
|
|
|
if (!-e $request_filename){
|
|
rewrite ^(.*)$ /index.php break;
|
|
}
|
|
'';
|
|
|
|
locations = {
|
|
"/setup".extraConfig =
|
|
let
|
|
scheme = if config.services.nginx.virtualHosts.${hostName}.forceSSL then "https" else "http";
|
|
in
|
|
''
|
|
rewrite ^(.*)$ ${scheme}://${hostName}/ redirect;
|
|
'';
|
|
|
|
"~ .php$" = {
|
|
extraConfig = ''
|
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
fastcgi_pass unix:${config.services.phpfpm.pools."invoiceplane-${hostName}".socket};
|
|
include ${config.services.nginx.package}/conf/fastcgi_params;
|
|
include ${config.services.nginx.package}/conf/fastcgi.conf;
|
|
'';
|
|
};
|
|
};
|
|
})
|
|
) eachSite;
|
|
};
|
|
})
|
|
|
|
]);
|
|
}
|