Compare commits

...

7 commits

10 changed files with 206 additions and 43 deletions

View file

@ -164,6 +164,29 @@
"type": "github" "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": { "nix-darwin": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@ -282,6 +305,7 @@
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"home-manager": "home-manager", "home-manager": "home-manager",
"invoiceplane-template": "invoiceplane-template",
"nix-darwin": "nix-darwin", "nix-darwin": "nix-darwin",
"nixos-22-05": "nixos-22-05", "nixos-22-05": "nixos-22-05",
"nixos-flake": "nixos-flake", "nixos-flake": "nixos-flake",

View file

@ -34,6 +34,10 @@
nixos-hardware.url = "github:nixos/nixos-hardware"; 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 # PubSolarOS additions
triton-vmtools.url = "git+https://git.pub.solar/pub-solar/infra-vintage?ref=main&dir=vmtools"; triton-vmtools.url = "git+https://git.pub.solar/pub-solar/infra-vintage?ref=main&dir=vmtools";
triton-vmtools.inputs.nixpkgs.follows = "nixpkgs"; triton-vmtools.inputs.nixpkgs.follows = "nixpkgs";

View file

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

View file

@ -1,6 +1,7 @@
{...}: { {...}: {
imports = [ imports = [
./paperless.nix ./paperless.nix
./invoiceplane.nix
./fae.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 = { virtualHosts = {
"paperless.faenix.eu" = { "paperless.faenix.eu" = {
#listenAddresses = [
# "192.168.13.35"
#];
forceSSL = true; forceSSL = true;
useACMEHost = "paperless.faenix.eu"; useACMEHost = "paperless.faenix.eu";
locations."/".proxyPass = "http://127.0.0.1:${builtins.toString config.services.paperless.port}"; locations."/".proxyPass = "http://127.0.0.1:${builtins.toString config.services.paperless.port}";

View file

@ -1,2 +1,3 @@
# switch keyboard input language # 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; services.fstrim.enable = true;

View file

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

View file

@ -4,9 +4,16 @@ let
inherit (lib) inherit (lib)
any any
attrValues attrValues
boolToString
concatMapStringsSep concatMapStringsSep
concatStrings concatStrings
concatStringsSep
escapeShellArg
flatten flatten
isBool
isInt
isList
isString
literalExpression literalExpression
mapAttrs' mapAttrs'
mapAttrsToList mapAttrsToList
@ -16,6 +23,7 @@ let
mkMerge mkMerge
mkOption mkOption
nameValuePair nameValuePair
optionalString
types; types;
cfg = config.services.invoiceplane; cfg = config.services.invoiceplane;
@ -31,7 +39,7 @@ let
DB_HOSTNAME=${cfg.database.host} DB_HOSTNAME=${cfg.database.host}
DB_USERNAME=${cfg.database.user} DB_USERNAME=${cfg.database.user}
# NOTE: file_get_contents adds newline at the end of returned string # 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_DATABASE=${cfg.database.name}
DB_PORT=${toString cfg.database.port} DB_PORT=${toString cfg.database.port}
SESS_EXPIRATION=864000 SESS_EXPIRATION=864000
@ -43,19 +51,28 @@ let
REMOVE_INDEXPHP=true REMOVE_INDEXPHP=true
''; '';
extraConfig = hostName: cfg: pkgs.writeText "extraConfig.php" '' mkPhpValue = v:
${toString cfg.extraConfig} 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 { pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
pname = "invoiceplane-${hostName}"; pname = "invoiceplane-${hostName}";
version = src.version; version = src.version;
src = pkgs.invoiceplane; src = pkgs.invoiceplane;
postPhase = '' postPatch = ''
# Patch index.php file to load additional config file # Patch index.php file to load additional config file
substituteInPlace index.php \ 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 = '' installPhase = ''
@ -79,16 +96,16 @@ let
''; '';
}; };
siteOpts = { lib, name, ... }: siteOpts = { name, ... }:
{ {
options = { options = {
enable = mkEnableOption (lib.mdDoc "InvoicePlane web application"); enable = mkEnableOption "InvoicePlane web application";
stateDir = mkOption { stateDir = mkOption {
type = types.path; type = types.path;
default = "/var/lib/invoiceplane/${name}"; default = "/var/lib/invoiceplane/${name}";
description = lib.mdDoc '' description = ''
This directory is used for uploads of attachments and cache. This directory is used for uploads of attachments and cache.
The directory passed here is automatically created and permissions The directory passed here is automatically created and permissions
adjusted as required. adjusted as required.
@ -99,32 +116,32 @@ let
host = mkOption { host = mkOption {
type = types.str; type = types.str;
default = "localhost"; default = "localhost";
description = lib.mdDoc "Database host address."; description = "Database host address.";
}; };
port = mkOption { port = mkOption {
type = types.port; type = types.port;
default = 3306; default = 3306;
description = lib.mdDoc "Database host port."; description = "Database host port.";
}; };
name = mkOption { name = mkOption {
type = types.str; type = types.str;
default = "invoiceplane"; default = "invoiceplane";
description = lib.mdDoc "Database name."; description = "Database name.";
}; };
user = mkOption { user = mkOption {
type = types.str; type = types.str;
default = "invoiceplane"; default = "invoiceplane";
description = lib.mdDoc "Database user."; description = "Database user.";
}; };
passwordFile = mkOption { passwordFile = mkOption {
type = types.nullOr types.path; type = types.nullOr types.path;
default = null; default = null;
example = "/run/keys/invoiceplane-dbpassword"; example = "/run/keys/invoiceplane-dbpassword";
description = lib.mdDoc '' description = ''
A file containing the password corresponding to A file containing the password corresponding to
{option}`database.user`. {option}`database.user`.
''; '';
@ -133,14 +150,14 @@ let
createLocally = mkOption { createLocally = mkOption {
type = types.bool; type = types.bool;
default = true; default = true;
description = lib.mdDoc "Create the database and database user locally."; description = "Create the database and database user locally.";
}; };
}; };
invoiceTemplates = mkOption { invoiceTemplates = mkOption {
type = types.listOf types.path; type = types.listOf types.path;
default = []; default = [];
description = lib.mdDoc '' description = ''
List of path(s) to respective template(s) which are copied from the 'invoice_templates/pdf' directory. List of path(s) to respective template(s) which are copied from the 'invoice_templates/pdf' directory.
::: {.note} ::: {.note}
@ -179,45 +196,44 @@ let
"pm.max_spare_servers" = 4; "pm.max_spare_servers" = 4;
"pm.max_requests" = 500; "pm.max_requests" = 500;
}; };
description = lib.mdDoc '' description = ''
Options for the InvoicePlane PHP pool. See the documentation on `php-fpm.conf` Options for the InvoicePlane PHP pool. See the documentation on `php-fpm.conf`
for details on configuration directives. for details on configuration directives.
''; '';
}; };
extraConfig = mkOption { settings = mkOption {
type = types.nullOr types.lines; type = types.attrsOf types.anything;
default = null; default = {};
example = '' description = ''
SETUP_COMPLETED=true Structural InvoicePlane configuration. Refer to
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> <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 = { cron = {
enable = mkOption { enable = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = lib.mdDoc '' description = ''
Enable cron service which periodically runs Invoiceplane tasks. Enable cron service which periodically runs Invoiceplane tasks.
Requires key taken from the administration page. Refer to Requires key taken from the administration page. Refer to
<https://wiki.invoiceplane.com/en/1.0/modules/recurring-invoices> <https://wiki.invoiceplane.com/en/1.0/modules/recurring-invoices>
on how to configure it. on how to configure it.
''; '';
}; };
key = mkOption { key = mkOption {
type = types.str; 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 { options.sites = mkOption {
type = types.attrsOf (types.submodule siteOpts); type = types.attrsOf (types.submodule siteOpts);
default = {}; 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 { options.webserver = mkOption {
type = types.enum [ "caddy" ]; type = types.enum [ "caddy" "nginx" ];
default = "caddy"; default = "caddy";
description = lib.mdDoc '' example = "nginx";
Which webserver to use for virtual host management. Currently only description = ''
caddy is supported. Which webserver to use for virtual host management.
''; '';
}; };
}; };
default = {}; default = {};
description = lib.mdDoc "InvoicePlane configuration."; description = "InvoicePlane configuration.";
}; };
}; };
@ -258,8 +274,8 @@ in
# implementation # implementation
config = mkIf (eachSite != {}) (mkMerge [{ config = mkIf (eachSite != {}) (mkMerge [{
assertions = flatten (mapAttrsToList (hostName: cfg: assertions = flatten (mapAttrsToList (hostName: cfg: [
[{ assertion = cfg.database.createLocally -> cfg.database.user == user; { 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''; 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; { 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;
};
})
]); ]);
} }