1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.gancio;
9 settingsFormat = pkgs.formats.json { };
10 inherit (lib)
11 mkEnableOption
12 mkPackageOption
13 mkOption
14 types
15 literalExpression
16 mkIf
17 optional
18 mapAttrsToList
19 concatStringsSep
20 concatMapStringsSep
21 getExe
22 mkMerge
23 mkDefault
24 ;
25in
26{
27 options.services.gancio = {
28 enable = mkEnableOption "Gancio, a shared agenda for local communities";
29
30 package = mkPackageOption pkgs "gancio" { };
31
32 plugins = mkOption {
33 type = with types; listOf package;
34 default = [ ];
35 example = literalExpression "[ pkgs.gancioPlugins.telegram-bridge ]";
36 description = ''
37 Paths of gancio plugins to activate (linked under $WorkingDirectory/plugins/).
38 '';
39 };
40
41 user = mkOption {
42 type = types.str;
43 description = "The user (and PostgreSQL database name) used to run the gancio server";
44 default = "gancio";
45 };
46
47 settings = mkOption rec {
48 type = types.submodule {
49 freeformType = settingsFormat.type;
50 options = {
51 hostname = mkOption {
52 type = types.str;
53 description = "The domain name under which the server is reachable.";
54 };
55 baseurl = mkOption {
56 type = types.str;
57 default = "http${
58 lib.optionalString config.services.nginx.virtualHosts."${cfg.settings.hostname}".enableACME "s"
59 }://${cfg.settings.hostname}";
60 defaultText = lib.literalExpression ''"https://''${config.services.gancio.settings.hostname}"'';
61 example = "https://demo.gancio.org/gancio";
62 description = "The full URL under which the server is reachable.";
63 };
64 server = {
65 socket = mkOption {
66 type = types.path;
67 readOnly = true;
68 default = "/run/gancio/socket";
69 description = ''
70 The unix socket for the gancio server to listen on.
71 '';
72 };
73 };
74 db = {
75 dialect = mkOption {
76 type = types.enum [
77 "sqlite"
78 "postgres"
79 ];
80 default = "sqlite";
81 description = ''
82 The database dialect to use
83 '';
84 };
85 storage = mkOption {
86 description = ''
87 Location for the SQLite database.
88 '';
89 readOnly = true;
90 type = types.nullOr types.str;
91 default = if cfg.settings.db.dialect == "sqlite" then "/var/lib/gancio/db.sqlite" else null;
92 defaultText = ''if config.services.gancio.settings.db.dialect == "sqlite" then "/var/lib/gancio/db.sqlite" else null'';
93 };
94 host = mkOption {
95 description = ''
96 Connection string for the PostgreSQL database
97 '';
98 readOnly = true;
99 type = types.nullOr types.str;
100 default = if cfg.settings.db.dialect == "postgres" then "/run/postgresql" else null;
101 defaultText = ''if config.services.gancio.settings.db.dialect == "postgres" then "/run/postgresql" else null'';
102 };
103 database = mkOption {
104 description = ''
105 Name of the PostgreSQL database
106 '';
107 readOnly = true;
108 type = types.nullOr types.str;
109 default = if cfg.settings.db.dialect == "postgres" then cfg.user else null;
110 defaultText = ''if config.services.gancio.settings.db.dialect == "postgres" then cfg.user else null'';
111 };
112 };
113 log_level = mkOption {
114 description = "Gancio log level.";
115 type = types.enum [
116 "debug"
117 "info"
118 "warning"
119 "error"
120 ];
121 default = "info";
122 };
123 # FIXME upstream proper journald logging
124 log_path = mkOption {
125 description = "Directory Gancio logs into";
126 readOnly = true;
127 type = types.str;
128 default = "/var/log/gancio";
129 };
130 };
131 };
132 description = ''
133 Configuration for Gancio, see <https://gancio.org/install/config> for supported values.
134 '';
135 };
136
137 userLocale = mkOption {
138 type = with types; attrsOf (attrsOf (attrsOf str));
139 default = { };
140 example = {
141 en.register.description = "My new registration page description";
142 };
143 description = ''
144 Override default locales within gancio.
145 See [default languages and locales](https://framagit.org/les/gancio/tree/master/locales).
146 '';
147 };
148
149 nginx = mkOption {
150 type = types.submodule (
151 lib.recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {
152 # enable encryption by default,
153 # as sensitive login credentials should not be transmitted in clear text.
154 options.forceSSL.default = true;
155 options.enableACME.default = true;
156 }
157 );
158 default = { };
159 example = {
160 enableACME = false;
161 forceSSL = false;
162 };
163 description = "Extra configuration for the nginx virtual host of gancio.";
164 };
165 };
166
167 config = mkIf cfg.enable {
168 environment.systemPackages = [
169 (pkgs.runCommand "gancio" { } ''
170 mkdir -p $out/bin
171 echo '#!${pkgs.runtimeShell}
172 cd /var/lib/gancio/
173 sudo=exec
174 if [[ "$USER" != ${cfg.user} ]]; then
175 sudo="exec /run/wrappers/bin/sudo -u ${cfg.user}"
176 fi
177 $sudo ${lib.getExe cfg.package} "''${@:--help}"
178 ' > $out/bin/gancio
179 chmod +x $out/bin/gancio
180 '')
181 ];
182
183 users.users.gancio = lib.mkIf (cfg.user == "gancio") {
184 isSystemUser = true;
185 group = cfg.user;
186 home = "/var/lib/gancio";
187 };
188 users.groups.gancio = lib.mkIf (cfg.user == "gancio") { };
189
190 systemd.tmpfiles.settings."10-gancio" =
191 let
192 rules = {
193 mode = "0755";
194 user = cfg.user;
195 group = config.users.users.${cfg.user}.group;
196 };
197 in
198 {
199 "/var/lib/gancio/user_locale".d = rules;
200 "/var/lib/gancio/plugins".d = rules;
201 };
202
203 systemd.services.gancio =
204 let
205 configFile = settingsFormat.generate "gancio-config.json" cfg.settings;
206 in
207 {
208 description = "Gancio server";
209 documentation = [ "https://gancio.org/" ];
210
211 wantedBy = [ "multi-user.target" ];
212 after = [
213 "network.target"
214 ] ++ optional (cfg.settings.db.dialect == "postgres") "postgresql.service";
215
216 environment = {
217 NODE_ENV = "production";
218 };
219
220 path = [
221 # required for sendmail
222 "/run/wrappers"
223 ];
224
225 preStart = ''
226 # We need this so the gancio executable run by the user finds the right settings.
227 ln -sf ${configFile} config.json
228
229 rm -f user_locale/*
230 ${concatStringsSep "\n" (
231 mapAttrsToList (
232 l: c: "ln -sf ${settingsFormat.generate "gancio-${l}-locale.json" c} user_locale/${l}.json"
233 ) cfg.userLocale
234 )}
235
236 rm -f plugins/*
237 ${concatMapStringsSep "\n" (p: "ln -sf ${p} plugins/") cfg.plugins}
238 '';
239
240 serviceConfig = {
241 ExecStart = "${getExe cfg.package} start ${configFile}";
242 # set umask so that nginx can write to the server socket
243 # FIXME: upstream socket permission configuration in Nuxt
244 UMask = "0002";
245 RuntimeDirectory = "gancio";
246 StateDirectory = "gancio";
247 WorkingDirectory = "/var/lib/gancio";
248 LogsDirectory = "gancio";
249 User = cfg.user;
250 # hardening
251 RestrictRealtime = true;
252 RestrictNamespaces = true;
253 LockPersonality = true;
254 ProtectKernelModules = true;
255 ProtectKernelTunables = true;
256 ProtectKernelLogs = true;
257 ProtectControlGroups = true;
258 ProtectClock = true;
259 RestrictSUIDSGID = true;
260 SystemCallArchitectures = "native";
261 CapabilityBoundingSet = "";
262 ProtectProc = "invisible";
263 };
264 };
265
266 services.postgresql = mkIf (cfg.settings.db.dialect == "postgres") {
267 enable = true;
268 ensureDatabases = [ cfg.user ];
269 ensureUsers = [
270 {
271 name = cfg.user;
272 ensureDBOwnership = true;
273 }
274 ];
275 };
276
277 services.nginx = {
278 enable = true;
279 virtualHosts."${cfg.settings.hostname}" = mkMerge [
280 cfg.nginx
281 {
282 locations = {
283 "/" = {
284 index = "index.html";
285 tryFiles = "$uri $uri @proxy";
286 };
287 "@proxy" = {
288 proxyWebsockets = true;
289 proxyPass = "http://unix:${cfg.settings.server.socket}";
290 recommendedProxySettings = true;
291 };
292 };
293 }
294 ];
295 };
296 # for nginx to access gancio socket
297 users.users."${config.services.nginx.user}" = lib.mkIf (config.services.nginx.enable) {
298 extraGroups = [ config.users.users.${cfg.user}.group ];
299 };
300 };
301}