{ pkgs, lib, config, ... }: with lib; let cfg = config.services.mobilizon; user = "mobilizon"; group = "mobilizon"; settingsFormat = pkgs.formats.elixirConf { elixir = pkgs.elixir_1_14; }; configFile = settingsFormat.generate "mobilizon-config.exs" cfg.settings; # Make a package containing launchers with the correct envirenment, instead of # setting it with systemd services, so that the user can also use them without # troubles launchers = pkgs.stdenv.mkDerivation rec { pname = "${cfg.package.pname}-launchers"; inherit (cfg.package) version; src = cfg.package; nativeBuildInputs = with pkgs; [ makeWrapper ]; dontBuild = true; installPhase = '' mkdir -p $out/bin makeWrapper \ $src/bin/mobilizon \ $out/bin/mobilizon \ --run '. ${secretEnvFile}' \ --set MOBILIZON_CONFIG_PATH "${configFile}" \ --set-default RELEASE_TMP "/tmp" makeWrapper \ $src/bin/mobilizon_ctl \ $out/bin/mobilizon_ctl \ --run '. ${secretEnvFile}' \ --set MOBILIZON_CONFIG_PATH "${configFile}" \ --set-default RELEASE_TMP "/tmp" ''; }; repoSettings = cfg.settings.":mobilizon"."Mobilizon.Storage.Repo"; instanceSettings = cfg.settings.":mobilizon".":instance"; isLocalPostgres = repoSettings.socket_dir != null; dbUser = if repoSettings.username != null then repoSettings.username else "mobilizon"; postgresql = config.services.postgresql.package; postgresqlSocketDir = "/var/run/postgresql"; secretEnvFile = "/var/lib/mobilizon/secret-env.sh"; in { options = { services.mobilizon = { enable = mkEnableOption (lib.mdDoc "Mobilizon federated organization and mobilization platform"); nginx.enable = lib.mkOption { type = lib.types.bool; default = true; description = lib.mdDoc '' Whether an Nginx virtual host should be set up to serve Mobilizon. ''; }; package = mkPackageOptionMD pkgs "mobilizon" { }; settings = mkOption { type = let elixirTypes = settingsFormat.lib.types; in types.submodule { freeformType = settingsFormat.type; options = { ":mobilizon" = { "Mobilizon.Web.Endpoint" = { url.host = mkOption { type = elixirTypes.str; defaultText = lib.literalMD '' ''${settings.":mobilizon".":instance".hostname} ''; description = lib.mdDoc '' Your instance's hostname for generating URLs throughout the app ''; }; http = { port = mkOption { type = elixirTypes.port; default = 4000; description = lib.mdDoc '' The port to run the server ''; }; ip = mkOption { type = elixirTypes.tuple; default = settingsFormat.lib.mkTuple [ 0 0 0 0 0 0 0 1 ]; description = lib.mdDoc '' The IP address to listen on. Defaults to [::1] notated as a byte tuple. ''; }; }; has_reverse_proxy = mkOption { type = elixirTypes.bool; default = true; description = lib.mdDoc '' Whether you use a reverse proxy ''; }; }; ":instance" = { name = mkOption { type = elixirTypes.str; description = lib.mdDoc '' The fallback instance name if not configured into the admin UI ''; }; hostname = mkOption { type = elixirTypes.str; description = lib.mdDoc '' Your instance's hostname ''; }; email_from = mkOption { type = elixirTypes.str; defaultText = literalExpression '' noreply@''${settings.":mobilizon".":instance".hostname} ''; description = lib.mdDoc '' The email for the From: header in emails ''; }; email_reply_to = mkOption { type = elixirTypes.str; defaultText = literalExpression '' ''${email_from} ''; description = lib.mdDoc '' The email for the Reply-To: header in emails ''; }; }; "Mobilizon.Storage.Repo" = { socket_dir = mkOption { type = types.nullOr elixirTypes.str; default = postgresqlSocketDir; description = lib.mdDoc '' Path to the postgres socket directory. Set this to null if you want to connect to a remote database. If non-null, the local PostgreSQL server will be configured with the configured database, permissions, and required extensions. If connecting to a remote database, please follow the instructions on how to setup your database: ''; }; username = mkOption { type = types.nullOr elixirTypes.str; default = user; description = lib.mdDoc '' User used to connect to the database ''; }; database = mkOption { type = types.nullOr elixirTypes.str; default = "mobilizon_prod"; description = lib.mdDoc '' Name of the database ''; }; }; }; }; }; default = { }; description = lib.mdDoc '' Mobilizon Elixir documentation, see for supported values. ''; }; }; }; config = mkIf cfg.enable { assertions = [ { assertion = cfg.nginx.enable -> (cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.ip == settingsFormat.lib.mkTuple [ 0 0 0 0 0 0 0 1 ]); message = "Setting the IP mobilizon listens on is only possible when the nginx config is not used, as it is hardcoded there."; } ]; services.mobilizon.settings = { ":mobilizon" = { "Mobilizon.Web.Endpoint" = { server = true; url.host = mkDefault instanceSettings.hostname; secret_key_base = settingsFormat.lib.mkGetEnv { envVariable = "MOBILIZON_INSTANCE_SECRET"; }; }; "Mobilizon.Web.Auth.Guardian".secret_key = settingsFormat.lib.mkGetEnv { envVariable = "MOBILIZON_AUTH_SECRET"; }; ":instance" = { registrations_open = mkDefault false; demo = mkDefault false; email_from = mkDefault "noreply@${instanceSettings.hostname}"; email_reply_to = mkDefault instanceSettings.email_from; }; "Mobilizon.Storage.Repo" = { # Forced by upstream since it uses PostgreSQL-specific extensions adapter = settingsFormat.lib.mkAtom "Ecto.Adapters.Postgres"; pool_size = mkDefault 10; }; }; ":tzdata".":data_dir" = "/var/lib/mobilizon/tzdata/"; }; # This somewhat follows upstream's systemd service here: # https://framagit.org/framasoft/mobilizon/-/blob/master/support/systemd/mobilizon.service systemd.services.mobilizon = { description = "Mobilizon federated organization and mobilization platform"; wantedBy = [ "multi-user.target" ]; path = with pkgs; [ gawk imagemagick libwebp file # Optional: gifsicle jpegoptim optipng pngquant ]; serviceConfig = { ExecStartPre = "${launchers}/bin/mobilizon_ctl migrate"; ExecStart = "${launchers}/bin/mobilizon start"; ExecStop = "${launchers}/bin/mobilizon stop"; User = user; Group = group; StateDirectory = "mobilizon"; Restart = "on-failure"; PrivateTmp = true; ProtectSystem = "full"; NoNewPrivileges = true; ReadWritePaths = mkIf isLocalPostgres postgresqlSocketDir; }; }; # Create the needed secrets before running Mobilizon, so that they are not # in the nix store # # Since some of these tasks are quite common for Elixir projects (COOKIE for # every BEAM project, Phoenix and Guardian are also quite common), this # service could be abstracted in the future, and used by other Elixir # projects. systemd.services.mobilizon-setup-secrets = { description = "Mobilizon setup secrets"; before = [ "mobilizon.service" ]; wantedBy = [ "mobilizon.service" ]; script = let # Taken from here: # https://framagit.org/framasoft/mobilizon/-/blob/1.0.7/lib/mix/tasks/mobilizon/instance.ex#L132-133 genSecret = "IO.puts(:crypto.strong_rand_bytes(64)" + "|> Base.encode64()" + "|> binary_part(0, 64))"; # Taken from here: # https://github.com/elixir-lang/elixir/blob/v1.11.3/lib/mix/lib/mix/release.ex#L499 genCookie = "IO.puts(Base.encode32(:crypto.strong_rand_bytes(32)))"; evalElixir = str: '' ${pkgs.elixir_1_14}/bin/elixir --eval '${str}' ''; in '' set -euxo pipefail if [ ! -f "${secretEnvFile}" ]; then install -m 600 /dev/null "${secretEnvFile}" cat > "${secretEnvFile}" <