Repository: NixOS/nixpkgs
Pull request #349750: froide-govplan: init at 0-unstable-2025-01-27
Author: login: onny
Labels: name: 6.topic: nixos
description: Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS
name: 8.has: documentation
description: This PR adds or changes documentation
name: 8.has: changelog
description: This PR adds or changes release notes
name: 8.has: package (new)
description: This PR adds a new package
name: 8.has: module (new)
description: This PR adds a module in `nixos/`
name: 8.has: module (update)
description: This PR changes an existing module in `nixos/`
name: 10.rebuild-linux: 1-10
description: This PR causes between 1 and 10 packages to rebuild on Linux.
name: 10.rebuild-darwin: 1-10
description: This PR causes between 1 and 10 packages to rebuild on Darwin.
name: 11.by: package-maintainer
description: This PR was created by a maintainer of all the package it changes.
name: 10.rebuild-darwin: 1
description: This PR causes 1 package to rebuild on Darwin.
Created: 2024-10-19T11:41:34Z | Merged: 2025-03-26T13:27:02Z

[Froide-Govplan](https://github.com/okfde/froide-govplan), a web application government planer. Used by the project [fragdenstaat.de/koalitionstracker](https://fragdenstaat.de/koalitionstracker)

Enable froide-govplan by adding following snippet to your system configuration:

```
services.froide-govplan.enable = true;
```

Follow further setup instructions [here](https://github.com/okfde/froide-govplan).

Depends on:

- https://github.com/NixOS/nixpkgs/pull/350187
- https://github.com/NixOS/nixpkgs/pull/350423
- https://github.com/NixOS/nixpkgs/pull/350697
- https://github.com/NixOS/nixpkgs/pull/350865
- https://github.com/NixOS/nixpkgs/pull/351208
- https://github.com/NixOS/nixpkgs/pull/351419
- https://github.com/NixOS/nixpkgs/pull/351620
- https://github.com/NixOS/nixpkgs/pull/351838
- https://github.com/NixOS/nixpkgs/pull/352059
- https://github.com/NixOS/nixpkgs/pull/352293
- https://github.com/NixOS/nixpkgs/pull/352544
- https://github.com/NixOS/nixpkgs/pull/352837
- https://github.com/NixOS/nixpkgs/pull/353106
- https://github.com/NixOS/nixpkgs/pull/353483
- https://github.com/NixOS/nixpkgs/pull/354163
- https://github.com/NixOS/nixpkgs/pull/355167
- https://github.com/NixOS/nixpkgs/pull/355390
- https://github.com/NixOS/nixpkgs/pull/355835
- https://github.com/NixOS/nixpkgs/pull/358429
- https://github.com/okfde/froide-govplan/pull/17
- https://github.com/okfde/froide-govplan/issues/18
- https://github.com/NixOS/nixpkgs/pull/361675
- https://github.com/okfde/froide/issues/923 (froide)
- https://github.com/okfde/froide-govplan/issues/20 (froide-govplan)
- https://github.com/xi/django-mfa3/issues/24
- https://github.com/NixOS/nixpkgs/pull/362045
- https://github.com/NixOS/nixpkgs/pull/362537
- https://github.com/okfde/froide/issues/893
- https://github.com/NixOS/nixpkgs/pull/363883
- https://github.com/okfde/froide-govplan/pull/21
- https://github.com/okfde/froide-govplan/issues/22
- https://github.com/okfde/froide-govplan/issues/23
- https://github.com/okfde/froide-govplan/issues/24

ToDo

- [x] configure database url to use socket instead of tcp
- [x] support defining secret_key
- [x] STATIC_ROOT not working
- [x] froide-govplan app needs to run as govplan user
- [x] use latest froide version (no downgrade)
- [x] after creating cms page, need to restart froide-govplan to circumvent namespace error
- [x] Add test

<!--
^ Please summarise the changes you have done and explain why they are necessary here ^

For package updates please link to a changelog or describe changes, this helps your fellow maintainers discover breaking updates.
For new packages please briefly describe the package or provide a link to its homepage.
-->

## Things done

<!-- Please check what applies. Note that these are not hard requirements but merely serve as information for reviewers. -->

- Built on platform(s)
  - [x] x86_64-linux
  - [ ] aarch64-linux
  - [ ] x86_64-darwin
  - [ ] aarch64-darwin
- For non-Linux: Is sandboxing enabled in `nix.conf`? (See [Nix manual](https://nixos.org/manual/nix/stable/command-ref/conf-file.html))
  - [ ] `sandbox = relaxed`
  - [ ] `sandbox = true`
- [x] Tested, as applicable:
  - [NixOS test(s)](https://nixos.org/manual/nixos/unstable/index.html#sec-nixos-tests) (look inside [nixos/tests](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests))
  - and/or [package tests](https://github.com/NixOS/nixpkgs/blob/master/pkgs/README.md#package-tests)
  - or, for functions and "core" functionality, tests in [lib/tests](https://github.com/NixOS/nixpkgs/blob/master/lib/tests) or [pkgs/test](https://github.com/NixOS/nixpkgs/blob/master/pkgs/test)
  - made sure NixOS tests are [linked](https://github.com/NixOS/nixpkgs/blob/master/pkgs/README.md#linking-nixos-module-tests-to-a-package) to the relevant packages
- [ ] Tested compilation of all packages that depend on this change using `nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD"`. Note: all changes have to be committed, also see [nixpkgs-review usage](https://github.com/Mic92/nixpkgs-review#usage)
- [x] Tested basic functionality of all binary files (usually in `./result/bin/`)
- [24.11 Release Notes](https://github.com/NixOS/nixpkgs/blob/master/nixos/doc/manual/release-notes/rl-2411.section.md) (or backporting [23.11](https://github.com/NixOS/nixpkgs/blob/master/nixos/doc/manual/release-notes/rl-2311.section.md) and [24.05](https://github.com/NixOS/nixpkgs/blob/master/nixos/doc/manual/release-notes/rl-2405.section.md) Release notes)
  - [ ] (Package updates) Added a release notes entry if the change is major or breaking
  - [ ] (Module updates) Added a release notes entry if the change is significant
  - [x] (Module addition) Added a release notes entry if adding a new NixOS module
- [x] Fits [CONTRIBUTING.md](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md).

<!--
To help with the large amounts of pull requests, we would appreciate your
reviews of other pull requests, especially simple package updates. Just leave a
comment describing what you have tested in the relevant package/service.
Reviewing helps to reduce the average time-to-merge for everyone.
Thanks a lot if you do!

List of open PRs: https://github.com/NixOS/nixpkgs/pulls
Reviewing guidelines: https://github.com/NixOS/nixpkgs/blob/master/pkgs/README.md#reviewing-contributions
-->

---

Add a :+1: [reaction] to [pull requests you find important].

[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[pull requests you find important]: https://github.com/NixOS/nixpkgs/pulls?q=is%3Aopen+sort%3Areactions-%2B1-desc



Diff:
diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md
index dd360120b3c58..629bf0aa76532 100644
--- a/nixos/doc/manual/release-notes/rl-2505.section.md
+++ b/nixos/doc/manual/release-notes/rl-2505.section.md
@@ -119,6 +119,8 @@
 
 - [cross-seed](https://www.cross-seed.org), a tool to set-up fully automatic cross-seeding of torrents. Available as [services.cross-seed](#opt-services.cross-seed.enable).
 
+- [Froide-Govplan](https://github.com/okfde/froide-govplan), a web application government planer. Available as [services.froide-govplan](#opt-services.froide-govplan.enable).
+
 - [agorakit](https://github.com/agorakit/agorakit), an organization tool for citizens' collectives. Available with [services.agorakit](options.html#opt-services.agorakit.enable).
 
 - [vivid](https://github.com/sharkdp/vivid), a generator for LS_COLOR. Available as [programs.vivid](#opt-programs.vivid.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index c498c1c618625..9527b88a5bb2e 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1501,6 +1501,7 @@
   ./services/web-apps/flarum.nix
   ./services/web-apps/fluidd.nix
   ./services/web-apps/freshrss.nix
+  ./services/web-apps/froide-govplan.nix
   ./services/web-apps/galene.nix
   ./services/web-apps/gancio.nix
   ./services/web-apps/gerrit.nix
diff --git a/nixos/modules/services/web-apps/froide-govplan.nix b/nixos/modules/services/web-apps/froide-govplan.nix
new file mode 100644
index 0000000000000..e5c7c0b196764
--- /dev/null
+++ b/nixos/modules/services/web-apps/froide-govplan.nix
@@ -0,0 +1,237 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+let
+
+  cfg = config.services.froide-govplan;
+  pythonFmt = pkgs.formats.pythonVars { };
+  settingsFile = pythonFmt.generate "extra_settings.py" cfg.settings;
+
+  pkg = cfg.package.overridePythonAttrs (old: {
+    postInstall =
+      old.postInstall
+      + ''
+        ln -s ${settingsFile} $out/${pkg.python.sitePackages}/froide_govplan/project/extra_settings.py
+      '';
+  });
+
+  froide-govplan = pkgs.writeShellApplication {
+    name = "froide-govplan";
+    runtimeInputs = [ pkgs.coreutils ];
+    text = ''
+      SUDO="exec"
+      if [[ "$USER" != govplan ]]; then
+        SUDO="exec /run/wrappers/bin/sudo -u govplan"
+      fi
+      $SUDO env ${lib.getExe pkg} "$@"
+    '';
+  };
+
+  # Service hardening
+  defaultServiceConfig = {
+    # Secure the services
+    ReadWritePaths = [ cfg.dataDir ];
+    CacheDirectory = "froide-govplan";
+    CapabilityBoundingSet = "";
+    # ProtectClock adds DeviceAllow=char-rtc r
+    DeviceAllow = "";
+    LockPersonality = true;
+    MemoryDenyWriteExecute = true;
+    NoNewPrivileges = true;
+    PrivateDevices = true;
+    PrivateMounts = true;
+    PrivateTmp = true;
+    PrivateUsers = true;
+    ProtectClock = true;
+    ProtectHome = true;
+    ProtectHostname = true;
+    ProtectSystem = "strict";
+    ProtectControlGroups = true;
+    ProtectKernelLogs = true;
+    ProtectKernelModules = true;
+    ProtectKernelTunables = true;
+    ProtectProc = "invisible";
+    ProcSubset = "pid";
+    RestrictAddressFamilies = [
+      "AF_UNIX"
+      "AF_INET"
+      "AF_INET6"
+    ];
+    RestrictNamespaces = true;
+    RestrictRealtime = true;
+    RestrictSUIDSGID = true;
+    SystemCallArchitectures = "native";
+    SystemCallFilter = [
+      "@system-service"
+      "~@privileged @setuid @keyring"
+    ];
+    UMask = "0066";
+  };
+
+in
+{
+  options.services.froide-govplan = {
+
+    enable = lib.mkEnableOption "Gouvernment planer web app Govplan";
+
+    package = lib.mkPackageOption pkgs "froide-govplan" { };
+
+    hostName = lib.mkOption {
+      type = lib.types.str;
+      default = "localhost";
+      description = "FQDN for the froide-govplan instance.";
+    };
+
+    dataDir = lib.mkOption {
+      type = lib.types.str;
+      default = "/var/lib/froide-govplan";
+      description = "Directory to store the Froide-Govplan server data.";
+    };
+
+    secretKeyFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = ''
+        Path to a file containing the secret key.
+      '';
+    };
+
+    settings = lib.mkOption {
+      description = ''
+        Configuration options to set in `extra_settings.py`.
+      '';
+
+      default = { };
+
+      type = lib.types.submodule {
+        freeformType = pythonFmt.type;
+
+        options = {
+          ALLOWED_HOSTS = lib.mkOption {
+            type = with lib.types; listOf str;
+            default = [ "*" ];
+            description = ''
+              A list of valid fully-qualified domain names (FQDNs) and/or IP
+              addresses that can be used to reach the Froide-Govplan service.
+            '';
+          };
+        };
+      };
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    services.froide-govplan = {
+      settings = {
+        STATIC_ROOT = "${cfg.dataDir}/static";
+        DEBUG = false;
+        DATABASES.default = {
+          ENGINE = "django.contrib.gis.db.backends.postgis";
+          NAME = "govplan";
+          USER = "govplan";
+          HOST = "/run/postgresql";
+        };
+      };
+    };
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = [ "govplan" ];
+      ensureUsers = [
+        {
+          name = "govplan";
+          ensureDBOwnership = true;
+        }
+      ];
+      extensions = ps: with ps; [ postgis ];
+    };
+
+    services.nginx = {
+      enable = lib.mkDefault true;
+      virtualHosts."${cfg.hostName}".locations = {
+        "/".extraConfig = "proxy_pass http://unix:/run/froide-govplan/froide-govplan.socket;";
+        "/static/".alias = "${cfg.dataDir}/static/";
+      };
+      proxyTimeout = lib.mkDefault "120s";
+    };
+
+    systemd = {
+      services = {
+
+        postgresql.serviceConfig.ExecStartPost =
+          let
+            sqlFile = pkgs.writeText "immich-pgvectors-setup.sql" ''
+              CREATE EXTENSION IF NOT EXISTS postgis;
+            '';
+          in
+          [
+            ''
+              ${lib.getExe' config.services.postgresql.package "psql"} -d govplan -f "${sqlFile}"
+            ''
+          ];
+
+        froide-govplan = {
+          description = "Gouvernment planer Govplan";
+          serviceConfig = defaultServiceConfig // {
+            WorkingDirectory = cfg.dataDir;
+            StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/froide-govplan") "froide-govplan";
+            User = "govplan";
+            Group = "govplan";
+          };
+          after = [
+            "postgresql.service"
+            "network.target"
+            "systemd-tmpfiles-setup.service"
+          ];
+          wantedBy = [ "multi-user.target" ];
+          environment =
+            {
+              PYTHONPATH = pkg.pythonPath;
+              GDAL_LIBRARY_PATH = "${pkgs.gdal}/lib/libgdal.so";
+              GEOS_LIBRARY_PATH = "${pkgs.geos}/lib/libgeos_c.so";
+            }
+            // lib.optionalAttrs (cfg.secretKeyFile != null) {
+              SECRET_KEY_FILE = cfg.secretKeyFile;
+            };
+          preStart = ''
+            # Auto-migrate on first run or if the package has changed
+            versionFile="${cfg.dataDir}/src-version"
+            version=$(cat "$versionFile" 2>/dev/null || echo 0)
+
+            if [[ $version != ${pkg.version} ]]; then
+              ${lib.getExe pkg} migrate --no-input
+              ${lib.getExe pkg} collectstatic --no-input --clear
+              echo ${pkg.version} > "$versionFile"
+            fi
+          '';
+          script = ''
+            ${pkg.python.pkgs.uvicorn}/bin/uvicorn --uds /run/froide-govplan/froide-govplan.socket \
+              --app-dir ${pkg}/${pkg.python.sitePackages}/froide_govplan \
+              project.asgi:application
+          '';
+        };
+      };
+
+    };
+
+    systemd.tmpfiles.rules = [ "d /run/froide-govplan - govplan govplan - -" ];
+
+    environment.systemPackages = [ froide-govplan ];
+
+    users.users.govplan = {
+      home = "${cfg.dataDir}";
+      isSystemUser = true;
+      group = "govplan";
+    };
+    users.groups.govplan = { };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ onny ];
+
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 87d07c0b0f23e..8a9345ba9d47f 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -411,6 +411,7 @@ in {
   freetube = discoverTests (import ./freetube.nix);
   freshrss = handleTest ./freshrss {};
   frigate = runTest ./frigate.nix;
+  froide-govplan = runTest ./web-apps/froide-govplan.nix;
   frp = handleTest ./frp.nix {};
   frr = handleTest ./frr.nix {};
   fsck = handleTest ./fsck.nix {};
diff --git a/nixos/tests/web-apps/froide-govplan.nix b/nixos/tests/web-apps/froide-govplan.nix
new file mode 100644
index 0000000000000..05f42a52863eb
--- /dev/null
+++ b/nixos/tests/web-apps/froide-govplan.nix
@@ -0,0 +1,44 @@
+{
+  lib,
+  pkgs,
+  ...
+}:
+
+{
+  name = "froide-govplan";
+  meta.maintainers = with lib.maintainers; [ onny ];
+
+  nodes = {
+    machine = {
+      virtualisation.memorySize = 2048;
+      services.froide-govplan.enable = true;
+    };
+  };
+
+  testScript =
+    let
+      changePassword = pkgs.writeText "change-password.py" ''
+        from django.contrib.auth.models import User
+        u = User.objects.get(username='govplan')
+        u.set_password('govplan')
+        u.save()
+      '';
+    in
+    ''
+      start_all()
+      machine.wait_for_unit("froide-govplan.service")
+      machine.wait_for_file("/run/froide-govplan/froide-govplan.socket")
+
+      with subtest("Home screen loads"):
+          machine.succeed(
+              "curl -sSfL http://[::1]:80 | grep '<title>django CMS</title>'"
+          )
+
+      with subtest("Superuser can be created"):
+          machine.succeed(
+              "froide-govplan createsuperuser --noinput --username govplan --email govplan@example.com"
+          )
+          # Django doesn't have a "clean" way of inputting the password from the command line
+          machine.succeed("cat '${changePassword}' | froide-govplan shell")
+    '';
+}
diff --git a/pkgs/by-name/fr/froide-govplan/load_extra_settings.patch b/pkgs/by-name/fr/froide-govplan/load_extra_settings.patch
new file mode 100644
index 0000000000000..5f1defcdcd1ae
--- /dev/null
+++ b/pkgs/by-name/fr/froide-govplan/load_extra_settings.patch
@@ -0,0 +1,15 @@
+diff --git a/project/settings.py b/project/settings.py
+index dd282ac..64ff265 100644
+--- a/project/settings.py
++++ b/project/settings.py
+@@ -202,3 +202,10 @@ CMS_CONFIRM_VERSION4 = True
+ 
+ GOVPLAN_NAME = "GovPlan"
+ GOVPLAN_ENABLE_FOIREQUEST = False
++
++EXTRA_SETTINGS_PATH = os.path.join(PROJECT_DIR, 'extra_settings.py')
++
++if os.path.exists(EXTRA_SETTINGS_PATH):
++    with open(EXTRA_SETTINGS_PATH) as f:
++        code = compile(f.read(), EXTRA_SETTINGS_PATH, 'exec')
++        exec(code)
diff --git a/pkgs/by-name/fr/froide-govplan/package.nix b/pkgs/by-name/fr/froide-govplan/package.nix
new file mode 100644
index 0000000000000..cd2f605c7ca94
--- /dev/null
+++ b/pkgs/by-name/fr/froide-govplan/package.nix
@@ -0,0 +1,127 @@
+{
+  lib,
+  python3Packages,
+  fetchFromGitHub,
+  makeBinaryWrapper,
+  froide,
+  gdal,
+  geos,
+  nixosTests,
+  fetchpatch,
+  froide-govplan,
+  gettext,
+}:
+let
+  python = python3Packages.python.override {
+    packageOverrides = self: super: {
+      django = super.django.override { withGdal = true; };
+    };
+  };
+in
+python.pkgs.buildPythonApplication rec {
+  pname = "froide-govplan";
+  version = "0-unstable-2025-01-27";
+  pyproject = true;
+
+  src = fetchFromGitHub {
+    owner = "okfde";
+    repo = "froide-govplan";
+    # No tagged release yet
+    # https://github.com/okfde/froide-govplan/issues/15
+    rev = "f1763807614b8c54a9214359a2a1e442ca58cb6d";
+    hash = "sha256-Y7/qjhu3y9E55ZDmDf5HUk9JVcUTvi9KnqavqwNixTM=";
+  };
+
+  patches = [
+    # Add missing oauth2_provider app
+    # https://github.com/okfde/froide-govplan/pull/17
+    (fetchpatch {
+      url = "https://github.com/okfde/froide-govplan/commit/bc388b693ebc7656fc7917511048a47b68e119fe.patch";
+      hash = "sha256-StIyN3gFKd/fld14s03fsV6fmWinSRWINUyRRBzqVco=";
+      name = "add_oauth2_provider_app.patch";
+    })
+    # Enable automatic image cropping for filer
+    # https://github.com/okfde/froide-govplan/pull/19
+    (fetchpatch {
+      url = "https://github.com/okfde/froide-govplan/commit/d5db02f3bbc501929a901e964147a0ed2f974f41.patch";
+      hash = "sha256-jf6e84fd2kQIPER2HGmZNkbtl067311xdNJRU+iFVXM=";
+      name = "filer_enable_automatic_cropping.patch";
+    })
+    # Add support for reading SECRET_KEY_FILE
+    # https://github.com/okfde/froide-govplan/pull/21
+    (fetchpatch {
+      url = "https://github.com/okfde/froide-govplan/commit/39dc381ed6c4afcbbaea616f0e8f44d5eeb96ec2.patch";
+      hash = "sha256-PxBtB6VupK4QJiWKKlTmub+7tfi6nAHWe29lxpNgoVk=";
+      name = "add_secret_key_file_support.patch";
+    })
+
+    # Patch settings.py to source additional settings from the NixOS module
+    ./load_extra_settings.patch
+  ];
+
+  build-system = [ python.pkgs.setuptools ];
+
+  nativeBuildInputs = [
+    gettext
+    makeBinaryWrapper
+  ];
+
+  build-inputs = [ gdal ];
+
+  dependencies = with python.pkgs; [
+    bleach
+    django-admin-sortable2
+    django-cms
+    django-filer
+    django-mfa3
+    django-mptt
+    django-oauth-toolkit
+    django-sekizai
+    django-tinymce
+    django-treebeard
+    djangocms-alias
+    # Downgrade to last working version
+    (toPythonModule (
+      froide.overridePythonAttrs (prev: {
+        nativeBuildInputs = [ makeBinaryWrapper ];
+        postBuild = "";
+        doCheck = false;
+        pnpmDeps = null;
+        src = prev.src.override {
+          rev = "a78a4054f9f37b0a5109a6d8cfbbda742f86a8ca";
+          hash = "sha256-gtOssbsVf3nG+pmLPgvh4685vHh2x+jlXiTjU+JhQa8=";
+        };
+      })
+    ))
+    psycopg
+  ];
+
+  preBuild = "${python.interpreter} -m django compilemessages";
+
+  postInstall = ''
+    cp manage.py $out/${python.sitePackages}/froide_govplan/
+    cp -r project $out/${python.sitePackages}/froide_govplan/
+    cp -r froide_govplan/locale $out/${python.sitePackages}/froide_govplan/
+    makeWrapper $out/${python.sitePackages}/froide_govplan/manage.py $out/bin/froide-govplan \
+      --prefix PYTHONPATH : "$PYTHONPATH" \
+      --set GDAL_LIBRARY_PATH "${gdal}/lib/libgdal.so" \
+      --set GEOS_LIBRARY_PATH "${geos}/lib/libgeos_c.so"
+  '';
+
+  passthru = {
+    tests = {
+      inherit (nixosTests) froide-govplan;
+    };
+    python = python;
+    pythonPath = "${python.pkgs.makePythonPath dependencies}:${froide-govplan}/${python.sitePackages}";
+  };
+
+  meta = {
+    description = "Government planner and basis of FragDenStaat.de Koalitionstracker";
+    homepage = "https://github.com/okfde/froide-govplan";
+    license = lib.licenses.mit;
+    maintainers = [ lib.maintainers.onny ];
+    mainProgram = "froide-govplan";
+  };
+
+}


Reviews:
user: login: mweinelt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T11:53:17Z
user: login: onny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T13:29:31Z
user: login: drupol
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:35:19Z
user: login: drupol
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:35:41Z
user: login: drupol
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:36:09Z
user: login: drupol
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:36:37Z
user: login: drupol
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T15:29:23Z
user: login: drupol
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T17:03:33Z
user: login: drupol
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T06:39:53Z
user: login: onny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T13:19:27Z
user: login: drupol
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T13:20:30Z
user: login: drupol
body: I'll mark this as LGTM, but I haven't tested it, I trust @onny for that.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T13:21:07Z

Review comments:
path: pkgs/development/python-modules/psycopg/default.nix
user: login: mweinelt
body: That's not going to fly.
created_at: 2025-03-06T11:53:17Z
author_association: MEMBER
path: pkgs/development/python-modules/psycopg/default.nix
user: login: onny
body: fixed :)
created_at: 2025-03-06T13:29:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,234 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+let
+
+  cfg = config.services.froide-govplan;
+  pythonFmt = pkgs.formats.pythonVars { };
+  settingsFile = pythonFmt.generate "extra_settings.py" cfg.settings;
+
+  pkg = cfg.package.overridePythonAttrs (old: {
+    postInstall =
+      old.postInstall
+      + ''
+        ln -s ${settingsFile} $out/${pkg.python.sitePackages}/froide_govplan/project/extra_settings.py
+      '';
+  });
+
+  froide-govplan = pkgs.writeScriptBin "froide-govplan" ''
path: nixos/modules/services/web-apps/froide-govplan.nix
user: login: drupol
body: You could use `writeShellApplication` so that it automatically run `shellCheck` on the script.
created_at: 2025-03-21T12:35:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -411,6 +411,7 @@ in {
   freetube = discoverTests (import ./freetube.nix);
   freshrss = handleTest ./freshrss {};
   frigate = runTest ./frigate.nix;
+  froide-govplan = handleTest ./web-apps/froide-govplan.nix {};
path: nixos/tests/all-tests.nix
user: login: drupol
body: Use `runTest` instead please.
created_at: 2025-03-21T12:35:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,130 @@
+{
+  lib,
+  python3,
+  fetchFromGitHub,
+  makeWrapper,
+  froide,
+  gdal,
+  geos,
+  nixosTests,
+  fetchpatch,
+  froide-govplan,
+  gettext,
+}:
+
+let
+
path: pkgs/by-name/fr/froide-govplan/package.nix
user: login: drupol
body: Why those blank lines in all the files?
created_at: 2025-03-21T12:36:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,130 @@
+{
+  lib,
+  python3,
path: pkgs/by-name/fr/froide-govplan/package.nix
user: login: drupol
body: Use `python3Packages`.
created_at: 2025-03-21T12:36:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,127 @@
+{
+  lib,
+  python3Packages,
+  fetchFromGitHub,
+  makeWrapper,
path: pkgs/by-name/fr/froide-govplan/package.nix
user: login: drupol
body: Let's use makeBinaryWrapper
created_at: 2025-03-23T15:28:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,127 @@
+{
+  lib,
+  python3Packages,
+  fetchFromGitHub,
+  makeBinaryWrapper,
+  froide,
+  gdal,
+  geos,
+  nixosTests,
+  fetchpatch,
+  froide-govplan,
+  gettext,
+}:
+let
+  python = python3Packages.python.override {
+    packageOverrides = self: super: {
+      django = super.django.override { withGdal = true; };
+    };
+  };
+in
+python.pkgs.buildPythonApplication rec {
+  pname = "froide-govplan";
+  version = "0-unstable-2025-01-27";
+  pyproject = true;
+
+  src = fetchFromGitHub {
+    owner = "okfde";
+    repo = "froide-govplan";
+    # No tagged release yet
+    # https://github.com/okfde/froide-govplan/issues/15
+    rev = "f1763807614b8c54a9214359a2a1e442ca58cb6d";
+    hash = "sha256-Y7/qjhu3y9E55ZDmDf5HUk9JVcUTvi9KnqavqwNixTM=";
+  };
+
+  patches = [
+    # Add missing oauth2_provider app
+    # https://github.com/okfde/froide-govplan/pull/17
+    (fetchpatch {
+      url = "https://github.com/okfde/froide-govplan/commit/bc388b693ebc7656fc7917511048a47b68e119fe.patch";
+      hash = "sha256-StIyN3gFKd/fld14s03fsV6fmWinSRWINUyRRBzqVco=";
+      name = "add_oauth2_provider_app.patch";
+    })
+    # Enable automatic image cropping for filer
+    # https://github.com/okfde/froide-govplan/pull/19
+    (fetchpatch {
+      url = "https://github.com/okfde/froide-govplan/commit/d5db02f3bbc501929a901e964147a0ed2f974f41.patch";
+      hash = "sha256-jf6e84fd2kQIPER2HGmZNkbtl067311xdNJRU+iFVXM=";
+      name = "filer_enable_automatic_cropping.patch";
+    })
+    # Add support for reading SECRET_KEY_FILE
+    # https://github.com/okfde/froide-govplan/pull/21
+    (fetchpatch {
+      url = "https://github.com/okfde/froide-govplan/commit/39dc381ed6c4afcbbaea616f0e8f44d5eeb96ec2.patch";
+      hash = "sha256-PxBtB6VupK4QJiWKKlTmub+7tfi6nAHWe29lxpNgoVk=";
+      name = "add_secret_key_file_support.patch";
+    })
+
+    # Patch settings.py to source additional settings from the NixOS module
+    ./load_extra_settings.patch
+  ];
+
+  build-system = [ python.pkgs.setuptools ];
+
+  nativeBuildInputs = [
+    gettext
+    makeBinaryWrapper
+  ];
+
+  build-inputs = [ gdal ];
+
+  dependencies = with python.pkgs; [
+    bleach
+    django-admin-sortable2
+    django-cms
+    django-filer
+    django-mfa3
+    django-mptt
+    django-oauth-toolkit
+    django-sekizai
+    django-tinymce
+    django-treebeard
+    djangocms-alias
+    # Downgrade to last working version
+    (toPythonModule (
+      froide.overridePythonAttrs (prev: {
+        nativeBuildInputs = [ makeWrapper ];
path: pkgs/by-name/fr/froide-govplan/package.nix
user: login: drupol
body: ```suggestion
        nativeBuildInputs = [ makeBinaryWrapper ];
```
created_at: 2025-03-23T17:03:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,127 @@
+{
+  lib,
+  python3Packages,
+  fetchFromGitHub,
+  makeBinaryWrapper,
+  froide,
+  gdal,
+  geos,
+  nixosTests,
+  fetchpatch,
+  froide-govplan,
+  gettext,
+}:
+let
+  python = python3Packages.python.override {
+    packageOverrides = self: super: {
+      django = super.django.override { withGdal = true; };
+    };
+  };
+in
+python.pkgs.buildPythonApplication rec {
+  pname = "froide-govplan";
+  version = "0-unstable-2025-01-27";
+  pyproject = true;
+
+  src = fetchFromGitHub {
+    owner = "okfde";
+    repo = "froide-govplan";
+    # No tagged release yet
+    # https://github.com/okfde/froide-govplan/issues/15
+    rev = "f1763807614b8c54a9214359a2a1e442ca58cb6d";
+    hash = "sha256-Y7/qjhu3y9E55ZDmDf5HUk9JVcUTvi9KnqavqwNixTM=";
+  };
+
+  patches = [
+    # Add missing oauth2_provider app
+    # https://github.com/okfde/froide-govplan/pull/17
+    (fetchpatch {
+      url = "https://github.com/okfde/froide-govplan/commit/bc388b693ebc7656fc7917511048a47b68e119fe.patch";
+      hash = "sha256-StIyN3gFKd/fld14s03fsV6fmWinSRWINUyRRBzqVco=";
+      name = "add_oauth2_provider_app.patch";
+    })
+    # Enable automatic image cropping for filer
+    # https://github.com/okfde/froide-govplan/pull/19
+    (fetchpatch {
+      url = "https://github.com/okfde/froide-govplan/commit/d5db02f3bbc501929a901e964147a0ed2f974f41.patch";
+      hash = "sha256-jf6e84fd2kQIPER2HGmZNkbtl067311xdNJRU+iFVXM=";
+      name = "filer_enable_automatic_cropping.patch";
+    })
+    # Add support for reading SECRET_KEY_FILE
+    # https://github.com/okfde/froide-govplan/pull/21
+    (fetchpatch {
+      url = "https://github.com/okfde/froide-govplan/commit/39dc381ed6c4afcbbaea616f0e8f44d5eeb96ec2.patch";
+      hash = "sha256-PxBtB6VupK4QJiWKKlTmub+7tfi6nAHWe29lxpNgoVk=";
+      name = "add_secret_key_file_support.patch";
+    })
+
+    # Patch settings.py to source additional settings from the NixOS module
+    ./load_extra_settings.patch
+  ];
+
+  build-system = [ python.pkgs.setuptools ];
+
+  nativeBuildInputs = [
+    gettext
+    makeBinaryWrapper
+  ];
+
+  build-inputs = [ gdal ];
+
+  dependencies = with python.pkgs; [
+    bleach
+    django-admin-sortable2
+    django-cms
+    django-filer
+    django-mfa3
+    django-mptt
+    django-oauth-toolkit
+    django-sekizai
+    django-tinymce
+    django-treebeard
+    djangocms-alias
+    # Downgrade to last working version
+    (toPythonModule (
+      froide.overridePythonAttrs (prev: {
+        nativeBuildInputs = [ makeWrapper ];
+        postBuild = "";
+        doCheck = false;
+        pnpmDeps = null;
+        src = prev.src.override {
+          rev = "a78a4054f9f37b0a5109a6d8cfbbda742f86a8ca";
+          hash = "sha256-gtOssbsVf3nG+pmLPgvh4685vHh2x+jlXiTjU+JhQa8=";
+        };
+      })
+    ))
+    psycopg
+  ];
+
+  preBuild = "${python.interpreter} -m django compilemessages";
+
+  postInstall = ''
+    cp manage.py $out/${python.sitePackages}/froide_govplan/
+    cp -r project $out/${python.sitePackages}/froide_govplan/
+    cp -r froide_govplan/locale $out/${python.sitePackages}/froide_govplan/
+    makeBinaryWrapper $out/${python.sitePackages}/froide_govplan/manage.py $out/bin/froide-govplan \
path: pkgs/by-name/fr/froide-govplan/package.nix
user: login: drupol
body: ```suggestion
    makeWrapper $out/${python.sitePackages}/froide_govplan/manage.py $out/bin/froide-govplan \
```
created_at: 2025-03-23T17:03:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,237 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+let
+
+  cfg = config.services.froide-govplan;
+  pythonFmt = pkgs.formats.pythonVars { };
+  settingsFile = pythonFmt.generate "extra_settings.py" cfg.settings;
+
+  pkg = cfg.package.overridePythonAttrs (old: {
+    postInstall =
+      old.postInstall
+      + ''
+        ln -s ${settingsFile} $out/${pkg.python.sitePackages}/froide_govplan/project/extra_settings.py
+      '';
+  });
+
+  froide-govplan = pkgs.writeShellApplication {
+    name = "froide-govplan";
+    runtimeInputs = [ pkgs.coreutils ];
+    text = ''
+      SUDO="exec"
+      if [[ "$USER" != govplan ]]; then
+        SUDO="exec /run/wrappers/bin/sudo -u govplan"
+      fi
+      $SUDO env ${lib.getExe pkg} "$@"
+    '';
+  };
+
+  # Service hardening
+  defaultServiceConfig = {
+    # Secure the services
+    ReadWritePaths = [ cfg.dataDir ];
+    CacheDirectory = "froide-govplan";
+    CapabilityBoundingSet = "";
+    # ProtectClock adds DeviceAllow=char-rtc r
+    DeviceAllow = "";
+    LockPersonality = true;
+    MemoryDenyWriteExecute = true;
+    NoNewPrivileges = true;
+    PrivateDevices = true;
+    PrivateMounts = true;
+    PrivateTmp = true;
+    PrivateUsers = true;
+    ProtectClock = true;
+    ProtectHome = true;
+    ProtectHostname = true;
+    ProtectSystem = "strict";
+    ProtectControlGroups = true;
+    ProtectKernelLogs = true;
+    ProtectKernelModules = true;
+    ProtectKernelTunables = true;
+    ProtectProc = "invisible";
+    ProcSubset = "pid";
+    RestrictAddressFamilies = [
+      "AF_UNIX"
+      "AF_INET"
+      "AF_INET6"
+    ];
+    RestrictNamespaces = true;
+    RestrictRealtime = true;
+    RestrictSUIDSGID = true;
+    SystemCallArchitectures = "native";
+    SystemCallFilter = [
+      "@system-service"
+      "~@privileged @setuid @keyring"
+    ];
+    UMask = "0066";
+  };
+
+in
+{
+  options.services.froide-govplan = {
+
+    enable = lib.mkEnableOption "Gouvernment planer web app Govplan";
+
+    package = lib.mkPackageOption pkgs "froide-govplan" { };
+
+    hostName = lib.mkOption {
+      type = lib.types.str;
+      default = "localhost";
+      description = "FQDN for the froide-govplan instance.";
+    };
+
+    dataDir = lib.mkOption {
+      type = lib.types.str;
+      default = "/var/lib/froide-govplan";
+      description = "Directory to store the Froide-Govplan server data.";
+    };
+
+    secretKeyFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = ''
+        Path to a file containing the secret key.
+      '';
+    };
+
+    settings = lib.mkOption {
+      description = ''
+        Configuration options to set in `extra_settings.py`.
+      '';
+
+      default = { };
+
+      type = lib.types.submodule {
+        freeformType = pythonFmt.type;
+
+        options = {
+          ALLOWED_HOSTS = lib.mkOption {
+            type = with lib.types; listOf str;
+            default = [ "*" ];
+            description = ''
+              A list of valid fully-qualified domain names (FQDNs) and/or IP
+              addresses that can be used to reach the Froide-Govplan service.
+            '';
+          };
+        };
+      };
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    services.froide-govplan = {
+      settings = {
+        STATIC_ROOT = "${cfg.dataDir}/static";
+        DEBUG = false;
+        DATABASES.default = {
+          ENGINE = "django.contrib.gis.db.backends.postgis";
+          NAME = "govplan";
+          USER = "govplan";
+          HOST = "/run/postgresql";
+        };
+      };
+    };
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = [ "govplan" ];
+      ensureUsers = [
+        {
+          name = "govplan";
+          ensureDBOwnership = true;
+        }
+      ];
+      extensions = ps: with ps; [ postgis ];
+    };
+
+    services.nginx = {
+      enable = lib.mkDefault true;
+      virtualHosts."${cfg.hostName}".locations = {
+        "/".extraConfig = "proxy_pass http://unix:/run/froide-govplan/froide-govplan.socket;";
+        "/static/".alias = "${cfg.dataDir}/static/";
+      };
+      proxyTimeout = lib.mkDefault "120s";
+    };
+
+    systemd = {
+      services = {
+
+        postgresql.serviceConfig.ExecStartPost =
+          let
+            sqlFile = pkgs.writeText "immich-pgvectors-setup.sql" ''
+              CREATE EXTENSION IF NOT EXISTS postgis;
+            '';
+          in
+          [
+            ''
+              ${lib.getExe' config.services.postgresql.package "psql"} -d govplan -f "${sqlFile}"
+            ''
+          ];
+
+        froide-govplan = {
+          description = "Gouvernment planer Govplan";
+          serviceConfig = defaultServiceConfig // {
+            WorkingDirectory = cfg.dataDir;
+            StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/froide-govplan") "froide-govplan";
+            User = "govplan";
+            Group = "govplan";
+          };
+          after = [
+            "postgresql.service"
+            "network.target"
+            "systemd-tmpfiles-setup.service"
+          ];
+          wantedBy = [ "multi-user.target" ];
+          environment =
+            {
+              PYTHONPATH = pkg.pythonPath;
+              GDAL_LIBRARY_PATH = "${pkgs.gdal}/lib/libgdal.so";
+              GEOS_LIBRARY_PATH = "${pkgs.geos}/lib/libgeos_c.so";
+            }
+            // lib.optionalAttrs (cfg.secretKeyFile != null) {
+              SECRET_KEY_FILE = cfg.secretKeyFile;
+            };
+          preStart = ''
+            # Auto-migrate on first run or if the package has changed
+            versionFile="${cfg.dataDir}/src-version"
+            version=$(cat "$versionFile" 2>/dev/null || echo 0)
+
+            if [[ $version != ${pkg.version} ]]; then
+              ${lib.getExe pkg} migrate --no-input
+              ${lib.getExe pkg} collectstatic --no-input --clear
+              echo ${pkg.version} > "$versionFile"
+            fi
+          '';
+          script = ''
+            ${pkg.python.pkgs.uvicorn}/bin/uvicorn --uds /run/froide-govplan/froide-govplan.socket \
+              --app-dir ${pkg}/${pkg.python.sitePackages}/froide_govplan \
+              project.asgi:application
+          '';
+        };
+      };
+
+    };
+
+    systemd.tmpfiles.rules = [ "d /run/froide-govplan - govplan govplan - -" ];
+
+    environment.systemPackages = [ froide-govplan ];
+
+    users.users.govplan = {
path: nixos/modules/services/web-apps/froide-govplan.nix
user: login: drupol
body: Why a user is needed ? Can't you use a systemd dynamicUser ?
created_at: 2025-03-26T06:39:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,237 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+let
+
+  cfg = config.services.froide-govplan;
+  pythonFmt = pkgs.formats.pythonVars { };
+  settingsFile = pythonFmt.generate "extra_settings.py" cfg.settings;
+
+  pkg = cfg.package.overridePythonAttrs (old: {
+    postInstall =
+      old.postInstall
+      + ''
+        ln -s ${settingsFile} $out/${pkg.python.sitePackages}/froide_govplan/project/extra_settings.py
+      '';
+  });
+
+  froide-govplan = pkgs.writeShellApplication {
+    name = "froide-govplan";
+    runtimeInputs = [ pkgs.coreutils ];
+    text = ''
+      SUDO="exec"
+      if [[ "$USER" != govplan ]]; then
+        SUDO="exec /run/wrappers/bin/sudo -u govplan"
+      fi
+      $SUDO env ${lib.getExe pkg} "$@"
+    '';
+  };
+
+  # Service hardening
+  defaultServiceConfig = {
+    # Secure the services
+    ReadWritePaths = [ cfg.dataDir ];
+    CacheDirectory = "froide-govplan";
+    CapabilityBoundingSet = "";
+    # ProtectClock adds DeviceAllow=char-rtc r
+    DeviceAllow = "";
+    LockPersonality = true;
+    MemoryDenyWriteExecute = true;
+    NoNewPrivileges = true;
+    PrivateDevices = true;
+    PrivateMounts = true;
+    PrivateTmp = true;
+    PrivateUsers = true;
+    ProtectClock = true;
+    ProtectHome = true;
+    ProtectHostname = true;
+    ProtectSystem = "strict";
+    ProtectControlGroups = true;
+    ProtectKernelLogs = true;
+    ProtectKernelModules = true;
+    ProtectKernelTunables = true;
+    ProtectProc = "invisible";
+    ProcSubset = "pid";
+    RestrictAddressFamilies = [
+      "AF_UNIX"
+      "AF_INET"
+      "AF_INET6"
+    ];
+    RestrictNamespaces = true;
+    RestrictRealtime = true;
+    RestrictSUIDSGID = true;
+    SystemCallArchitectures = "native";
+    SystemCallFilter = [
+      "@system-service"
+      "~@privileged @setuid @keyring"
+    ];
+    UMask = "0066";
+  };
+
+in
+{
+  options.services.froide-govplan = {
+
+    enable = lib.mkEnableOption "Gouvernment planer web app Govplan";
+
+    package = lib.mkPackageOption pkgs "froide-govplan" { };
+
+    hostName = lib.mkOption {
+      type = lib.types.str;
+      default = "localhost";
+      description = "FQDN for the froide-govplan instance.";
+    };
+
+    dataDir = lib.mkOption {
+      type = lib.types.str;
+      default = "/var/lib/froide-govplan";
+      description = "Directory to store the Froide-Govplan server data.";
+    };
+
+    secretKeyFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = ''
+        Path to a file containing the secret key.
+      '';
+    };
+
+    settings = lib.mkOption {
+      description = ''
+        Configuration options to set in `extra_settings.py`.
+      '';
+
+      default = { };
+
+      type = lib.types.submodule {
+        freeformType = pythonFmt.type;
+
+        options = {
+          ALLOWED_HOSTS = lib.mkOption {
+            type = with lib.types; listOf str;
+            default = [ "*" ];
+            description = ''
+              A list of valid fully-qualified domain names (FQDNs) and/or IP
+              addresses that can be used to reach the Froide-Govplan service.
+            '';
+          };
+        };
+      };
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    services.froide-govplan = {
+      settings = {
+        STATIC_ROOT = "${cfg.dataDir}/static";
+        DEBUG = false;
+        DATABASES.default = {
+          ENGINE = "django.contrib.gis.db.backends.postgis";
+          NAME = "govplan";
+          USER = "govplan";
+          HOST = "/run/postgresql";
+        };
+      };
+    };
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = [ "govplan" ];
+      ensureUsers = [
+        {
+          name = "govplan";
+          ensureDBOwnership = true;
+        }
+      ];
+      extensions = ps: with ps; [ postgis ];
+    };
+
+    services.nginx = {
+      enable = lib.mkDefault true;
+      virtualHosts."${cfg.hostName}".locations = {
+        "/".extraConfig = "proxy_pass http://unix:/run/froide-govplan/froide-govplan.socket;";
+        "/static/".alias = "${cfg.dataDir}/static/";
+      };
+      proxyTimeout = lib.mkDefault "120s";
+    };
+
+    systemd = {
+      services = {
+
+        postgresql.serviceConfig.ExecStartPost =
+          let
+            sqlFile = pkgs.writeText "immich-pgvectors-setup.sql" ''
+              CREATE EXTENSION IF NOT EXISTS postgis;
+            '';
+          in
+          [
+            ''
+              ${lib.getExe' config.services.postgresql.package "psql"} -d govplan -f "${sqlFile}"
+            ''
+          ];
+
+        froide-govplan = {
+          description = "Gouvernment planer Govplan";
+          serviceConfig = defaultServiceConfig // {
+            WorkingDirectory = cfg.dataDir;
+            StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/froide-govplan") "froide-govplan";
+            User = "govplan";
+            Group = "govplan";
+          };
+          after = [
+            "postgresql.service"
+            "network.target"
+            "systemd-tmpfiles-setup.service"
+          ];
+          wantedBy = [ "multi-user.target" ];
+          environment =
+            {
+              PYTHONPATH = pkg.pythonPath;
+              GDAL_LIBRARY_PATH = "${pkgs.gdal}/lib/libgdal.so";
+              GEOS_LIBRARY_PATH = "${pkgs.geos}/lib/libgeos_c.so";
+            }
+            // lib.optionalAttrs (cfg.secretKeyFile != null) {
+              SECRET_KEY_FILE = cfg.secretKeyFile;
+            };
+          preStart = ''
+            # Auto-migrate on first run or if the package has changed
+            versionFile="${cfg.dataDir}/src-version"
+            version=$(cat "$versionFile" 2>/dev/null || echo 0)
+
+            if [[ $version != ${pkg.version} ]]; then
+              ${lib.getExe pkg} migrate --no-input
+              ${lib.getExe pkg} collectstatic --no-input --clear
+              echo ${pkg.version} > "$versionFile"
+            fi
+          '';
+          script = ''
+            ${pkg.python.pkgs.uvicorn}/bin/uvicorn --uds /run/froide-govplan/froide-govplan.socket \
+              --app-dir ${pkg}/${pkg.python.sitePackages}/froide_govplan \
+              project.asgi:application
+          '';
+        };
+      };
+
+    };
+
+    systemd.tmpfiles.rules = [ "d /run/froide-govplan - govplan govplan - -" ];
+
+    environment.systemPackages = [ froide-govplan ];
+
+    users.users.govplan = {
path: nixos/modules/services/web-apps/froide-govplan.nix
user: login: onny
body: i use it for socket authentication to the psql db
created_at: 2025-03-26T13:19:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,237 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+let
+
+  cfg = config.services.froide-govplan;
+  pythonFmt = pkgs.formats.pythonVars { };
+  settingsFile = pythonFmt.generate "extra_settings.py" cfg.settings;
+
+  pkg = cfg.package.overridePythonAttrs (old: {
+    postInstall =
+      old.postInstall
+      + ''
+        ln -s ${settingsFile} $out/${pkg.python.sitePackages}/froide_govplan/project/extra_settings.py
+      '';
+  });
+
+  froide-govplan = pkgs.writeShellApplication {
+    name = "froide-govplan";
+    runtimeInputs = [ pkgs.coreutils ];
+    text = ''
+      SUDO="exec"
+      if [[ "$USER" != govplan ]]; then
+        SUDO="exec /run/wrappers/bin/sudo -u govplan"
+      fi
+      $SUDO env ${lib.getExe pkg} "$@"
+    '';
+  };
+
+  # Service hardening
+  defaultServiceConfig = {
+    # Secure the services
+    ReadWritePaths = [ cfg.dataDir ];
+    CacheDirectory = "froide-govplan";
+    CapabilityBoundingSet = "";
+    # ProtectClock adds DeviceAllow=char-rtc r
+    DeviceAllow = "";
+    LockPersonality = true;
+    MemoryDenyWriteExecute = true;
+    NoNewPrivileges = true;
+    PrivateDevices = true;
+    PrivateMounts = true;
+    PrivateTmp = true;
+    PrivateUsers = true;
+    ProtectClock = true;
+    ProtectHome = true;
+    ProtectHostname = true;
+    ProtectSystem = "strict";
+    ProtectControlGroups = true;
+    ProtectKernelLogs = true;
+    ProtectKernelModules = true;
+    ProtectKernelTunables = true;
+    ProtectProc = "invisible";
+    ProcSubset = "pid";
+    RestrictAddressFamilies = [
+      "AF_UNIX"
+      "AF_INET"
+      "AF_INET6"
+    ];
+    RestrictNamespaces = true;
+    RestrictRealtime = true;
+    RestrictSUIDSGID = true;
+    SystemCallArchitectures = "native";
+    SystemCallFilter = [
+      "@system-service"
+      "~@privileged @setuid @keyring"
+    ];
+    UMask = "0066";
+  };
+
+in
+{
+  options.services.froide-govplan = {
+
+    enable = lib.mkEnableOption "Gouvernment planer web app Govplan";
+
+    package = lib.mkPackageOption pkgs "froide-govplan" { };
+
+    hostName = lib.mkOption {
+      type = lib.types.str;
+      default = "localhost";
+      description = "FQDN for the froide-govplan instance.";
+    };
+
+    dataDir = lib.mkOption {
+      type = lib.types.str;
+      default = "/var/lib/froide-govplan";
+      description = "Directory to store the Froide-Govplan server data.";
+    };
+
+    secretKeyFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = ''
+        Path to a file containing the secret key.
+      '';
+    };
+
+    settings = lib.mkOption {
+      description = ''
+        Configuration options to set in `extra_settings.py`.
+      '';
+
+      default = { };
+
+      type = lib.types.submodule {
+        freeformType = pythonFmt.type;
+
+        options = {
+          ALLOWED_HOSTS = lib.mkOption {
+            type = with lib.types; listOf str;
+            default = [ "*" ];
+            description = ''
+              A list of valid fully-qualified domain names (FQDNs) and/or IP
+              addresses that can be used to reach the Froide-Govplan service.
+            '';
+          };
+        };
+      };
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    services.froide-govplan = {
+      settings = {
+        STATIC_ROOT = "${cfg.dataDir}/static";
+        DEBUG = false;
+        DATABASES.default = {
+          ENGINE = "django.contrib.gis.db.backends.postgis";
+          NAME = "govplan";
+          USER = "govplan";
+          HOST = "/run/postgresql";
+        };
+      };
+    };
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = [ "govplan" ];
+      ensureUsers = [
+        {
+          name = "govplan";
+          ensureDBOwnership = true;
+        }
+      ];
+      extensions = ps: with ps; [ postgis ];
+    };
+
+    services.nginx = {
+      enable = lib.mkDefault true;
+      virtualHosts."${cfg.hostName}".locations = {
+        "/".extraConfig = "proxy_pass http://unix:/run/froide-govplan/froide-govplan.socket;";
+        "/static/".alias = "${cfg.dataDir}/static/";
+      };
+      proxyTimeout = lib.mkDefault "120s";
+    };
+
+    systemd = {
+      services = {
+
+        postgresql.serviceConfig.ExecStartPost =
+          let
+            sqlFile = pkgs.writeText "immich-pgvectors-setup.sql" ''
+              CREATE EXTENSION IF NOT EXISTS postgis;
+            '';
+          in
+          [
+            ''
+              ${lib.getExe' config.services.postgresql.package "psql"} -d govplan -f "${sqlFile}"
+            ''
+          ];
+
+        froide-govplan = {
+          description = "Gouvernment planer Govplan";
+          serviceConfig = defaultServiceConfig // {
+            WorkingDirectory = cfg.dataDir;
+            StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/froide-govplan") "froide-govplan";
+            User = "govplan";
+            Group = "govplan";
+          };
+          after = [
+            "postgresql.service"
+            "network.target"
+            "systemd-tmpfiles-setup.service"
+          ];
+          wantedBy = [ "multi-user.target" ];
+          environment =
+            {
+              PYTHONPATH = pkg.pythonPath;
+              GDAL_LIBRARY_PATH = "${pkgs.gdal}/lib/libgdal.so";
+              GEOS_LIBRARY_PATH = "${pkgs.geos}/lib/libgeos_c.so";
+            }
+            // lib.optionalAttrs (cfg.secretKeyFile != null) {
+              SECRET_KEY_FILE = cfg.secretKeyFile;
+            };
+          preStart = ''
+            # Auto-migrate on first run or if the package has changed
+            versionFile="${cfg.dataDir}/src-version"
+            version=$(cat "$versionFile" 2>/dev/null || echo 0)
+
+            if [[ $version != ${pkg.version} ]]; then
+              ${lib.getExe pkg} migrate --no-input
+              ${lib.getExe pkg} collectstatic --no-input --clear
+              echo ${pkg.version} > "$versionFile"
+            fi
+          '';
+          script = ''
+            ${pkg.python.pkgs.uvicorn}/bin/uvicorn --uds /run/froide-govplan/froide-govplan.socket \
+              --app-dir ${pkg}/${pkg.python.sitePackages}/froide_govplan \
+              project.asgi:application
+          '';
+        };
+      };
+
+    };
+
+    systemd.tmpfiles.rules = [ "d /run/froide-govplan - govplan govplan - -" ];
+
+    environment.systemPackages = [ froide-govplan ];
+
+    users.users.govplan = {
path: nixos/modules/services/web-apps/froide-govplan.nix
user: login: drupol
body: Fair enough.
created_at: 2025-03-26T13:20:30Z
author_association: CONTRIBUTOR

PR comments:
user: login: onny
created_at: 2025-03-23T17:16:35Z
body: @drupol Thank you for reviewing :)
author_association: CONTRIBUTOR
user: login: onny
created_at: 2025-03-26T06:37:22Z
body: @drupol Is this ready to merge? :)
author_association: CONTRIBUTOR
user: login: onny
created_at: 2025-03-26T13:32:48Z
body: thank you @drupol 
author_association: CONTRIBUTOR