dput-ng: fix eval (#364540)
[NixPkgs.git] / nixos / modules / services / web-apps / mobilizon.nix
blobd70c50d981dee9366643a791cb16066d0fece069
2   pkgs,
3   lib,
4   config,
5   ...
6 }:
8 with lib;
10 let
11   cfg = config.services.mobilizon;
13   user = "mobilizon";
14   group = "mobilizon";
16   settingsFormat = pkgs.formats.elixirConf { elixir = cfg.package.elixirPackage; };
18   configFile = settingsFormat.generate "mobilizon-config.exs" cfg.settings;
20   # Make a package containing launchers with the correct envirenment, instead of
21   # setting it with systemd services, so that the user can also use them without
22   # troubles
23   launchers = pkgs.stdenv.mkDerivation rec {
24     pname = "${cfg.package.pname}-launchers";
25     inherit (cfg.package) version;
27     src = cfg.package;
29     nativeBuildInputs = with pkgs; [ makeWrapper ];
31     dontBuild = true;
33     installPhase = ''
34       mkdir -p $out/bin
36       makeWrapper \
37         $src/bin/mobilizon \
38         $out/bin/mobilizon \
39         --run '. ${secretEnvFile}' \
40         --set MOBILIZON_CONFIG_PATH "${configFile}" \
41         --set-default RELEASE_TMP "/tmp"
43       makeWrapper \
44         $src/bin/mobilizon_ctl \
45         $out/bin/mobilizon_ctl \
46         --run '. ${secretEnvFile}' \
47         --set MOBILIZON_CONFIG_PATH "${configFile}" \
48         --set-default RELEASE_TMP "/tmp"
49     '';
50   };
52   repoSettings = cfg.settings.":mobilizon"."Mobilizon.Storage.Repo";
53   instanceSettings = cfg.settings.":mobilizon".":instance";
55   isLocalPostgres = repoSettings.socket_dir != null;
57   dbUser = if repoSettings.username != null then repoSettings.username else "mobilizon";
59   postgresql = config.services.postgresql.package;
60   postgresqlSocketDir = "/run/postgresql";
62   secretEnvFile = "/var/lib/mobilizon/secret-env.sh";
65   options = {
66     services.mobilizon = {
67       enable = mkEnableOption "Mobilizon federated organization and mobilization platform";
69       nginx.enable = lib.mkOption {
70         type = lib.types.bool;
71         default = true;
72         description = ''
73           Whether an Nginx virtual host should be
74           set up to serve Mobilizon.
75         '';
76       };
78       package = mkPackageOption pkgs "mobilizon" { };
80       settings = mkOption {
81         type =
82           let
83             elixirTypes = settingsFormat.lib.types;
84           in
85           types.submodule {
86             freeformType = settingsFormat.type;
88             options = {
89               ":mobilizon" = {
91                 "Mobilizon.Web.Endpoint" = {
92                   url.host = mkOption {
93                     type = elixirTypes.str;
94                     defaultText = lib.literalMD ''
95                       ''${settings.":mobilizon".":instance".hostname}
96                     '';
97                     description = ''
98                       Your instance's hostname for generating URLs throughout the app
99                     '';
100                   };
102                   http = {
103                     port = mkOption {
104                       type = elixirTypes.port;
105                       default = 4000;
106                       description = ''
107                         The port to run the server
108                       '';
109                     };
110                     ip = mkOption {
111                       type = elixirTypes.tuple;
112                       default = settingsFormat.lib.mkTuple [
113                         0
114                         0
115                         0
116                         0
117                         0
118                         0
119                         0
120                         1
121                       ];
122                       description = ''
123                         The IP address to listen on. Defaults to [::1] notated as a byte tuple.
124                       '';
125                     };
126                   };
128                   has_reverse_proxy = mkOption {
129                     type = elixirTypes.bool;
130                     default = true;
131                     description = ''
132                       Whether you use a reverse proxy
133                     '';
134                   };
135                 };
137                 ":instance" = {
138                   name = mkOption {
139                     type = elixirTypes.str;
140                     description = ''
141                       The fallback instance name if not configured into the admin UI
142                     '';
143                   };
145                   hostname = mkOption {
146                     type = elixirTypes.str;
147                     description = ''
148                       Your instance's hostname
149                     '';
150                   };
152                   email_from = mkOption {
153                     type = elixirTypes.str;
154                     defaultText = literalExpression ''
155                       noreply@''${settings.":mobilizon".":instance".hostname}
156                     '';
157                     description = ''
158                       The email for the From: header in emails
159                     '';
160                   };
162                   email_reply_to = mkOption {
163                     type = elixirTypes.str;
164                     defaultText = literalExpression ''
165                       ''${email_from}
166                     '';
167                     description = ''
168                       The email for the Reply-To: header in emails
169                     '';
170                   };
171                 };
173                 "Mobilizon.Storage.Repo" = {
174                   socket_dir = mkOption {
175                     type = types.nullOr elixirTypes.str;
176                     default = postgresqlSocketDir;
177                     description = ''
178                       Path to the postgres socket directory.
180                       Set this to null if you want to connect to a remote database.
182                       If non-null, the local PostgreSQL server will be configured with
183                       the configured database, permissions, and required extensions.
185                       If connecting to a remote database, please follow the
186                       instructions on how to setup your database:
187                       <https://docs.joinmobilizon.org/administration/install/release/#database-setup>
188                     '';
189                   };
191                   username = mkOption {
192                     type = types.nullOr elixirTypes.str;
193                     default = user;
194                     description = ''
195                       User used to connect to the database
196                     '';
197                   };
199                   database = mkOption {
200                     type = types.nullOr elixirTypes.str;
201                     default = "mobilizon_prod";
202                     description = ''
203                       Name of the database
204                     '';
205                   };
206                 };
207               };
208             };
209           };
210         default = { };
212         description = ''
213           Mobilizon Elixir documentation, see
214           <https://docs.joinmobilizon.org/administration/configure/reference/>
215           for supported values.
216         '';
217       };
218     };
219   };
221   config = mkIf cfg.enable {
223     assertions = [
224       {
225         assertion =
226           cfg.nginx.enable
227           -> (
228             cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.ip == settingsFormat.lib.mkTuple [
229               0
230               0
231               0
232               0
233               0
234               0
235               0
236               1
237             ]
238           );
239         message = "Setting the IP mobilizon listens on is only possible when the nginx config is not used, as it is hardcoded there.";
240       }
241     ];
243     services.mobilizon.settings = {
244       ":mobilizon" = {
245         "Mobilizon.Web.Endpoint" = {
246           server = true;
247           url.host = mkDefault instanceSettings.hostname;
248           secret_key_base = settingsFormat.lib.mkGetEnv { envVariable = "MOBILIZON_INSTANCE_SECRET"; };
249         };
251         "Mobilizon.Web.Auth.Guardian".secret_key = settingsFormat.lib.mkGetEnv {
252           envVariable = "MOBILIZON_AUTH_SECRET";
253         };
255         ":instance" = {
256           registrations_open = mkDefault false;
257           demo = mkDefault false;
258           email_from = mkDefault "noreply@${instanceSettings.hostname}";
259           email_reply_to = mkDefault instanceSettings.email_from;
260         };
262         "Mobilizon.Storage.Repo" = {
263           # Forced by upstream since it uses PostgreSQL-specific extensions
264           adapter = settingsFormat.lib.mkAtom "Ecto.Adapters.Postgres";
265           pool_size = mkDefault 10;
266         };
267       };
269       ":tzdata".":data_dir" = "/var/lib/mobilizon/tzdata/";
270     };
272     # This somewhat follows upstream's systemd service here:
273     # https://framagit.org/framasoft/mobilizon/-/blob/master/support/systemd/mobilizon.service
274     systemd.services.mobilizon = {
275       description = "Mobilizon federated organization and mobilization platform";
277       wantedBy = [ "multi-user.target" ];
279       path = with pkgs; [
280         gawk
281         imagemagick
282         libwebp
283         file
285         # Optional:
286         gifsicle
287         jpegoptim
288         optipng
289         pngquant
290       ];
292       serviceConfig = {
293         ExecStartPre = "${launchers}/bin/mobilizon_ctl migrate";
294         ExecStart = "${launchers}/bin/mobilizon start";
295         ExecStop = "${launchers}/bin/mobilizon stop";
297         User = user;
298         Group = group;
300         StateDirectory = "mobilizon";
302         Restart = "on-failure";
304         PrivateTmp = true;
305         ProtectSystem = "full";
306         NoNewPrivileges = true;
308         ReadWritePaths = mkIf isLocalPostgres postgresqlSocketDir;
309       };
310     };
312     # Create the needed secrets before running Mobilizon, so that they are not
313     # in the nix store
314     #
315     # Since some of these tasks are quite common for Elixir projects (COOKIE for
316     # every BEAM project, Phoenix and Guardian are also quite common), this
317     # service could be abstracted in the future, and used by other Elixir
318     # projects.
319     systemd.services.mobilizon-setup-secrets = {
320       description = "Mobilizon setup secrets";
321       before = [ "mobilizon.service" ];
322       wantedBy = [ "mobilizon.service" ];
324       script =
325         let
326           # Taken from here:
327           # https://framagit.org/framasoft/mobilizon/-/blob/1.0.7/lib/mix/tasks/mobilizon/instance.ex#L132-133
328           genSecret =
329             "IO.puts(:crypto.strong_rand_bytes(64)" + "|> Base.encode64()" + "|> binary_part(0, 64))";
331           # Taken from here:
332           # https://github.com/elixir-lang/elixir/blob/v1.11.3/lib/mix/lib/mix/release.ex#L499
333           genCookie = "IO.puts(Base.encode32(:crypto.strong_rand_bytes(32)))";
335           evalElixir = str: ''
336             ${cfg.package.elixirPackage}/bin/elixir --eval '${str}'
337           '';
338         in
339         ''
340           set -euxo pipefail
342           if [ ! -f "${secretEnvFile}" ]; then
343             install -m 600 /dev/null "${secretEnvFile}"
344             cat > "${secretEnvFile}" <<EOF
345           # This file was automatically generated by mobilizon-setup-secrets.service
346           export MOBILIZON_AUTH_SECRET='$(${evalElixir genSecret})'
347           export MOBILIZON_INSTANCE_SECRET='$(${evalElixir genSecret})'
348           export RELEASE_COOKIE='$(${evalElixir genCookie})'
349           EOF
350           fi
351         '';
353       serviceConfig = {
354         Type = "oneshot";
355         User = user;
356         Group = group;
357         StateDirectory = "mobilizon";
358       };
359     };
361     # Add the required PostgreSQL extensions to the local PostgreSQL server,
362     # if local PostgreSQL is configured.
363     systemd.services.mobilizon-postgresql = mkIf isLocalPostgres {
364       description = "Mobilizon PostgreSQL setup";
366       after = [ "postgresql.service" ];
367       before = [
368         "mobilizon.service"
369         "mobilizon-setup-secrets.service"
370       ];
371       wantedBy = [ "mobilizon.service" ];
373       path = [ postgresql ];
375       # Taken from here:
376       # https://framagit.org/framasoft/mobilizon/-/blob/1.1.0/priv/templates/setup_db.eex
377       # TODO(to maintainers of mobilizon): the owner database alteration is necessary
378       # as PostgreSQL 15 changed their behaviors w.r.t. to privileges.
379       # See https://github.com/NixOS/nixpkgs/issues/216989 to get rid
380       # of that workaround.
381       script = ''
382         psql "${repoSettings.database}" -c "\
383           CREATE EXTENSION IF NOT EXISTS postgis; \
384           CREATE EXTENSION IF NOT EXISTS pg_trgm; \
385           CREATE EXTENSION IF NOT EXISTS unaccent;"
386         psql -tAc 'ALTER DATABASE "${repoSettings.database}" OWNER TO "${dbUser}";'
388       '';
390       serviceConfig = {
391         Type = "oneshot";
392         User = config.services.postgresql.superUser;
393       };
394     };
396     systemd.tmpfiles.rules = [
397       "d /var/lib/mobilizon/uploads/exports/csv 700 mobilizon mobilizon - -"
398       "Z /var/lib/mobilizon 700 mobilizon mobilizon - -"
399     ];
401     services.postgresql = mkIf isLocalPostgres {
402       enable = true;
403       ensureDatabases = [ repoSettings.database ];
404       ensureUsers = [
405         {
406           name = dbUser;
407           # Given that `dbUser` is potentially arbitrarily custom, we will perform
408           # manual fixups in mobilizon-postgres.
409           # TODO(to maintainers of mobilizon): Feel free to simplify your setup by using `ensureDBOwnership`.
410           ensureDBOwnership = false;
411         }
412       ];
413       extensions = ps: with ps; [ postgis ];
414     };
416     # Nginx config taken from support/nginx/mobilizon-release.conf
417     services.nginx =
418       let
419         inherit (cfg.settings.":mobilizon".":instance") hostname;
420         proxyPass = "http://[::1]:" + toString cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.port;
421       in
422       lib.mkIf cfg.nginx.enable {
423         enable = true;
424         virtualHosts."${hostname}" = {
425           enableACME = lib.mkDefault true;
426           forceSSL = lib.mkDefault true;
427           extraConfig = ''
428             proxy_http_version 1.1;
429             proxy_set_header Upgrade $http_upgrade;
430             proxy_set_header Connection "upgrade";
431             proxy_set_header Host $host;
432             proxy_set_header X-Real-IP $remote_addr;
433             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
434             proxy_set_header X-Forwarded-Proto $scheme;
435           '';
436           locations."/" = {
437             inherit proxyPass;
438           };
439           locations."~ ^/(js|css|img)" = {
440             root = "${cfg.package}/lib/mobilizon-${cfg.package.version}/priv/static";
441             extraConfig = ''
442               etag off;
443               access_log off;
444               add_header Cache-Control "public, max-age=31536000, immutable";
445             '';
446           };
447           locations."~ ^/(media|proxy)" = {
448             inherit proxyPass;
449             extraConfig = ''
450               etag off;
451               access_log off;
452               add_header Cache-Control "public, max-age=31536000, immutable";
453             '';
454           };
455         };
456       };
458     users.users.${user} = {
459       description = "Mobilizon daemon user";
460       group = group;
461       isSystemUser = true;
462     };
464     users.groups.${group} = { };
466     # So that we have the `mobilizon` and `mobilizon_ctl` commands.
467     # The `mobilizon remote` command is useful for dropping a shell into the
468     # running Mobilizon instance, and `mobilizon_ctl` is used for common
469     # management tasks (e.g. adding users).
470     environment.systemPackages = [ launchers ];
471   };
473   meta.maintainers = with lib.maintainers; [
474     minijackson
475     erictapen
476   ];