{ config, lib, pkgs, ... }: let cfg = config.services.gancio; settingsFormat = pkgs.formats.json { }; inherit (lib) mkEnableOption mkPackageOption mkOption types literalExpression mkIf optional mapAttrsToList concatStringsSep concatMapStringsSep getExe mkMerge mkDefault ; in { options.services.gancio = { enable = mkEnableOption "Gancio, a shared agenda for local communities"; package = mkPackageOption pkgs "gancio" { }; plugins = mkOption { type = with types; listOf package; default = [ ]; example = literalExpression "[ pkgs.gancioPlugins.telegram-bridge ]"; description = '' Paths of gancio plugins to activate (linked under $WorkingDirectory/plugins/). ''; }; user = mkOption { type = types.str; description = "The user (and PostgreSQL database name) used to run the gancio server"; default = "gancio"; }; settings = mkOption rec { type = types.submodule { freeformType = settingsFormat.type; options = { hostname = mkOption { type = types.str; description = "The domain name under which the server is reachable."; }; baseurl = mkOption { type = types.str; default = "http${ lib.optionalString config.services.nginx.virtualHosts."${cfg.settings.hostname}".enableACME "s" }://${cfg.settings.hostname}"; defaultText = lib.literalExpression ''"https://''${cfg.settings.hostname}"''; example = "https://demo.gancio.org/gancio"; description = "The full URL under which the server is reachable."; }; server = { socket = mkOption { type = types.path; readOnly = true; default = "/run/gancio/socket"; description = '' The unix socket for the gancio server to listen on. ''; }; }; db = { dialect = mkOption { type = types.enum [ "sqlite" "postgres" ]; default = "sqlite"; description = '' The database dialect to use ''; }; storage = mkOption { description = '' Location for the SQLite database. ''; readOnly = true; type = types.nullOr types.str; default = if cfg.settings.db.dialect == "sqlite" then "/var/lib/gancio/db.sqlite" else null; defaultText = '' if cfg.settings.db.dialect == "sqlite" then "/var/lib/gancio/db.sqlite" else null ''; }; host = mkOption { description = '' Connection string for the PostgreSQL database ''; readOnly = true; type = types.nullOr types.str; default = if cfg.settings.db.dialect == "postgres" then "/run/postgresql" else null; defaultText = '' if cfg.settings.db.dialect == "postgres" then "/run/postgresql" else null ''; }; database = mkOption { description = '' Name of the PostgreSQL database ''; readOnly = true; type = types.nullOr types.str; default = if cfg.settings.db.dialect == "postgres" then cfg.user else null; defaultText = '' if cfg.settings.db.dialect == "postgres" then cfg.user else null ''; }; }; log_level = mkOption { description = "Gancio log level."; type = types.enum [ "debug" "info" "warning" "error" ]; default = "info"; }; # FIXME upstream proper journald logging log_path = mkOption { description = "Directory Gancio logs into"; readOnly = true; type = types.str; default = "/var/log/gancio"; }; }; }; description = '' Configuration for Gancio, see for supported values. ''; }; userLocale = mkOption { type = with types; attrsOf (attrsOf (attrsOf str)); default = { }; example = { en.register.description = "My new registration page description"; }; description = '' Override default locales within gancio. See [https://framagit.org/les/gancio/tree/master/locales](default languages and locales). ''; }; nginx = mkOption { type = types.submodule ( lib.recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) { # enable encryption by default, # as sensitive login credentials should not be transmitted in clear text. options.forceSSL.default = true; options.enableACME.default = true; } ); default = { }; example = { enableACME = false; forceSSL = false; }; description = "Extra configuration for the nginx virtual host of gancio."; }; }; config = mkIf cfg.enable { environment.systemPackages = [ cfg.package ]; users.users.gancio = lib.mkIf (cfg.user == "gancio") { isSystemUser = true; group = cfg.user; home = "/var/lib/gancio"; }; users.groups.gancio = lib.mkIf (cfg.user == "gancio") { }; systemd.tmpfiles.settings."10-gancio" = let rules = { mode = "0755"; user = cfg.user; group = config.users.users.${cfg.user}.group; }; in { "/var/lib/gancio/user_locale".d = rules; "/var/lib/gancio/plugins".d = rules; }; systemd.services.gancio = let configFile = settingsFormat.generate "gancio-config.json" cfg.settings; in { description = "Gancio server"; documentation = [ "https://gancio.org/" ]; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ] ++ optional (cfg.settings.db.dialect == "postgres") "postgresql.service"; environment = { NODE_ENV = "production"; }; preStart = '' # We need this so the gancio executable run by the user finds the right settings. ln -sf ${configFile} config.json rm -f user_locale/* ${concatStringsSep "\n" ( mapAttrsToList ( l: c: "ln -sf ${settingsFormat.generate "gancio-${l}-locale.json" c} user_locale/${l}.json" ) cfg.userLocale )} rm -f plugins/* ${concatMapStringsSep "\n" (p: "ln -sf ${p} plugins/") cfg.plugins} ''; serviceConfig = { ExecStart = "${getExe cfg.package} start ${configFile}"; # set umask so that nginx can write to the server socket # FIXME: upstream socket permission configuration in Nuxt UMask = "0002"; RuntimeDirectory = "gancio"; StateDirectory = "gancio"; WorkingDirectory = "/var/lib/gancio"; LogsDirectory = "gancio"; User = cfg.user; # hardening RestrictRealtime = true; RestrictNamespaces = true; LockPersonality = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectKernelLogs = true; ProtectControlGroups = true; ProtectClock = true; RestrictSUIDSGID = true; SystemCallArchitectures = "native"; CapabilityBoundingSet = ""; ProtectProc = "invisible"; }; }; services.postgresql = mkIf (cfg.settings.db.dialect == "postgres") { enable = true; ensureDatabases = [ cfg.user ]; ensureUsers = [ { name = cfg.user; ensureDBOwnership = true; } ]; }; services.nginx = { enable = true; virtualHosts."${cfg.settings.hostname}" = mkMerge [ cfg.nginx { locations = { "/" = { index = "index.html"; tryFiles = "$uri $uri @proxy"; }; "@proxy" = { proxyWebsockets = true; proxyPass = "http://unix:${cfg.settings.server.socket}"; recommendedProxySettings = true; }; }; } ]; }; # for nginx to access gancio socket users.users."${config.services.nginx.user}".extraGroups = [ config.users.users.${cfg.user}.group ]; }; }