Merge branch 'main' of git.pub.solar:b12f/os
This commit is contained in:
commit
254886e511
|
@ -61,6 +61,7 @@
|
||||||
self.nixosModules.yule
|
self.nixosModules.yule
|
||||||
self.nixosModules.docker
|
self.nixosModules.docker
|
||||||
self.nixosModules.wireguard-client
|
self.nixosModules.wireguard-client
|
||||||
|
self.nixosModules.invoiceplane
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ in {
|
||||||
|
|
||||||
boot.kernelParams = [
|
boot.kernelParams = [
|
||||||
"boot.shell_on_fail=1"
|
"boot.shell_on_fail=1"
|
||||||
"ip=192.168.178.2::192.168.178.1:255.255.255.0:pie.b12f.io::off"
|
"ip=192.168.178.2::192.168.178.1:255.255.255.255:pie.b12f.io::off"
|
||||||
];
|
];
|
||||||
|
|
||||||
boot.initrd.network.enable = true;
|
boot.initrd.network.enable = true;
|
||||||
|
|
|
@ -27,10 +27,23 @@ in {
|
||||||
user = "invoiceplane";
|
user = "invoiceplane";
|
||||||
name = "invoiceplane";
|
name = "invoiceplane";
|
||||||
passwordFile = config.age.secrets."invoiceplane-db-password.age".path;
|
passwordFile = config.age.secrets."invoiceplane-db-password.age".path;
|
||||||
host = "localhost";
|
host = "127.0.0.1";
|
||||||
port = 3306;
|
port = 3306;
|
||||||
createLocally = false;
|
createLocally = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
poolConfig = {
|
||||||
|
"pm" = "dynamic";
|
||||||
|
"pm.max_children" = 32;
|
||||||
|
"pm.max_requests" = 500;
|
||||||
|
"pm.max_spare_servers" = 4;
|
||||||
|
"pm.min_spare_servers" = 2;
|
||||||
|
"pm.start_servers" = 2;
|
||||||
|
"php_admin_value[error_log]" = "/var/lib/invoiceplane/invoicing.b12f.io/logs/php-error.log";
|
||||||
|
"php_admin_flag[display_errors]" = "off";
|
||||||
|
"php_admin_flag[log_errors]" = "on";
|
||||||
|
"catch_workers_output" = "yes";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
virtualisation = {
|
virtualisation = {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
}: {
|
}: {
|
||||||
flake = {
|
flake = {
|
||||||
nixosModules = rec {
|
nixosModules = rec {
|
||||||
|
adb = import ./adb;
|
||||||
arduino = import ./arduino;
|
arduino = import ./arduino;
|
||||||
audio = import ./audio;
|
audio = import ./audio;
|
||||||
bluetooth = import ./bluetooth;
|
bluetooth = import ./bluetooth;
|
||||||
|
@ -16,7 +17,7 @@
|
||||||
email = import ./email;
|
email = import ./email;
|
||||||
gaming = import ./gaming;
|
gaming = import ./gaming;
|
||||||
graphical = import ./graphical;
|
graphical = import ./graphical;
|
||||||
adb = import ./adb;
|
invoiceplane = import ./invoiceplane;
|
||||||
nix = import ./nix;
|
nix = import ./nix;
|
||||||
nextcloud = import ./nextcloud;
|
nextcloud = import ./nextcloud;
|
||||||
office = import ./office;
|
office = import ./office;
|
||||||
|
|
362
modules/invoiceplane/default.nix
Normal file
362
modules/invoiceplane/default.nix
Normal file
|
@ -0,0 +1,362 @@
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
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=${if cfg.database.passwordFile == null then "" else "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
|
||||||
|
'';
|
||||||
|
|
||||||
|
extraConfig = hostName: cfg: pkgs.writeText "extraConfig.php" ''
|
||||||
|
${toString cfg.extraConfig}
|
||||||
|
'';
|
||||||
|
|
||||||
|
pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
|
||||||
|
pname = "invoiceplane-${hostName}";
|
||||||
|
version = src.version;
|
||||||
|
src = pkgs.invoiceplane;
|
||||||
|
|
||||||
|
postPhase = ''
|
||||||
|
# Patch index.php file to load additional config file
|
||||||
|
substituteInPlace index.php \
|
||||||
|
--replace "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 = { lib, name, ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
|
||||||
|
enable = mkEnableOption (lib.mdDoc "InvoicePlane web application");
|
||||||
|
|
||||||
|
stateDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/invoiceplane/${name}";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
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 = lib.mdDoc "Database host address.";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 3306;
|
||||||
|
description = lib.mdDoc "Database host port.";
|
||||||
|
};
|
||||||
|
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "invoiceplane";
|
||||||
|
description = lib.mdDoc "Database name.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "invoiceplane";
|
||||||
|
description = lib.mdDoc "Database user.";
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
default = null;
|
||||||
|
example = "/run/keys/invoiceplane-dbpassword";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
A file containing the password corresponding to
|
||||||
|
{option}`database.user`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
createLocally = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = lib.mdDoc "Create the database and database user locally.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
invoiceTemplates = mkOption {
|
||||||
|
type = types.listOf types.path;
|
||||||
|
default = [];
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
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 = lib.mdDoc ''
|
||||||
|
Options for the InvoicePlane PHP pool. See the documentation on `php-fpm.conf`
|
||||||
|
for details on configuration directives.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraConfig = mkOption {
|
||||||
|
type = types.nullOr types.lines;
|
||||||
|
default = null;
|
||||||
|
example = ''
|
||||||
|
SETUP_COMPLETED=true
|
||||||
|
DISABLE_SETUP=true
|
||||||
|
IP_URL=https://invoice.example.com
|
||||||
|
'';
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
InvoicePlane configuration. Refer to
|
||||||
|
<https://github.com/InvoicePlane/InvoicePlane/blob/master/ipconfig.php.example>
|
||||||
|
for details on supported values.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
cron = {
|
||||||
|
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
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 = lib.mdDoc "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 = lib.mdDoc "Specification of one or more WordPress sites to serve";
|
||||||
|
};
|
||||||
|
|
||||||
|
options.webserver = mkOption {
|
||||||
|
type = types.enum [ "caddy" ];
|
||||||
|
default = "caddy";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Which webserver to use for virtual host management. Currently only
|
||||||
|
caddy is supported.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
default = {};
|
||||||
|
description = lib.mdDoc "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;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
]);
|
||||||
|
}
|
Loading…
Reference in a new issue