From e1ba5571e4b62191b5a4b684c78cd05555c9183b Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 14 Jan 2026 00:51:27 +0100 Subject: [PATCH] nixos/strichliste: init --- .../manual/release-notes/rl-2605.section.md | 2 + nixos/modules/module-list.nix | 1 + .../modules/services/web-apps/strichliste.nix | 521 ++++++++++++++++++ 3 files changed, 524 insertions(+) create mode 100644 nixos/modules/services/web-apps/strichliste.nix diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 89c18eddcc9f..a8ec7cae2eff 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -102,6 +102,8 @@ - [Tinyauth](https://tinyauth.app/), a simple authentication middleware for web apps, with OAuth and LDAP support. Available as [services.tinyauth](#opt-services.tinyauth.enable). +- [Strichliste](https://www.strichliste.org), a digital self-service tallysheet used in hackerspaces, clubs and offices. Available as [services.strichliste](#opt-services.strichliste.enable). + - [Dawarich](https://dawarich.app/), a self-hostable location history tracker. Available as [services.dawarich](#opt-services.dawarich.enable). - [Howdy](https://github.com/boltgolt/howdy), a Windows Hello™ style facial authentication program for Linux. diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index f89194a4ef39..f72dcccf3e87 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1779,6 +1779,7 @@ ./services/web-apps/stash.nix ./services/web-apps/stirling-pdf.nix ./services/web-apps/strfry.nix + ./services/web-apps/strichliste.nix ./services/web-apps/suwayomi-server.nix ./services/web-apps/szurubooru.nix ./services/web-apps/tabbyapi.nix diff --git a/nixos/modules/services/web-apps/strichliste.nix b/nixos/modules/services/web-apps/strichliste.nix new file mode 100644 index 000000000000..4f7db7cd31da --- /dev/null +++ b/nixos/modules/services/web-apps/strichliste.nix @@ -0,0 +1,521 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + mkEnableOption + mkForce + mkIf + mkMerge + mkOption + mkPackageOption + types + ; + + cfg = config.services.strichliste; + + format = pkgs.formats.yaml { }; + settingsFile = format.generate "strichliste.yaml" { + parameters.strichliste = cfg.settings; + }; + + unitDependencies = + lib.optionals ( + lib.hasInfix "pgpsql" cfg.environment.DATABASE_URL + || lib.hasInfix "postgres" cfg.environment.DATABASE_URL + ) [ "postgresql.service" ] + ++ lib.optionals (lib.hasInfix "mysql" cfg.environment.DATABASE_URL) [ "mysql.service" ]; +in +{ + meta.buildDocsInSandbox = false; + + options.services.strichliste = { + enable = mkEnableOption "strichliste, a web based tally sheet."; + + packages = { + backend = mkPackageOption pkgs "strichliste" { }; + frontend = mkOption { + type = types.package; + default = pkgs.strichliste.frontend; + description = '' + The strichliste-frontend package to use. + ''; + }; + }; + + settings = mkOption { + type = types.submodule { + freeformType = format.type; + options = { + common = { + idleTimeout = mkOption { + type = types.int; + default = 30000; + description = '' + Time until the app returns to the start page. + ''; + }; + }; + + user = { + stalePeriod = mkOption { + type = types.str; + default = "10 day"; + example = "1 week"; + description = '' + Duration after which users are listed as inactive. + + The format used is documented in . + + ::: {.tip} + This helps unclutter the user listing by prioritizing active users. + ::: + ''; + }; + }; + + i18n = { + timezone = mkOption { + type = types.str; + default = config.time.timeZone; + defaultText = lib.literalExpression "config.time.timeZone"; + example = "Europe/Berlin"; + description = '' + Timezone used throughout the app, e.g. in the transaction log. + ''; + }; + + language = mkOption { + type = types.str; + default = "en"; + example = "de"; + description = '' + Language used throughout the app. + ''; + }; + + currency = { + name = mkOption { + type = types.str; + example = "Euro"; + description = '' + Name of the currency. + ''; + }; + + symbol = mkOption { + type = types.str; + example = "€"; + description = '' + Symbol for the currency. + ''; + }; + + alpha3 = mkOption { + type = types.str; + example = "EUR"; + description = '' + [ISO 4217] alpha code representing the currency. + + [ISO 4217]: https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes + ''; + }; + }; + }; + + account = { + lower = mkOption { + type = types.int; + default = -200000; + example = 0; + description = '' + The credit limit for user accounts. + ''; + }; + + upper = mkOption { + type = types.ints.positive; + default = 200000; + description = '' + The maximum balance on a user account. + ''; + }; + }; + + payment = { + boundary = { + lower = mkOption { + type = types.int; + default = -2000; + example = 0; + description = '' + The lowest amount that can be used for payments. + ''; + }; + + upper = mkOption { + type = types.ints.positive; + default = 15000; + description = '' + The highest amount that can be used for payment. + ''; + }; + }; + + deposit = { + enabled = mkOption { + type = types.bool; + default = true; + description = '' + Whether to allow money deposits. + ''; + }; + + custom = mkOption { + type = types.bool; + default = true; + description = '' + Whether to allow custom amounts for deposits. + ''; + }; + + steps = mkOption { + type = types.listOf ( + types.oneOf [ + types.int + types.float + ] + ); + example = [ + 0.5 + 1 + 2 + 5 + 10 + 20 + ]; + description = '' + List of selectable deposit amounts. + + This should match your most common coins and banknotes. + ''; + }; + }; + + dispense = { + enabled = mkOption { + type = types.bool; + default = true; + description = '' + Whether to allow spending money. + ''; + }; + + custom = mkOption { + type = types.bool; + default = true; + description = '' + Whether to allow custom spending amounts. + ''; + }; + + steps = mkOption { + type = types.listOf ( + types.oneOf [ + types.int + types.float + ] + ); + example = [ + 0.5 + 1 + 2 + 5 + 10 + 20 + ]; + description = '' + List of selectable spending amounts. + + This should match your most common products. + ''; + }; + }; + + transaction = { + enabled = mkOption { + type = types.bool; + default = true; + description = '' + Whether to allow transactions between user accounts. + ''; + }; + }; + + undo = { + enabled = mkOption { + type = types.bool; + default = true; + description = '' + Whether to allow undoing transactions withing the {option}`services.strichliste.settings.payment.undo.timeout` period. + ''; + }; + + delete = mkOption { + type = types.bool; + default = true; + description = '' + Whether to allow deleting within the {option}`services.strichliste.settings.payment.undo.timeout` period. + ''; + }; + + timeout = mkOption { + type = types.str; + default = "5 minute"; + description = '' + The time period after creating a transaction in which undoing/deleting remains possible. + + The format used is documented in . + ''; + }; + }; + }; + }; + }; + description = '' + The {file}`strichliste.yaml` configuration as a Nix attribute set. + + See the [configuration reference](https://github.com/strichliste/strichliste-backend/blob/v${cfg.packages.backend.version}/docs/Config.md) + for possible options. + ''; + }; + + domain = mkOption { + type = types.str; + example = "strichliste.example.com"; + description = '' + Domain name used to configure the webserver virtual host. + ''; + }; + + environment = mkOption { + type = types.submodule { + freeformType = types.attrs; + options = { + APP_ENV = mkOption { + type = types.str; + default = "prod"; + description = '' + The active environment. + ''; + }; + APP_LOG_DIR = mkOption { + type = types.path; + default = "/var/log/strichliste"; + description = '' + Directory to write logs. + ''; + }; + APP_CACHE_DIR = mkOption { + type = types.path; + default = "/var/cache/strichliste"; + description = '' + Directory used for caching. + ''; + }; + CORS_ALLOW_ORIGIN = mkOption { + type = types.str; + default = "^https?://${config.services.strichliste.domain}(:[0-9]+)?$"; + defaultText = lib.literalExpression "^https?://$${config.services.strichliste.domain}(:[0-9]+)?$"; + description = '' + Regular expression defining the allowed CORS origins. + ''; + }; + DATABASE_URL = mkOption { + type = types.str; + default = "sqlite:////var/lib/strichliste/db.sqlite"; + example = "postgresql://strichliste@localhost/strichliste?host=/run/postgresql"; + description = '' + See + for more URL examples. + ''; + }; + }; + }; + default = { }; + description = '' + Environment variables consumed by Symfony. + + See for possible options. + ''; + }; + + environmentFiles = mkOption { + type = types.listOf types.path; + default = [ ]; + example = lib.literalExpression '' + [ + "/run/keys/strichliste.env" + ] + ''; + description = '' + Environment files to configure Symfony. + + See for possible options. + + ::: {.important} + You should configure `APP_SECRET` here. + ::: + ''; + }; + + nginx = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable and configure an nginx vhost to serve strichliste. + ''; + }; + + virtualHost = mkOption { + type = types.submodule ( + import ../web-servers/nginx/vhost-options.nix { + inherit config lib; + } + ); + example = lib.literalExpression '' + { + enableACME = true; + forceSSL = true; + } + ''; + description = '' + Nginx virtual settings to allow direct customization of its settings. + ''; + }; + }; + }; + + config = mkMerge [ + (mkIf (cfg.enable && cfg.nginx.enable) { + services.phpfpm.pools.strichliste.settings = { + "listen.owner" = config.services.nginx.user; + "listen.group" = config.services.nginx.group; + }; + + services.nginx.enable = true; + services.nginx.virtualHosts.${cfg.domain} = mkMerge [ + cfg.nginx.virtualHost + { + root = mkForce "${cfg.packages.frontend}"; + locations = { + "/" = { + tryFiles = toString [ + "$uri" + "$uri/" + "index.html" + ]; + }; + + "/api/" = { + fastcgiParams = { + SCRIPT_FILENAME = "${cfg.packages.backend}/share/php/strichliste-backend/public/index.php"; + SCRIPT_NAME = "/index.php"; + REQUEST_URI = "$request_uri"; + + modHeadersAvailable = "true"; + front_controller_active = "true"; + }; + extraConfig = '' + fastcgi_intercept_errors on; + fastcgi_pass unix:${config.services.phpfpm.pools.strichliste.socket}; + fastcgi_request_buffering off; + ''; + }; + }; + } + ]; + }) + + (mkIf cfg.enable { + environment.etc."strichliste.yaml".source = settingsFile; + + systemd.tmpfiles.settings."strichliste" = { + ${cfg.environment.APP_CACHE_DIR}.d = { + user = "strichliste"; + group = "strichliste"; + mode = "0700"; + }; + ${cfg.environment.APP_LOG_DIR}.d = { + user = "strichliste"; + group = "strichliste"; + mode = "0700"; + }; + }; + + systemd.services.strichliste-migrate = { + wantedBy = [ "phpfpm-strichliste.service" ]; + before = [ "phpfpm-strichliste.service" ]; + wants = unitDependencies; + after = unitDependencies; + inherit (cfg) environment; + preStart = '' + set -ex + if [ ! -e "/var/lib/strichliste/.db-init" ]; then + ${lib.optionalString (lib.hasInfix "sqlite" cfg.environment.DATABASE_URL) '' + ${lib.getExe cfg.packages.backend} doctrine:database:create + ''} + ${lib.getExe cfg.packages.backend} doctrine:schema:create + touch "/var/lib/strichliste/.db-init" + fi + ''; + serviceConfig = { + Type = "exec"; + User = "strichliste"; + Group = "strichliste"; + EnvironmentFile = cfg.environmentFiles; + ExecStart = toString [ + (lib.getExe cfg.packages.backend) + "doctrine:migrations:migrate" + "--allow-no-migration" + "--no-interaction" + ]; + }; + }; + + systemd.services.phpfpm-strichliste = { + inherit (cfg) environment; + serviceConfig.EnvironmentFile = cfg.environmentFiles; + }; + + services.phpfpm.pools.strichliste = { + user = "strichliste"; + group = "strichliste"; + settings = { + # support environment variables + "clear_env" = false; + "pm" = "dynamic"; + "pm.max_children" = 8; + "pm.start_servers" = 1; + "pm.min_spare_servers" = 1; + "pm.max_spare_servers" = 4; + "pm.max_requests" = 256; + }; + inherit (cfg.packages.backend) phpPackage; + }; + + users.groups.strichliste = { }; + users.users.strichliste = { + group = "strichliste"; + home = "/var/lib/strichliste"; + createHome = true; + isSystemUser = true; + }; + }) + ]; +}