grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / tt-rss.nix
blob9826febb3c66fc434c87280908d36bac193902a5
1 { config, lib, pkgs, ... }:
3 with lib;
4 let
5   cfg = config.services.tt-rss;
7   inherit (cfg) phpPackage;
9   configVersion = 26;
11   dbPort = if cfg.database.port == null
12     then (if cfg.database.type == "pgsql" then 5432 else 3306)
13     else cfg.database.port;
15   poolName = "tt-rss";
17   mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
18   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
20   tt-rss-config = let
21     password =
22       if (cfg.database.password != null) then
23         "'${(escape ["'" "\\"] cfg.database.password)}'"
24       else if (cfg.database.passwordFile != null) then
25         "file_get_contents('${cfg.database.passwordFile}')"
26       else
27         null
28       ;
29   in pkgs.writeText "config.php" ''
30     <?php
31       putenv('TTRSS_PHP_EXECUTABLE=${phpPackage}/bin/php');
33       putenv('TTRSS_LOCK_DIRECTORY=${cfg.root}/lock');
34       putenv('TTRSS_CACHE_DIR=${cfg.root}/cache');
35       putenv('TTRSS_ICONS_DIR=${cfg.root}/feed-icons');
36       putenv('TTRSS_ICONS_URL=feed-icons');
37       putenv('TTRSS_SELF_URL_PATH=${cfg.selfUrlPath}');
39       putenv('TTRSS_MYSQL_CHARSET=UTF8');
41       putenv('TTRSS_DB_TYPE=${cfg.database.type}');
42       putenv('TTRSS_DB_HOST=${optionalString (cfg.database.host != null) cfg.database.host}');
43       putenv('TTRSS_DB_USER=${cfg.database.user}');
44       putenv('TTRSS_DB_NAME=${cfg.database.name}');
45       putenv('TTRSS_DB_PASS=' ${optionalString (password != null) ". ${password}"});
46       putenv('TTRSS_DB_PORT=${toString dbPort}');
48       putenv('TTRSS_AUTH_AUTO_CREATE=${boolToString cfg.auth.autoCreate}');
49       putenv('TTRSS_AUTH_AUTO_LOGIN=${boolToString cfg.auth.autoLogin}');
51       putenv('TTRSS_FEED_CRYPT_KEY=${escape ["'" "\\"] cfg.feedCryptKey}');
54       putenv('TTRSS_SINGLE_USER_MODE=${boolToString cfg.singleUserMode}');
56       putenv('TTRSS_SIMPLE_UPDATE_MODE=${boolToString cfg.simpleUpdateMode}');
58       # Never check for updates - the running version of the code should
59       # be controlled entirely by the version of TT-RSS active in the
60       # current Nix profile. If TT-RSS updates itself to a version
61       # requiring a database schema upgrade, and then the SystemD
62       # tt-rss.service is restarted, the old code copied from the Nix
63       # store will overwrite the updated version, causing the code to
64       # detect the need for a schema "upgrade" (since the schema version
65       # in the database is different than in the code), but the update
66       # schema operation in TT-RSS will do nothing because the schema
67       # version in the database is newer than that in the code.
68       putenv('TTRSS_CHECK_FOR_UPDATES=false');
70       putenv('TTRSS_FORCE_ARTICLE_PURGE=${toString cfg.forceArticlePurge}');
71       putenv('TTRSS_SESSION_COOKIE_LIFETIME=${toString cfg.sessionCookieLifetime}');
72       putenv('TTRSS_ENABLE_GZIP_OUTPUT=${boolToString cfg.enableGZipOutput}');
74       putenv('TTRSS_PLUGINS=${builtins.concatStringsSep "," cfg.plugins}');
76       putenv('TTRSS_LOG_DESTINATION=${cfg.logDestination}');
77       putenv('TTRSS_CONFIG_VERSION=${toString configVersion}');
80       putenv('TTRSS_PUBSUBHUBBUB_ENABLED=${boolToString cfg.pubSubHubbub.enable}');
81       putenv('TTRSS_PUBSUBHUBBUB_HUB=${cfg.pubSubHubbub.hub}');
83       putenv('TTRSS_SPHINX_SERVER=${cfg.sphinx.server}');
84       putenv('TTRSS_SPHINX_INDEX=${builtins.concatStringsSep "," cfg.sphinx.index}');
86       putenv('TTRSS_ENABLE_REGISTRATION=${boolToString cfg.registration.enable}');
87       putenv('TTRSS_REG_NOTIFY_ADDRESS=${cfg.registration.notifyAddress}');
88       putenv('TTRSS_REG_MAX_USERS=${toString cfg.registration.maxUsers}');
90       putenv('TTRSS_SMTP_SERVER=${cfg.email.server}');
91       putenv('TTRSS_SMTP_LOGIN=${cfg.email.login}');
92       putenv('TTRSS_SMTP_PASSWORD=${escape ["'" "\\"] cfg.email.password}');
93       putenv('TTRSS_SMTP_SECURE=${cfg.email.security}');
95       putenv('TTRSS_SMTP_FROM_NAME=${escape ["'" "\\"] cfg.email.fromName}');
96       putenv('TTRSS_SMTP_FROM_ADDRESS=${escape ["'" "\\"] cfg.email.fromAddress}');
97       putenv('TTRSS_DIGEST_SUBJECT=${escape ["'" "\\"] cfg.email.digestSubject}');
99       ${cfg.extraConfig}
100   '';
102   # tt-rss and plugins and themes and config.php
103   servedRoot = pkgs.runCommand "tt-rss-served-root" {} ''
104     cp --no-preserve=mode -r ${pkgs.tt-rss} $out
105     cp ${tt-rss-config} $out/config.php
106     ${optionalString (cfg.pluginPackages != []) ''
107     for plugin in ${concatStringsSep " " cfg.pluginPackages}; do
108     cp -r "$plugin"/* "$out/plugins.local/"
109     done
110     ''}
111     ${optionalString (cfg.themePackages != []) ''
112     for theme in ${concatStringsSep " " cfg.themePackages}; do
113     cp -r "$theme"/* "$out/themes.local/"
114     done
115     ''}
116   '';
118  in {
120   ###### interface
122   options = {
124     services.tt-rss = {
126       enable = mkEnableOption "tt-rss";
128       root = mkOption {
129         type = types.path;
130         default = "/var/lib/tt-rss";
131         description = ''
132           Root of the application.
133         '';
134       };
136       user = mkOption {
137         type = types.str;
138         default = "tt_rss";
139         description = ''
140           User account under which both the update daemon and the web-application run.
141         '';
142       };
144       pool = mkOption {
145         type = types.str;
146         default = "${poolName}";
147         description = ''
148           Name of existing phpfpm pool that is used to run web-application.
149           If not specified a pool will be created automatically with
150           default values.
151         '';
152       };
154       virtualHost = mkOption {
155         type = types.nullOr types.str;
156         default = "tt-rss";
157         description = ''
158           Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
159         '';
160       };
162       database = {
163         type = mkOption {
164           type = types.enum ["pgsql" "mysql"];
165           default = "pgsql";
166           description = ''
167             Database to store feeds. Supported are pgsql and mysql.
168           '';
169         };
171         host = mkOption {
172           type = types.nullOr types.str;
173           default = null;
174           description = ''
175             Host of the database. Leave null to use Unix domain socket.
176           '';
177         };
179         name = mkOption {
180           type = types.str;
181           default = "tt_rss";
182           description = ''
183             Name of the existing database.
184           '';
185         };
187         user = mkOption {
188           type = types.str;
189           default = "tt_rss";
190           description = ''
191             The database user. The user must exist and has access to
192             the specified database.
193           '';
194         };
196         password = mkOption {
197           type = types.nullOr types.str;
198           default = null;
199           description = ''
200             The database user's password.
201           '';
202         };
204         passwordFile = mkOption {
205           type = types.nullOr types.str;
206           default = null;
207           description = ''
208             The database user's password.
209           '';
210         };
212         port = mkOption {
213           type = types.nullOr types.port;
214           default = null;
215           description = ''
216             The database's port. If not set, the default ports will be provided (5432
217             and 3306 for pgsql and mysql respectively).
218           '';
219         };
221         createLocally = mkOption {
222           type = types.bool;
223           default = true;
224           description = "Create the database and database user locally.";
225         };
226       };
228       auth = {
229         autoCreate = mkOption {
230           type = types.bool;
231           default = true;
232           description = ''
233             Allow authentication modules to auto-create users in tt-rss internal
234             database when authenticated successfully.
235           '';
236         };
238         autoLogin = mkOption {
239           type = types.bool;
240           default = true;
241           description = ''
242             Automatically login user on remote or other kind of externally supplied
243             authentication, otherwise redirect to login form as normal.
244             If set to true, users won't be able to set application language
245             and settings profile.
246           '';
247         };
248       };
250       pubSubHubbub = {
251         hub = mkOption {
252           type = types.str;
253           default = "";
254           description = ''
255             URL to a PubSubHubbub-compatible hub server. If defined, "Published
256             articles" generated feed would automatically become PUSH-enabled.
257           '';
258         };
260         enable = mkOption {
261           type = types.bool;
262           default = false;
263           description = ''
264             Enable client PubSubHubbub support in tt-rss. When disabled, tt-rss
265             won't try to subscribe to PUSH feed updates.
266           '';
267         };
268       };
270       sphinx = {
271         server = mkOption {
272           type = types.str;
273           default = "localhost:9312";
274           description = ''
275             Hostname:port combination for the Sphinx server.
276           '';
277         };
279         index = mkOption {
280           type = types.listOf types.str;
281           default = ["ttrss" "delta"];
282           description = ''
283             Index names in Sphinx configuration. Example configuration
284             files are available on tt-rss wiki.
285           '';
286         };
287       };
289       registration = {
290         enable = mkOption {
291           type = types.bool;
292           default = false;
293           description = ''
294             Allow users to register themselves. Please be aware that allowing
295             random people to access your tt-rss installation is a security risk
296             and potentially might lead to data loss or server exploit. Disabled
297             by default.
298           '';
299         };
301         notifyAddress = mkOption {
302           type = types.str;
303           default = "";
304           description = ''
305             Email address to send new user notifications to.
306           '';
307         };
309         maxUsers = mkOption {
310           type = types.int;
311           default = 0;
312           description = ''
313             Maximum amount of users which will be allowed to register on this
314             system. 0 - no limit.
315           '';
316         };
317       };
319       email = {
320         server = mkOption {
321           type = types.str;
322           default = "";
323           example = "localhost:25";
324           description = ''
325             Hostname:port combination to send outgoing mail. Blank - use system
326             MTA.
327           '';
328         };
330         login = mkOption {
331           type = types.str;
332           default = "";
333           description = ''
334             SMTP authentication login used when sending outgoing mail.
335           '';
336         };
338         password = mkOption {
339           type = types.str;
340           default = "";
341           description = ''
342             SMTP authentication password used when sending outgoing mail.
343           '';
344         };
346         security = mkOption {
347           type = types.enum ["" "ssl" "tls"];
348           default = "";
349           description = ''
350             Used to select a secure SMTP connection. Allowed values: ssl, tls,
351             or empty.
352           '';
353         };
355         fromName = mkOption {
356           type = types.str;
357           default = "Tiny Tiny RSS";
358           description = ''
359             Name for sending outgoing mail. This applies to password reset
360             notifications, digest emails and any other mail.
361           '';
362         };
364         fromAddress = mkOption {
365           type = types.str;
366           default = "";
367           description = ''
368             Address for sending outgoing mail. This applies to password reset
369             notifications, digest emails and any other mail.
370           '';
371         };
373         digestSubject = mkOption {
374           type = types.str;
375           default = "[tt-rss] New headlines for last 24 hours";
376           description = ''
377             Subject line for email digests.
378           '';
379         };
380       };
382       sessionCookieLifetime = mkOption {
383         type = types.int;
384         default = 86400;
385         description = ''
386           Default lifetime of a session (e.g. login) cookie. In seconds,
387           0 means cookie will be deleted when browser closes.
388         '';
389       };
391       selfUrlPath = mkOption {
392         type = types.str;
393         description = ''
394           Full URL of your tt-rss installation. This should be set to the
395           location of tt-rss directory, e.g. http://example.org/tt-rss/
396           You need to set this option correctly otherwise several features
397           including PUSH, bookmarklets and browser integration will not work properly.
398         '';
399         example = "http://localhost";
400       };
402       feedCryptKey = mkOption {
403         type = types.str;
404         default = "";
405         description = ''
406           Key used for encryption of passwords for password-protected feeds
407           in the database. A string of 24 random characters. If left blank, encryption
408           is not used. Requires mcrypt functions.
409           Warning: changing this key will make your stored feed passwords impossible
410           to decrypt.
411         '';
412       };
414       singleUserMode = mkOption {
415         type = types.bool;
416         default = false;
418         description = ''
419           Operate in single user mode, disables all functionality related to
420           multiple users and authentication. Enabling this assumes you have
421           your tt-rss directory protected by other means (e.g. http auth).
422         '';
423       };
425       simpleUpdateMode = mkOption {
426         type = types.bool;
427         default = false;
428         description = ''
429           Enables fallback update mode where tt-rss tries to update feeds in
430           background while tt-rss is open in your browser.
431           If you don't have a lot of feeds and don't want to or can't run
432           background processes while not running tt-rss, this method is generally
433           viable to keep your feeds up to date.
434           Still, there are more robust (and recommended) updating methods
435           available, you can read about them here: <https://tt-rss.org/wiki/UpdatingFeeds>
436         '';
437       };
439       forceArticlePurge = mkOption {
440         type = types.int;
441         default = 0;
442         description = ''
443           When this option is not 0, users ability to control feed purging
444           intervals is disabled and all articles (which are not starred)
445           older than this amount of days are purged.
446         '';
447       };
449       enableGZipOutput = mkOption {
450         type = types.bool;
451         default = true;
452         description = ''
453           Selectively gzip output to improve wire performance. This requires
454           PHP Zlib extension on the server.
455           Enabling this can break tt-rss in several httpd/php configurations,
456           if you experience weird errors and tt-rss failing to start, blank pages
457           after login, or content encoding errors, disable it.
458         '';
459       };
461       phpPackage = lib.mkOption {
462         type = lib.types.package;
463         default = pkgs.php;
464         defaultText = "pkgs.php";
465         description = ''
466           php package to use for php fpm and update daemon.
467         '';
468       };
470       plugins = mkOption {
471         type = types.listOf types.str;
472         default = ["auth_internal" "note"];
473         description = ''
474           List of plugins to load automatically for all users.
475           System plugins have to be specified here. Please enable at least one
476           authentication plugin here (auth_*).
477           Users may enable other user plugins from Preferences/Plugins but may not
478           disable plugins specified in this list.
479           Disabling auth_internal in this list would automatically disable
480           reset password link on the login form.
481         '';
482       };
484       pluginPackages = mkOption {
485         type = types.listOf types.package;
486         default = [];
487         description = ''
488           List of plugins to install. The list elements are expected to
489           be derivations. All elements in this derivation are automatically
490           copied to the `plugins.local` directory.
491         '';
492       };
494       themePackages = mkOption {
495         type = types.listOf types.package;
496         default = [];
497         description = ''
498           List of themes to install. The list elements are expected to
499           be derivations. All elements in this derivation are automatically
500           copied to the `themes.local` directory.
501         '';
502       };
504       logDestination = mkOption {
505         type = types.enum ["" "sql" "syslog"];
506         default = "sql";
507         description = ''
508           Log destination to use. Possible values: sql (uses internal logging
509           you can read in Preferences -> System), syslog - logs to system log.
510           Setting this to blank uses PHP logging (usually to http server
511           error.log).
512         '';
513       };
515       extraConfig = mkOption {
516         type = types.lines;
517         default = "";
518         description = ''
519           Additional lines to append to `config.php`.
520         '';
521       };
522     };
523   };
525   imports = [
526     (mkRemovedOptionModule ["services" "tt-rss" "checkForUpdates"] ''
527       This option was removed because setting this to true will cause TT-RSS
528       to be unable to start if an automatic update of the code in
529       services.tt-rss.root leads to a database schema upgrade that is not
530       supported by the code active in the Nix store.
531     '')
532   ];
534   ###### implementation
536   config = mkIf cfg.enable {
538     assertions = [
539       {
540         assertion = cfg.database.password != null -> cfg.database.passwordFile == null;
541         message = "Cannot set both password and passwordFile";
542       }
543       {
544         assertion = cfg.database.createLocally -> cfg.database.name == cfg.user && cfg.database.user == cfg.user;
545         message = ''
546           When creating a database via NixOS, the db user and db name must be equal!
547           If you already have an existing DB+user and this assertion is new, you can safely set
548           `services.tt-rss.database.createLocally` to `false` because removal of `ensureUsers`
549           and `ensureDatabases` doesn't have any effect.
550         '';
551       }
552     ];
554     services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
555       ${poolName} = {
556         inherit (cfg) user;
557         inherit phpPackage;
558         settings = mapAttrs (name: mkDefault) {
559           "listen.owner" = "nginx";
560           "listen.group" = "nginx";
561           "listen.mode" = "0600";
562           "pm" = "dynamic";
563           "pm.max_children" = 75;
564           "pm.start_servers" = 10;
565           "pm.min_spare_servers" = 5;
566           "pm.max_spare_servers" = 20;
567           "pm.max_requests" = 500;
568           "catch_workers_output" = 1;
569         };
570       };
571     };
573     # NOTE: No configuration is done if not using virtual host
574     services.nginx = mkIf (cfg.virtualHost != null) {
575       enable = true;
576       virtualHosts = {
577         ${cfg.virtualHost} = {
578           root = "${cfg.root}/www";
580           locations."/" = {
581             index = "index.php";
582           };
584           locations."^~ /feed-icons" = {
585             root = "${cfg.root}";
586           };
588           locations."~ \\.php$" = {
589             extraConfig = ''
590               fastcgi_split_path_info ^(.+\.php)(/.+)$;
591               fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
592               fastcgi_index index.php;
593             '';
594           };
595         };
596       };
597     };
599     systemd.tmpfiles.rules = [
600       "d '${cfg.root}' 0555 ${cfg.user} tt_rss - -"
601       "d '${cfg.root}/lock' 0755 ${cfg.user} tt_rss - -"
602       "d '${cfg.root}/cache' 0755 ${cfg.user} tt_rss - -"
603       "d '${cfg.root}/cache/upload' 0755 ${cfg.user} tt_rss - -"
604       "d '${cfg.root}/cache/images' 0755 ${cfg.user} tt_rss - -"
605       "d '${cfg.root}/cache/export' 0755 ${cfg.user} tt_rss - -"
606       "d '${cfg.root}/feed-icons' 0755 ${cfg.user} tt_rss - -"
607       "L+ '${cfg.root}/www' - - - - ${servedRoot}"
608     ];
610     systemd.services = {
611       phpfpm-tt-rss = mkIf (cfg.pool == "${poolName}") {
612         restartTriggers = [ servedRoot ];
613       };
615       tt-rss = {
616         description = "Tiny Tiny RSS feeds update daemon";
618         preStart = ''
619           ${phpPackage}/bin/php ${cfg.root}/www/update.php --update-schema --force-yes
620         '';
622         serviceConfig = {
623           User = "${cfg.user}";
624           Group = "tt_rss";
625           ExecStart = "${phpPackage}/bin/php ${cfg.root}/www/update.php --daemon --quiet";
626           Restart = "on-failure";
627           RestartSec = "60";
628           SyslogIdentifier = "tt-rss";
629         };
631         wantedBy = [ "multi-user.target" ];
632         requires = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
633         after = [ "network.target" ] ++ optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
634       };
635     };
637     services.mysql = mkIf mysqlLocal {
638       enable = true;
639       package = mkDefault pkgs.mariadb;
640       ensureDatabases = [ cfg.database.name ];
641       ensureUsers = [
642         {
643           name = cfg.user;
644           ensurePermissions = {
645             "${cfg.database.name}.*" = "ALL PRIVILEGES";
646           };
647         }
648       ];
649     };
651     services.postgresql = mkIf pgsqlLocal {
652       enable = mkDefault true;
653       ensureDatabases = [ cfg.database.name ];
654       ensureUsers = [
655         { name = cfg.database.user;
656           ensureDBOwnership = true;
657         }
658       ];
659     };
661     users.users.tt_rss = optionalAttrs (cfg.user == "tt_rss") {
662       description = "tt-rss service user";
663       isSystemUser = true;
664       group = "tt_rss";
665     };
667     users.groups.tt_rss = {};
668   };