1{ config, lib, pkgs, ...}:
2
3let
4 defaultUser = "outline";
5 cfg = config.services.outline;
6 inherit (lib) mkRemovedOptionModule;
7in
8{
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";
20
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 };
38
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 };
48
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 };
57
58 #
59 # Required options
60 #
61
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 };
71
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 };
80
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 };
92
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 };
104
105 publicUrl = lib.mkOption {
106 type = lib.types.str;
107 default = "http://localhost:3000";
108 description = "The fully qualified, publicly accessible URL";
109 };
110
111 port = lib.mkOption {
112 type = lib.types.port;
113 default = 3000;
114 description = "Listening port.";
115 };
116
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.
125
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 };
195
196 #
197 # Authentication
198 #
199
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
204
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 };
222
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
227
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 };
245
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 };
271
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 };
323
324 #
325 # Optional configuration
326 #
327
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 };
348
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 };
359
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 };
369
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 };
378
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 };
387
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 };
396
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 };
402
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 };
428
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 };
437
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 };
446
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 };
456
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 };
465
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 };
514
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 };
545
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 };
558
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 };
566
567 users.groups = lib.optionalAttrs (cfg.group == defaultUser) {
568 ${defaultUser} = { };
569 };
570
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 ];
579
580 services.postgresql = lib.mkIf (cfg.databaseUrl == "local") {
581 enable = true;
582 ensureUsers = [{
583 name = "outline";
584 ensureDBOwnership = true;
585 }];
586 ensureDatabases = [ "outline" ];
587 };
588
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
609
610 psql outline -tAc 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'
611 '';
612
613 serviceConfig = {
614 User = pgsql.superUser;
615 Type = "oneshot";
616 RemainAfterExit = true;
617 };
618 };
619
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 };
625
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 ];
640
641
642 environment = lib.mkMerge [
643 {
644 NODE_ENV = "production";
645
646 REDIS_URL = if cfg.redisUrl == "local" then localRedisUrl else cfg.redisUrl;
647 URL = cfg.publicUrl;
648 PORT = builtins.toString cfg.port;
649
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;
661
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;
665
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 }
670
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 })
679
680 (lib.mkIf (cfg.slackAuthentication != null) {
681 SLACK_CLIENT_ID = cfg.slackAuthentication.clientId;
682 })
683
684 (lib.mkIf (cfg.googleAuthentication != null) {
685 GOOGLE_CLIENT_ID = cfg.googleAuthentication.clientId;
686 })
687
688 (lib.mkIf (cfg.azureAuthentication != null) {
689 AZURE_CLIENT_ID = cfg.azureAuthentication.clientId;
690 AZURE_RESOURCE_APP_ID = cfg.azureAuthentication.resourceAppId;
691 })
692
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 })
702
703 (lib.mkIf (cfg.slackIntegration != null) {
704 SLACK_APP_ID = cfg.slackIntegration.appId;
705 SLACK_MESSAGE_ACTIONS = builtins.toString cfg.slackIntegration.messageActions;
706 })
707
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 ];
718
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
726
727 '';
728
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 ''}
759
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 ''}
766
767 ${cfg.package}/bin/outline-server
768 '';
769
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";
778
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 };
791}