grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / outline.nix
blob4c1de579ecc5ddbad72dfcdcb41e9bd97e5aa702
1 { config, lib, pkgs, ...}:
3 let
4   defaultUser = "outline";
5   cfg = config.services.outline;
6   inherit (lib) mkRemovedOptionModule;
7 in
9   imports = [
10     (mkRemovedOptionModule [ "services" "outline" "sequelizeArguments" ] "Database migration are run agains configurated database by outline directly")
11   ];
12   # See here for a reference of all the options:
13   #   https://github.com/outline/outline/blob/v0.67.0/.env.sample
14   #   https://github.com/outline/outline/blob/v0.67.0/app.json
15   #   https://github.com/outline/outline/blob/v0.67.0/server/env.ts
16   #   https://github.com/outline/outline/blob/v0.67.0/shared/types.ts
17   # The order is kept the same here to make updating easier.
18   options.services.outline = {
19     enable = lib.mkEnableOption "outline";
21     package = lib.mkOption {
22       default = pkgs.outline;
23       defaultText = lib.literalExpression "pkgs.outline";
24       type = lib.types.package;
25       example = lib.literalExpression ''
26         pkgs.outline.overrideAttrs (super: {
27           # Ignore the domain part in emails that come from OIDC. This is might
28           # be helpful if you want multiple users with different email providers
29           # to still land in the same team. Note that this effectively makes
30           # Outline a single-team instance.
31           patchPhase = ${"''"}
32             sed -i 's/const domain = parts\.length && parts\[1\];/const domain = "example.com";/g' plugins/oidc/server/auth/oidc.ts
33           ${"''"};
34         })
35       '';
36       description = "Outline package to use.";
37     };
39     user = lib.mkOption {
40       type = lib.types.str;
41       default = defaultUser;
42       description = ''
43         User under which the service should run. If this is the default value,
44         the user will be created, with the specified group as the primary
45         group.
46       '';
47     };
49     group = lib.mkOption {
50       type = lib.types.str;
51       default = defaultUser;
52       description = ''
53         Group under which the service should run. If this is the default value,
54         the group will be created.
55       '';
56     };
58     #
59     # Required options
60     #
62     secretKeyFile = lib.mkOption {
63       type = lib.types.str;
64       default = "/var/lib/outline/secret_key";
65       description = ''
66         File path that contains the application secret key. It must be 32
67         bytes long and hex-encoded. If the file does not exist, a new key will
68         be generated and saved here.
69       '';
70     };
72     utilsSecretFile = lib.mkOption {
73       type = lib.types.str;
74       default = "/var/lib/outline/utils_secret";
75       description = ''
76         File path that contains the utility secret key. If the file does not
77         exist, a new key will be generated and saved here.
78       '';
79     };
81     databaseUrl = lib.mkOption {
82       type = lib.types.str;
83       default = "local";
84       description = ''
85         URI to use for the main PostgreSQL database. If this needs to include
86         credentials that shouldn't be world-readable in the Nix store, set an
87         environment file on the systemd service and override the
88         `DATABASE_URL` entry. Pass the string
89         `local` to setup a database on the local server.
90       '';
91     };
93     redisUrl = lib.mkOption {
94       type = lib.types.str;
95       default = "local";
96       description = ''
97         Connection to a redis server. If this needs to include credentials
98         that shouldn't be world-readable in the Nix store, set an environment
99         file on the systemd service and override the
100         `REDIS_URL` entry. Pass the string
101         `local` to setup a local Redis database.
102       '';
103     };
105     publicUrl = lib.mkOption {
106       type = lib.types.str;
107       default = "http://localhost:3000";
108       description = "The fully qualified, publicly accessible URL";
109     };
111     port = lib.mkOption {
112       type = lib.types.port;
113       default = 3000;
114       description = "Listening port.";
115     };
117     storage = lib.mkOption {
118       description = ''
119         To support uploading of images for avatars and document attachments an
120         s3-compatible storage can be provided. AWS S3 is recommended for
121         redundancy however if you want to keep all file storage local an
122         alternative such as [minio](https://github.com/minio/minio)
123         can be used.
124         Local filesystem storage can also be used.
126         A more detailed guide on setting up storage is available
127         [here](https://docs.getoutline.com/s/hosting/doc/file-storage-N4M0T6Ypu7).
128       '';
129       example = lib.literalExpression ''
130         {
131           accessKey = "...";
132           secretKeyFile = "/somewhere";
133           uploadBucketUrl = "https://minio.example.com";
134           uploadBucketName = "outline";
135           region = "us-east-1";
136         }
137       '';
138       type = lib.types.submodule {
139         options = {
140           storageType = lib.mkOption {
141             type = lib.types.enum [ "local" "s3" ];
142             description = "File storage type, it can be local or s3.";
143             default = "s3";
144           };
145           localRootDir = lib.mkOption {
146             type = lib.types.str;
147             description = ''
148               If `storageType` is `local`, this sets the parent directory
149               under which all attachments/images go.
150             '';
151             default = "/var/lib/outline/data";
152           };
153           accessKey = lib.mkOption {
154             type = lib.types.str;
155             description = "S3 access key.";
156           };
157           secretKeyFile = lib.mkOption {
158             type = lib.types.path;
159             description = "File path that contains the S3 secret key.";
160           };
161           region = lib.mkOption {
162             type = lib.types.str;
163             default = "xx-xxxx-x";
164             description = "AWS S3 region name.";
165           };
166           uploadBucketUrl = lib.mkOption {
167             type = lib.types.str;
168             description = ''
169               URL endpoint of an S3-compatible API where uploads should be
170               stored.
171             '';
172           };
173           uploadBucketName = lib.mkOption {
174             type = lib.types.str;
175             description = "Name of the bucket where uploads should be stored.";
176           };
177           uploadMaxSize = lib.mkOption {
178             type = lib.types.int;
179             default = 26214400;
180             description = "Maxmium file size for uploads.";
181           };
182           forcePathStyle = lib.mkOption {
183             type = lib.types.bool;
184             default = true;
185             description = "Force S3 path style.";
186           };
187           acl = lib.mkOption {
188             type = lib.types.str;
189             default = "private";
190             description = "ACL setting.";
191           };
192         };
193       };
194     };
196     #
197     # Authentication
198     #
200     slackAuthentication = lib.mkOption {
201       description = ''
202         To configure Slack auth, you'll need to create an Application at
203         https://api.slack.com/apps
205         When configuring the Client ID, add a redirect URL under "OAuth & Permissions"
206         to `https://[publicUrl]/auth/slack.callback`.
207       '';
208       default = null;
209       type = lib.types.nullOr (lib.types.submodule {
210         options = {
211           clientId = lib.mkOption {
212             type = lib.types.str;
213             description = "Authentication key.";
214           };
215           secretFile = lib.mkOption {
216             type = lib.types.str;
217             description = "File path containing the authentication secret.";
218           };
219         };
220       });
221     };
223     googleAuthentication = lib.mkOption {
224       description = ''
225         To configure Google auth, you'll need to create an OAuth Client ID at
226         https://console.cloud.google.com/apis/credentials
228         When configuring the Client ID, add an Authorized redirect URI to
229         `https://[publicUrl]/auth/google.callback`.
230       '';
231       default = null;
232       type = lib.types.nullOr (lib.types.submodule {
233         options = {
234           clientId = lib.mkOption {
235             type = lib.types.str;
236             description = "Authentication client identifier.";
237           };
238           clientSecretFile = lib.mkOption {
239             type = lib.types.str;
240             description = "File path containing the authentication secret.";
241           };
242         };
243       });
244     };
246     azureAuthentication = lib.mkOption {
247       description = ''
248         To configure Microsoft/Azure auth, you'll need to create an OAuth
249         Client. See
250         [the guide](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4)
251         for details on setting up your Azure App.
252       '';
253       default = null;
254       type = lib.types.nullOr (lib.types.submodule {
255         options = {
256           clientId = lib.mkOption {
257             type = lib.types.str;
258             description = "Authentication client identifier.";
259           };
260           clientSecretFile = lib.mkOption {
261             type = lib.types.str;
262             description = "File path containing the authentication secret.";
263           };
264           resourceAppId = lib.mkOption {
265             type = lib.types.str;
266             description = "Authentication application resource ID.";
267           };
268         };
269       });
270     };
272     oidcAuthentication = lib.mkOption {
273       description = ''
274         To configure generic OIDC auth, you'll need some kind of identity
275         provider. See the documentation for whichever IdP you use to fill out
276         all the fields. The redirect URL is
277         `https://[publicUrl]/auth/oidc.callback`.
278       '';
279       default = null;
280       type = lib.types.nullOr (lib.types.submodule {
281         options = {
282           clientId = lib.mkOption {
283             type = lib.types.str;
284             description = "Authentication client identifier.";
285           };
286           clientSecretFile = lib.mkOption {
287             type = lib.types.str;
288             description = "File path containing the authentication secret.";
289           };
290           authUrl = lib.mkOption {
291             type = lib.types.str;
292             description = "OIDC authentication URL endpoint.";
293           };
294           tokenUrl = lib.mkOption {
295             type = lib.types.str;
296             description = "OIDC token URL endpoint.";
297           };
298           userinfoUrl = lib.mkOption {
299             type = lib.types.str;
300             description = "OIDC userinfo URL endpoint.";
301           };
302           usernameClaim = lib.mkOption {
303             type = lib.types.str;
304             description = ''
305               Specify which claims to derive user information from. Supports any
306               valid JSON path with the JWT payload
307             '';
308             default = "preferred_username";
309           };
310           displayName = lib.mkOption {
311             type = lib.types.str;
312             description = "Display name for OIDC authentication.";
313             default = "OpenID";
314           };
315           scopes = lib.mkOption {
316             type = lib.types.listOf lib.types.str;
317             description = "OpenID authentication scopes.";
318             default = [ "openid" "profile" "email" ];
319           };
320         };
321       });
322     };
324     #
325     # Optional configuration
326     #
328     sslKeyFile = lib.mkOption {
329       type = lib.types.nullOr lib.types.str;
330       default = null;
331       description = ''
332         File path that contains the Base64-encoded private key for HTTPS
333         termination. This is only required if you do not use an external reverse
334         proxy. See
335         [the documentation](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4).
336       '';
337     };
338     sslCertFile = lib.mkOption {
339       type = lib.types.nullOr lib.types.str;
340       default = null;
341       description = ''
342         File path that contains the Base64-encoded certificate for HTTPS
343         termination. This is only required if you do not use an external reverse
344         proxy. See
345         [the documentation](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4).
346       '';
347     };
349     cdnUrl = lib.mkOption {
350       type = lib.types.str;
351       default = "";
352       description = ''
353         If using a Cloudfront/Cloudflare distribution or similar it can be set
354         using this option. This will cause paths to JavaScript files,
355         stylesheets and images to be updated to the hostname defined here. In
356         your CDN configuration the origin server should be set to public URL.
357       '';
358     };
360     forceHttps = lib.mkOption {
361       type = lib.types.bool;
362       default = true;
363       description = ''
364         Auto-redirect to HTTPS in production. The default is
365         `true` but you may set this to `false`
366         if you can be sure that SSL is terminated at an external loadbalancer.
367       '';
368     };
370     enableUpdateCheck = lib.mkOption {
371       type = lib.types.bool;
372       default = false;
373       description = ''
374         Have the installation check for updates by sending anonymized statistics
375         to the maintainers.
376       '';
377     };
379     concurrency = lib.mkOption {
380       type = lib.types.int;
381       default = 1;
382       description = ''
383         How many processes should be spawned. For a rough estimate, divide your
384         server's available memory by 512.
385       '';
386     };
388     maximumImportSize = lib.mkOption {
389       type = lib.types.int;
390       default = 5120000;
391       description = ''
392         The maximum size of document imports. Overriding this could be required
393         if you have especially large Word documents with embedded imagery.
394       '';
395     };
397     debugOutput = lib.mkOption {
398       type = lib.types.nullOr (lib.types.enum [ "http" ]);
399       default = null;
400       description = "Set this to `http` log HTTP requests.";
401     };
403     slackIntegration = lib.mkOption {
404       description = ''
405         For a complete Slack integration with search and posting to channels
406         this configuration is also needed. See here for details:
407         https://wiki.generaloutline.com/share/be25efd1-b3ef-4450-b8e5-c4a4fc11e02a
408       '';
409       default = null;
410       type = lib.types.nullOr (lib.types.submodule {
411         options = {
412           verificationTokenFile = lib.mkOption {
413             type = lib.types.str;
414             description = "File path containing the verification token.";
415           };
416           appId = lib.mkOption {
417             type = lib.types.str;
418             description = "Application ID.";
419           };
420           messageActions = lib.mkOption {
421             type = lib.types.bool;
422             default = true;
423             description = "Whether to enable message actions.";
424           };
425         };
426       });
427     };
429     googleAnalyticsId = lib.mkOption {
430       type = lib.types.nullOr lib.types.str;
431       default = null;
432       description = ''
433         Optionally enable Google Analytics to track page views in the knowledge
434         base.
435       '';
436     };
438     sentryDsn = lib.mkOption {
439       type = lib.types.nullOr lib.types.str;
440       default = null;
441       description = ''
442         Optionally enable [Sentry](https://sentry.io/) to
443         track errors and performance.
444       '';
445     };
447     sentryTunnel = lib.mkOption {
448       type = lib.types.nullOr lib.types.str;
449       default = null;
450       description = ''
451         Optionally add a
452         [Sentry proxy tunnel](https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option)
453         for bypassing ad blockers in the UI.
454       '';
455     };
457     logo = lib.mkOption {
458       type = lib.types.nullOr lib.types.str;
459       default = null;
460       description = ''
461         Custom logo displayed on the authentication screen. This will be scaled
462         to a height of 60px.
463       '';
464     };
466     smtp = lib.mkOption {
467       description = ''
468         To support sending outgoing transactional emails such as
469         "document updated" or "you've been invited" you'll need to provide
470         authentication for an SMTP server.
471       '';
472       default = null;
473       type = lib.types.nullOr (lib.types.submodule {
474         options = {
475           host = lib.mkOption {
476             type = lib.types.str;
477             description = "Host name or IP address of the SMTP server.";
478           };
479           port = lib.mkOption {
480             type = lib.types.port;
481             description = "TCP port of the SMTP server.";
482           };
483           username = lib.mkOption {
484             type = lib.types.str;
485             description = "Username to authenticate with.";
486           };
487           passwordFile = lib.mkOption {
488             type = lib.types.str;
489             description = ''
490               File path containing the password to authenticate with.
491             '';
492           };
493           fromEmail = lib.mkOption {
494             type = lib.types.str;
495             description = "Sender email in outgoing mail.";
496           };
497           replyEmail = lib.mkOption {
498             type = lib.types.str;
499             description = "Reply address in outgoing mail.";
500           };
501           tlsCiphers = lib.mkOption {
502             type = lib.types.str;
503             default = "";
504             description = "Override SMTP cipher configuration.";
505           };
506           secure = lib.mkOption {
507             type = lib.types.bool;
508             default = true;
509             description = "Use a secure SMTP connection.";
510           };
511         };
512       });
513     };
515     defaultLanguage = lib.mkOption {
516       type = lib.types.enum [
517          "da_DK"
518          "de_DE"
519          "en_US"
520          "es_ES"
521          "fa_IR"
522          "fr_FR"
523          "it_IT"
524          "ja_JP"
525          "ko_KR"
526          "nl_NL"
527          "pl_PL"
528          "pt_BR"
529          "pt_PT"
530          "ru_RU"
531          "sv_SE"
532          "th_TH"
533          "vi_VN"
534          "zh_CN"
535          "zh_TW"
536       ];
537       default = "en_US";
538       description = ''
539         The default interface language. See
540         [translate.getoutline.com](https://translate.getoutline.com/)
541         for a list of available language codes and their rough percentage
542         translated.
543       '';
544     };
546     rateLimiter.enable = lib.mkEnableOption "rate limiter for the application web server";
547     rateLimiter.requests = lib.mkOption {
548       type = lib.types.int;
549       default = 5000;
550       description = "Maximum number of requests in a throttling window.";
551     };
552     rateLimiter.durationWindow = lib.mkOption {
553       type = lib.types.int;
554       default = 60;
555       description = "Length of a throttling window.";
556     };
557   };
559   config = lib.mkIf cfg.enable {
560     users.users = lib.optionalAttrs (cfg.user == defaultUser) {
561       ${defaultUser} = {
562         isSystemUser = true;
563         group = cfg.group;
564       };
565     };
567     users.groups = lib.optionalAttrs (cfg.group == defaultUser) {
568       ${defaultUser} = { };
569     };
571     systemd.tmpfiles.rules = [
572       "f ${cfg.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -"
573       "f ${cfg.utilsSecretFile} 0600 ${cfg.user} ${cfg.group} -"
574       (if (cfg.storage.storageType == "s3") then
575         "f ${cfg.storage.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -"
576       else
577         "d ${cfg.storage.localRootDir} 0700 ${cfg.user} ${cfg.group} - -")
578     ];
580     services.postgresql = lib.mkIf (cfg.databaseUrl == "local") {
581       enable = true;
582       ensureUsers = [{
583         name = "outline";
584         ensureDBOwnership = true;
585       }];
586       ensureDatabases = [ "outline" ];
587     };
589     # Outline is unable to create the uuid-ossp extension when using postgresql 12, in later version this
590     # extension can be created without superuser permission. This services therefor this extension before
591     # outline starts and postgresql 12 is using on the host.
592     #
593     # Can be removed after postgresql 12 is dropped from nixos.
594     systemd.services.outline-postgresql =
595       let
596         pgsql = config.services.postgresql;
597       in
598         lib.mkIf (cfg.databaseUrl == "local" && pgsql.package == pkgs.postgresql_12) {
599           after = [ "postgresql.service" ];
600           bindsTo = [ "postgresql.service" ];
601           wantedBy = [ "outline.service" ];
602           partOf = [ "outline.service" ];
603           path = [
604             pgsql.package
605           ];
606           script = ''
607             set -o errexit -o pipefail -o nounset -o errtrace
608             shopt -s inherit_errexit
610             psql outline -tAc 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'
611           '';
613           serviceConfig = {
614             User = pgsql.superUser;
615             Type = "oneshot";
616             RemainAfterExit = true;
617           };
618         };
620     services.redis.servers.outline = lib.mkIf (cfg.redisUrl == "local") {
621       enable = true;
622       user = config.services.outline.user;
623       port = 0; # Disable the TCP listener
624     };
626     systemd.services.outline = let
627       localRedisUrl = "redis+unix:///run/redis-outline/redis.sock";
628       localPostgresqlUrl = "postgres://localhost/outline?host=/run/postgresql";
629     in {
630       description = "Outline wiki and knowledge base";
631       wantedBy = [ "multi-user.target" ];
632       after = [ "networking.target" ]
633         ++ lib.optional (cfg.databaseUrl == "local") "postgresql.service"
634         ++ lib.optional (cfg.redisUrl == "local") "redis-outline.service";
635       requires = lib.optional (cfg.databaseUrl == "local") "postgresql.service"
636         ++ lib.optional (cfg.redisUrl == "local") "redis-outline.service";
637       path = [
638         pkgs.openssl # Required by the preStart script
639       ];
642       environment = lib.mkMerge [
643         {
644           NODE_ENV = "production";
646           REDIS_URL = if cfg.redisUrl == "local" then localRedisUrl else cfg.redisUrl;
647           URL = cfg.publicUrl;
648           PORT = builtins.toString cfg.port;
650           CDN_URL = cfg.cdnUrl;
651           FORCE_HTTPS = builtins.toString cfg.forceHttps;
652           ENABLE_UPDATES = builtins.toString cfg.enableUpdateCheck;
653           WEB_CONCURRENCY = builtins.toString cfg.concurrency;
654           MAXIMUM_IMPORT_SIZE = builtins.toString cfg.maximumImportSize;
655           DEBUG = cfg.debugOutput;
656           GOOGLE_ANALYTICS_ID = lib.optionalString (cfg.googleAnalyticsId != null) cfg.googleAnalyticsId;
657           SENTRY_DSN = lib.optionalString (cfg.sentryDsn != null) cfg.sentryDsn;
658           SENTRY_TUNNEL = lib.optionalString (cfg.sentryTunnel != null) cfg.sentryTunnel;
659           TEAM_LOGO = lib.optionalString (cfg.logo != null) cfg.logo;
660           DEFAULT_LANGUAGE = cfg.defaultLanguage;
662           RATE_LIMITER_ENABLED = builtins.toString cfg.rateLimiter.enable;
663           RATE_LIMITER_REQUESTS = builtins.toString cfg.rateLimiter.requests;
664           RATE_LIMITER_DURATION_WINDOW = builtins.toString cfg.rateLimiter.durationWindow;
666           FILE_STORAGE = cfg.storage.storageType;
667           FILE_STORAGE_UPLOAD_MAX_SIZE = builtins.toString cfg.storage.uploadMaxSize;
668           FILE_STORAGE_LOCAL_ROOT_DIR = cfg.storage.localRootDir;
669         }
671         (lib.mkIf (cfg.storage.storageType == "s3") {
672           AWS_ACCESS_KEY_ID = cfg.storage.accessKey;
673           AWS_REGION = cfg.storage.region;
674           AWS_S3_UPLOAD_BUCKET_URL = cfg.storage.uploadBucketUrl;
675           AWS_S3_UPLOAD_BUCKET_NAME = cfg.storage.uploadBucketName;
676           AWS_S3_FORCE_PATH_STYLE = builtins.toString cfg.storage.forcePathStyle;
677           AWS_S3_ACL = cfg.storage.acl;
678         })
680         (lib.mkIf (cfg.slackAuthentication != null) {
681           SLACK_CLIENT_ID = cfg.slackAuthentication.clientId;
682         })
684         (lib.mkIf (cfg.googleAuthentication != null) {
685           GOOGLE_CLIENT_ID = cfg.googleAuthentication.clientId;
686         })
688         (lib.mkIf (cfg.azureAuthentication != null) {
689           AZURE_CLIENT_ID = cfg.azureAuthentication.clientId;
690           AZURE_RESOURCE_APP_ID = cfg.azureAuthentication.resourceAppId;
691         })
693         (lib.mkIf (cfg.oidcAuthentication != null) {
694           OIDC_CLIENT_ID = cfg.oidcAuthentication.clientId;
695           OIDC_AUTH_URI = cfg.oidcAuthentication.authUrl;
696           OIDC_TOKEN_URI = cfg.oidcAuthentication.tokenUrl;
697           OIDC_USERINFO_URI = cfg.oidcAuthentication.userinfoUrl;
698           OIDC_USERNAME_CLAIM = cfg.oidcAuthentication.usernameClaim;
699           OIDC_DISPLAY_NAME = cfg.oidcAuthentication.displayName;
700           OIDC_SCOPES = lib.concatStringsSep " " cfg.oidcAuthentication.scopes;
701         })
703         (lib.mkIf (cfg.slackIntegration != null) {
704           SLACK_APP_ID = cfg.slackIntegration.appId;
705           SLACK_MESSAGE_ACTIONS = builtins.toString cfg.slackIntegration.messageActions;
706         })
708         (lib.mkIf (cfg.smtp != null) {
709           SMTP_HOST = cfg.smtp.host;
710           SMTP_PORT = builtins.toString cfg.smtp.port;
711           SMTP_USERNAME = cfg.smtp.username;
712           SMTP_FROM_EMAIL = cfg.smtp.fromEmail;
713           SMTP_REPLY_EMAIL = cfg.smtp.replyEmail;
714           SMTP_TLS_CIPHERS = cfg.smtp.tlsCiphers;
715           SMTP_SECURE = builtins.toString cfg.smtp.secure;
716         })
717       ];
719       preStart = ''
720         if [ ! -s ${lib.escapeShellArg cfg.secretKeyFile} ]; then
721           openssl rand -hex 32 > ${lib.escapeShellArg cfg.secretKeyFile}
722         fi
723         if [ ! -s ${lib.escapeShellArg cfg.utilsSecretFile} ]; then
724           openssl rand -hex 32 > ${lib.escapeShellArg cfg.utilsSecretFile}
725         fi
727       '';
729       script = ''
730         export SECRET_KEY="$(head -n1 ${lib.escapeShellArg cfg.secretKeyFile})"
731         export UTILS_SECRET="$(head -n1 ${lib.escapeShellArg cfg.utilsSecretFile})"
732         ${lib.optionalString (cfg.storage.storageType == "s3") ''
733           export AWS_SECRET_ACCESS_KEY="$(head -n1 ${lib.escapeShellArg cfg.storage.secretKeyFile})"
734         ''}
735         ${lib.optionalString (cfg.slackAuthentication != null) ''
736           export SLACK_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.slackAuthentication.secretFile})"
737         ''}
738         ${lib.optionalString (cfg.googleAuthentication != null) ''
739           export GOOGLE_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.googleAuthentication.clientSecretFile})"
740         ''}
741         ${lib.optionalString (cfg.azureAuthentication != null) ''
742           export AZURE_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.azureAuthentication.clientSecretFile})"
743         ''}
744         ${lib.optionalString (cfg.oidcAuthentication != null) ''
745           export OIDC_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.oidcAuthentication.clientSecretFile})"
746         ''}
747         ${lib.optionalString (cfg.sslKeyFile != null) ''
748           export SSL_KEY="$(head -n1 ${lib.escapeShellArg cfg.sslKeyFile})"
749         ''}
750         ${lib.optionalString (cfg.sslCertFile != null) ''
751           export SSL_CERT="$(head -n1 ${lib.escapeShellArg cfg.sslCertFile})"
752         ''}
753         ${lib.optionalString (cfg.slackIntegration != null) ''
754           export SLACK_VERIFICATION_TOKEN="$(head -n1 ${lib.escapeShellArg cfg.slackIntegration.verificationTokenFile})"
755         ''}
756         ${lib.optionalString (cfg.smtp != null) ''
757           export SMTP_PASSWORD="$(head -n1 ${lib.escapeShellArg cfg.smtp.passwordFile})"
758         ''}
760         ${if (cfg.databaseUrl == "local") then ''
761           export DATABASE_URL=${lib.escapeShellArg localPostgresqlUrl}
762           export PGSSLMODE=disable
763         '' else ''
764           export DATABASE_URL=${lib.escapeShellArg cfg.databaseUrl}
765         ''}
767         ${cfg.package}/bin/outline-server
768       '';
770       serviceConfig = {
771         User = cfg.user;
772         Group = cfg.group;
773         Restart = "always";
774         ProtectSystem = "strict";
775         PrivateHome = true;
776         PrivateTmp = true;
777         UMask = "0007";
779         StateDirectory = "outline";
780         StateDirectoryMode = "0750";
781         RuntimeDirectory = "outline";
782         RuntimeDirectoryMode = "0750";
783         # This working directory is required to find stuff like the set of
784         # onboarding files:
785         WorkingDirectory = "${cfg.package}/share/outline";
786         # In case this directory is not in /var/lib/outline, it needs to be made writable explicitly
787         ReadWritePaths = lib.mkIf (cfg.storage.storageType == "local") [ cfg.storage.localRootDir ];
788       };
789     };
790   };