2023-09-05 15:53:09 +00:00
|
|
|
{ pkgs, lib, config, ... }:
|
|
|
|
|
|
|
|
with lib;
|
|
|
|
|
|
|
|
let
|
|
|
|
cfg = config.services.mobilizon;
|
|
|
|
|
|
|
|
user = "mobilizon";
|
|
|
|
group = "mobilizon";
|
|
|
|
|
2023-10-29 21:03:06 +00:00
|
|
|
settingsFormat = pkgs.formats.elixirConf { elixir = cfg.package.elixirPackage; };
|
2023-09-05 15:53:09 +00:00
|
|
|
|
|
|
|
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
|
2023-09-13 12:57:03 +00:00
|
|
|
(lib.mdDoc "Mobilizon federated organization and mobilization platform");
|
2023-09-05 15:53:09 +00:00
|
|
|
|
|
|
|
nginx.enable = lib.mkOption {
|
|
|
|
type = lib.types.bool;
|
|
|
|
default = true;
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether an Nginx virtual host should be
|
2023-09-05 15:53:09 +00:00
|
|
|
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;
|
2023-09-13 12:57:03 +00:00
|
|
|
defaultText = lib.literalMD ''
|
2023-09-05 15:53:09 +00:00
|
|
|
''${settings.":mobilizon".":instance".hostname}
|
|
|
|
'';
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-09-05 15:53:09 +00:00
|
|
|
Your instance's hostname for generating URLs throughout the app
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
http = {
|
|
|
|
port = mkOption {
|
|
|
|
type = elixirTypes.port;
|
|
|
|
default = 4000;
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-09-05 15:53:09 +00:00
|
|
|
The port to run the server
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
ip = mkOption {
|
|
|
|
type = elixirTypes.tuple;
|
|
|
|
default = settingsFormat.lib.mkTuple [ 0 0 0 0 0 0 0 1 ];
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-09-05 15:53:09 +00:00
|
|
|
The IP address to listen on. Defaults to [::1] notated as a byte tuple.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
has_reverse_proxy = mkOption {
|
|
|
|
type = elixirTypes.bool;
|
|
|
|
default = true;
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-09-05 15:53:09 +00:00
|
|
|
Whether you use a reverse proxy
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
":instance" = {
|
|
|
|
name = mkOption {
|
|
|
|
type = elixirTypes.str;
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-09-05 15:53:09 +00:00
|
|
|
The fallback instance name if not configured into the admin UI
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
hostname = mkOption {
|
|
|
|
type = elixirTypes.str;
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-09-05 15:53:09 +00:00
|
|
|
Your instance's hostname
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
email_from = mkOption {
|
|
|
|
type = elixirTypes.str;
|
|
|
|
defaultText = literalExpression ''
|
|
|
|
noreply@''${settings.":mobilizon".":instance".hostname}
|
|
|
|
'';
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-09-05 15:53:09 +00:00
|
|
|
The email for the From: header in emails
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
email_reply_to = mkOption {
|
|
|
|
type = elixirTypes.str;
|
|
|
|
defaultText = literalExpression ''
|
|
|
|
''${email_from}
|
|
|
|
'';
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-09-05 15:53:09 +00:00
|
|
|
The email for the Reply-To: header in emails
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
"Mobilizon.Storage.Repo" = {
|
|
|
|
socket_dir = mkOption {
|
|
|
|
type = types.nullOr elixirTypes.str;
|
|
|
|
default = postgresqlSocketDir;
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-09-05 15:53:09 +00:00
|
|
|
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:
|
2023-09-13 12:57:03 +00:00
|
|
|
<https://docs.joinmobilizon.org/administration/install/release/#database-setup>
|
2023-09-05 15:53:09 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
username = mkOption {
|
|
|
|
type = types.nullOr elixirTypes.str;
|
|
|
|
default = user;
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-09-05 15:53:09 +00:00
|
|
|
User used to connect to the database
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
database = mkOption {
|
|
|
|
type = types.nullOr elixirTypes.str;
|
|
|
|
default = "mobilizon_prod";
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-09-05 15:53:09 +00:00
|
|
|
Name of the database
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
default = { };
|
|
|
|
|
2023-09-13 12:57:03 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-09-05 15:53:09 +00:00
|
|
|
Mobilizon Elixir documentation, see
|
2023-09-13 12:57:03 +00:00
|
|
|
<https://docs.joinmobilizon.org/administration/configure/reference/>
|
2023-09-05 15:53:09 +00:00
|
|
|
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: ''
|
2023-10-29 21:03:06 +00:00
|
|
|
${cfg.package.elixirPackage}/bin/elixir --eval '${str}'
|
2023-09-05 15:53:09 +00:00
|
|
|
'';
|
|
|
|
in
|
|
|
|
''
|
|
|
|
set -euxo pipefail
|
|
|
|
|
|
|
|
if [ ! -f "${secretEnvFile}" ]; then
|
|
|
|
install -m 600 /dev/null "${secretEnvFile}"
|
|
|
|
cat > "${secretEnvFile}" <<EOF
|
|
|
|
# This file was automatically generated by mobilizon-setup-secrets.service
|
|
|
|
export MOBILIZON_AUTH_SECRET='$(${evalElixir genSecret})'
|
|
|
|
export MOBILIZON_INSTANCE_SECRET='$(${evalElixir genSecret})'
|
|
|
|
export RELEASE_COOKIE='$(${evalElixir genCookie})'
|
|
|
|
EOF
|
|
|
|
fi
|
|
|
|
'';
|
|
|
|
|
|
|
|
serviceConfig = {
|
|
|
|
Type = "oneshot";
|
|
|
|
User = user;
|
|
|
|
Group = group;
|
|
|
|
StateDirectory = "mobilizon";
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
# Add the required PostgreSQL extensions to the local PostgreSQL server,
|
|
|
|
# if local PostgreSQL is configured.
|
|
|
|
systemd.services.mobilizon-postgresql = mkIf isLocalPostgres {
|
|
|
|
description = "Mobilizon PostgreSQL setup";
|
|
|
|
|
|
|
|
after = [ "postgresql.service" ];
|
|
|
|
before = [ "mobilizon.service" "mobilizon-setup-secrets.service" ];
|
|
|
|
wantedBy = [ "mobilizon.service" ];
|
|
|
|
|
|
|
|
path = [ postgresql ];
|
|
|
|
|
|
|
|
# Taken from here:
|
|
|
|
# https://framagit.org/framasoft/mobilizon/-/blob/1.1.0/priv/templates/setup_db.eex
|
2023-11-17 14:19:14 +00:00
|
|
|
# TODO(to maintainers of mobilizon): the owner database alteration is necessary
|
|
|
|
# as PostgreSQL 15 changed their behaviors w.r.t. to privileges.
|
|
|
|
# See https://github.com/NixOS/nixpkgs/issues/216989 to get rid
|
|
|
|
# of that workaround.
|
2023-09-05 15:53:09 +00:00
|
|
|
script =
|
|
|
|
''
|
|
|
|
psql "${repoSettings.database}" -c "\
|
|
|
|
CREATE EXTENSION IF NOT EXISTS postgis; \
|
|
|
|
CREATE EXTENSION IF NOT EXISTS pg_trgm; \
|
|
|
|
CREATE EXTENSION IF NOT EXISTS unaccent;"
|
2023-11-17 14:19:14 +00:00
|
|
|
psql -tAc 'ALTER DATABASE "${repoSettings.database}" OWNER TO "${dbUser}";'
|
|
|
|
|
2023-09-05 15:53:09 +00:00
|
|
|
'';
|
|
|
|
|
|
|
|
serviceConfig = {
|
|
|
|
Type = "oneshot";
|
|
|
|
User = config.services.postgresql.superUser;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
systemd.tmpfiles.rules = [
|
|
|
|
"d /var/lib/mobilizon/uploads/exports/csv 700 mobilizon mobilizon - -"
|
|
|
|
"Z /var/lib/mobilizon 700 mobilizon mobilizon - -"
|
|
|
|
];
|
|
|
|
|
|
|
|
services.postgresql = mkIf isLocalPostgres {
|
|
|
|
enable = true;
|
|
|
|
ensureDatabases = [ repoSettings.database ];
|
|
|
|
ensureUsers = [
|
|
|
|
{
|
|
|
|
name = dbUser;
|
2023-11-17 14:19:14 +00:00
|
|
|
# Given that `dbUser` is potentially arbitrarily custom, we will perform
|
|
|
|
# manual fixups in mobilizon-postgres.
|
|
|
|
# TODO(to maintainers of mobilizon): Feel free to simplify your setup by using `ensureDBOwnership`.
|
|
|
|
ensureDBOwnership = false;
|
2023-09-05 15:53:09 +00:00
|
|
|
}
|
|
|
|
];
|
|
|
|
extraPlugins = with postgresql.pkgs; [ postgis ];
|
|
|
|
};
|
|
|
|
|
|
|
|
# Nginx config taken from support/nginx/mobilizon-release.conf
|
|
|
|
services.nginx =
|
|
|
|
let
|
|
|
|
inherit (cfg.settings.":mobilizon".":instance") hostname;
|
|
|
|
proxyPass = "http://[::1]:"
|
|
|
|
+ toString cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.port;
|
|
|
|
in
|
|
|
|
lib.mkIf cfg.nginx.enable {
|
|
|
|
enable = true;
|
|
|
|
virtualHosts."${hostname}" = {
|
|
|
|
enableACME = lib.mkDefault true;
|
|
|
|
forceSSL = lib.mkDefault true;
|
|
|
|
extraConfig = ''
|
|
|
|
proxy_http_version 1.1;
|
|
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
|
|
proxy_set_header Connection "upgrade";
|
|
|
|
proxy_set_header Host $host;
|
|
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
'';
|
|
|
|
locations."/" = {
|
|
|
|
inherit proxyPass;
|
|
|
|
};
|
|
|
|
locations."~ ^/(js|css|img)" = {
|
|
|
|
root = "${cfg.package}/lib/mobilizon-${cfg.package.version}/priv/static";
|
|
|
|
extraConfig = ''
|
|
|
|
etag off;
|
|
|
|
access_log off;
|
|
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
locations."~ ^/(media|proxy)" = {
|
|
|
|
inherit proxyPass;
|
|
|
|
extraConfig = ''
|
|
|
|
etag off;
|
|
|
|
access_log off;
|
|
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
users.users.${user} = {
|
|
|
|
description = "Mobilizon daemon user";
|
|
|
|
group = group;
|
|
|
|
isSystemUser = true;
|
|
|
|
};
|
|
|
|
|
|
|
|
users.groups.${group} = { };
|
|
|
|
|
|
|
|
# So that we have the `mobilizon` and `mobilizon_ctl` commands.
|
|
|
|
# The `mobilizon remote` command is useful for dropping a shell into the
|
|
|
|
# running Mobilizon instance, and `mobilizon_ctl` is used for common
|
|
|
|
# management tasks (e.g. adding users).
|
|
|
|
environment.systemPackages = [ launchers ];
|
|
|
|
};
|
|
|
|
|
|
|
|
meta.maintainers = with lib.maintainers; [ minijackson erictapen ];
|
|
|
|
}
|