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