Compare commits

...

7 commits

10 changed files with 206 additions and 43 deletions

View file

@ -164,6 +164,29 @@
"type": "github"
}
},
"invoiceplane-template": {
"inputs": {
"flake-parts": [
"flake-parts"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1718578450,
"narHash": "sha256-Nl6/5AzCg6yoU7OlJrOz8h4w2ENXZyj3AuCFXKxZ/W0=",
"ref": "refs/heads/main",
"rev": "79b1fdc7af77863a48dd58b22af57f4729660284",
"revCount": 29,
"type": "git",
"url": "https://git.pub.solar/teutat3s/invoiceplane-templates.git"
},
"original": {
"type": "git",
"url": "https://git.pub.solar/teutat3s/invoiceplane-templates.git"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
@ -282,6 +305,7 @@
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"home-manager": "home-manager",
"invoiceplane-template": "invoiceplane-template",
"nix-darwin": "nix-darwin",
"nixos-22-05": "nixos-22-05",
"nixos-flake": "nixos-flake",

View file

@ -34,6 +34,10 @@
nixos-hardware.url = "github:nixos/nixos-hardware";
invoiceplane-template.url = "git+https://git.pub.solar/teutat3s/invoiceplane-templates.git";
invoiceplane-template.inputs.nixpkgs.follows = "nixpkgs";
invoiceplane-template.inputs.flake-parts.follows = "flake-parts";
# PubSolarOS additions
triton-vmtools.url = "git+https://git.pub.solar/pub-solar/infra-vintage?ref=main&dir=vmtools";
triton-vmtools.inputs.nixpkgs.follows = "nixpkgs";

View file

@ -43,6 +43,7 @@
./fae
self.nixosModules.pub-solar
self.nixosModules.acme
self.nixosModules.invoiceplane
];
};

View file

@ -1,6 +1,7 @@
{...}: {
imports = [
./paperless.nix
./invoiceplane.nix
./fae.nix
];
}

View file

@ -0,0 +1,73 @@
{
flake,
config,
pkgs,
lib,
...
}: let
psCfg = config.pub-solar;
xdg = config.home-manager.users."${psCfg.user.name}".xdg;
backupDir = "/var/lib/invoiceplane/backup";
in {
security.acme.certs = {
"billing.faenix.eu" = {};
};
services.nginx.virtualHosts = {
"billing.faenix.eu" = {
forceSSL = true;
useACMEHost = "billing.faenix.eu";
};
};
services.invoiceplane = {
webserver = "nginx";
sites."billing.faenix.eu" = {
enable = true;
invoiceTemplates = [ flake.self.inputs.invoiceplane-template.packages.${pkgs.system}.invoiceplane-template ];
settings = {
IP_URL = "https://billing.faenix.eu";
DISABLE_SETUP = true;
SETUP_COMPLETED = true;
};
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[date.timezone]" = "Europe/Berlin";
"php_admin_value[error_log]" = "/var/lib/invoiceplane/billing.faenix.eu/logs/php-error.log";
"php_admin_flag[display_errors]" = "off";
"php_admin_flag[log_errors]" = "on";
"catch_workers_output" = "yes";
};
};
};
systemd.tmpfiles.rules = [
"d '${backupDir}' 0700 root root - -"
];
#services.restic.backups = {
# invoiceplane = {
# paths = [
# backupDir
# "/var/lib/invoiceplane/billing.faenix.eu"
# ];
# initialize = true;
# passwordFile = config.age.secrets."restic-password".path;
# # See https://www.hosting.de/blog/verschluesselte-backups-mit-rclone-und-restic-in-nextcloud/
# repository = "rclone:cloud.pub.solar:/backups/InvoicePlane";
# backupPrepareCommand = ''
# PW=$(cat ${config.age.secrets."invoiceplane-db-password".path})
# ${pkgs.docker-client}/bin/docker exec -t invoiceplane-db mariadb-dump --all-databases --password=$PW --user=invoiceplane > "${backupDir}/postgres.sql"
# '';
# rcloneConfigFile = config.age.secrets."rclone-pie.conf".path;
# };
#};
}

View file

@ -57,6 +57,9 @@ in {
virtualHosts = {
"paperless.faenix.eu" = {
#listenAddresses = [
# "192.168.13.35"
#];
forceSSL = true;
useACMEHost = "paperless.faenix.eu";
locations."/".proxyPass = "http://127.0.0.1:${builtins.toString config.services.paperless.port}";

View file

@ -1,2 +1,3 @@
# switch keyboard input language
bindsym $mod+tab exec swaymsg input "1118:1896:Microsoft_Microsoft___SiderWinderTM_X4_Keyboard_Consumer_Control" xkb_switch_layout next
#bindsym $mod+tab exec swaymsg input "1118:1896:Microsoft_Microsoft___SiderWinderTM_X4_Keyboard_Consumer_Control" xkb_switch_layout next
bindsym $mod+tab exec swaymsg input "7504:24868:Ultimate_Gadget_Laboratories_UHK_60_v2" xkb_switch_layout next

View file

@ -52,6 +52,8 @@ in {
'';
};
};
boot.kernelPackages = pkgs.linuxPackages_testing;
services.fstrim.enable = true;

View file

@ -15,7 +15,7 @@
#email = import ./email;
#gaming = import ./gaming;
graphical = import ./graphical;
#invoiceplane = import ./invoiceplane;
invoiceplane = import ./invoiceplane;
nix = import ./nix;
nextcloud = import ./nextcloud;
office = import ./office;

View file

@ -4,9 +4,16 @@ let
inherit (lib)
any
attrValues
boolToString
concatMapStringsSep
concatStrings
concatStringsSep
escapeShellArg
flatten
isBool
isInt
isList
isString
literalExpression
mapAttrs'
mapAttrsToList
@ -16,6 +23,7 @@ let
mkMerge
mkOption
nameValuePair
optionalString
types;
cfg = config.services.invoiceplane;
@ -31,7 +39,7 @@ let
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_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
@ -43,19 +51,28 @@ let
REMOVE_INDEXPHP=true
'';
extraConfig = hostName: cfg: pkgs.writeText "extraConfig.php" ''
${toString cfg.extraConfig}
'';
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;
postPhase = ''
postPatch = ''
# 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();";
--replace-fail "require('vendor/autoload.php');" "require('vendor/autoload.php'); \$dotenv = Dotenv\Dotenv::createImmutable(__DIR__, 'extraConfig.php'); \$dotenv->load();";
'';
installPhase = ''
@ -79,16 +96,16 @@ let
'';
};
siteOpts = { lib, name, ... }:
siteOpts = { name, ... }:
{
options = {
enable = mkEnableOption (lib.mdDoc "InvoicePlane web application");
enable = mkEnableOption "InvoicePlane web application";
stateDir = mkOption {
type = types.path;
default = "/var/lib/invoiceplane/${name}";
description = lib.mdDoc ''
description = ''
This directory is used for uploads of attachments and cache.
The directory passed here is automatically created and permissions
adjusted as required.
@ -99,32 +116,32 @@ let
host = mkOption {
type = types.str;
default = "localhost";
description = lib.mdDoc "Database host address.";
description = "Database host address.";
};
port = mkOption {
type = types.port;
default = 3306;
description = lib.mdDoc "Database host port.";
description = "Database host port.";
};
name = mkOption {
type = types.str;
default = "invoiceplane";
description = lib.mdDoc "Database name.";
description = "Database name.";
};
user = mkOption {
type = types.str;
default = "invoiceplane";
description = lib.mdDoc "Database user.";
description = "Database user.";
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/keys/invoiceplane-dbpassword";
description = lib.mdDoc ''
description = ''
A file containing the password corresponding to
{option}`database.user`.
'';
@ -133,14 +150,14 @@ let
createLocally = mkOption {
type = types.bool;
default = true;
description = lib.mdDoc "Create the database and database user locally.";
description = "Create the database and database user locally.";
};
};
invoiceTemplates = mkOption {
type = types.listOf types.path;
default = [];
description = lib.mdDoc ''
description = ''
List of path(s) to respective template(s) which are copied from the 'invoice_templates/pdf' directory.
::: {.note}
@ -179,45 +196,44 @@ let
"pm.max_spare_servers" = 4;
"pm.max_requests" = 500;
};
description = lib.mdDoc ''
description = ''
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
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 on supported values.
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 = lib.mdDoc ''
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 = lib.mdDoc "Cron key taken from the administration page.";
description = "Cron key taken from the administration page.";
};
};
};
@ -237,20 +253,20 @@ in
options.sites = mkOption {
type = types.attrsOf (types.submodule siteOpts);
default = {};
description = lib.mdDoc "Specification of one or more WordPress sites to serve";
description = "Specification of one or more WordPress sites to serve";
};
options.webserver = mkOption {
type = types.enum [ "caddy" ];
type = types.enum [ "caddy" "nginx" ];
default = "caddy";
description = lib.mdDoc ''
Which webserver to use for virtual host management. Currently only
caddy is supported.
example = "nginx";
description = ''
Which webserver to use for virtual host management.
'';
};
};
default = {};
description = lib.mdDoc "InvoicePlane configuration.";
description = "InvoicePlane configuration.";
};
};
@ -258,8 +274,8 @@ in
# implementation
config = mkIf (eachSite != {}) (mkMerge [{
assertions = flatten (mapAttrsToList (hostName: cfg:
[{ assertion = cfg.database.createLocally -> cfg.database.user == user;
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;
@ -373,5 +389,43 @@ in
};
})
(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;
};
})
]);
}