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 accelerateUrl = lib.mkOption {
170 type = lib.types.nullOr lib.types.str;
171 default = null;
172 description = ''
173 URL for AWS S3 [transfer acceleration](https://docs.aws.amazon.com/AmazonS3/latest/userguide/transfer-acceleration.html).
174 '';
175 };
176 secretKeyFile = lib.mkOption {
177 type = lib.types.path;
178 description = "File path that contains the S3 secret key.";
179 };
180 region = lib.mkOption {
181 type = lib.types.str;
182 default = "xx-xxxx-x";
183 description = "AWS S3 region name.";
184 };
185 uploadBucketUrl = lib.mkOption {
186 type = lib.types.str;
187 description = ''
188 URL endpoint of an S3-compatible API where uploads should be
189 stored.
190 '';
191 };
192 uploadBucketName = lib.mkOption {
193 type = lib.types.str;
194 description = "Name of the bucket where uploads should be stored.";
195 };
196 uploadMaxSize = lib.mkOption {
197 type = lib.types.int;
198 default = 26214400;
199 description = "Maxmium file size for uploads.";
200 };
201 forcePathStyle = lib.mkOption {
202 type = lib.types.bool;
203 default = true;
204 description = "Force S3 path style.";
205 };
206 acl = lib.mkOption {
207 type = lib.types.str;
208 default = "private";
209 description = "ACL setting.";
210 };
211 };
212 };
213 };
214
215 #
216 # Authentication
217 #
218
219 slackAuthentication = lib.mkOption {
220 description = ''
221 To configure Slack auth, you'll need to create an Application at
222 <https://api.slack.com/apps>
223
224 When configuring the Client ID, add a redirect URL under "OAuth & Permissions"
225 to `https://[publicUrl]/auth/slack.callback`.
226 '';
227 default = null;
228 type = lib.types.nullOr (
229 lib.types.submodule {
230 options = {
231 clientId = lib.mkOption {
232 type = lib.types.str;
233 description = "Authentication key.";
234 };
235 secretFile = lib.mkOption {
236 type = lib.types.str;
237 description = "File path containing the authentication secret.";
238 };
239 };
240 }
241 );
242 };
243
244 googleAuthentication = lib.mkOption {
245 description = ''
246 To configure Google auth, you'll need to create an OAuth Client ID at
247 <https://console.cloud.google.com/apis/credentials>
248
249 When configuring the Client ID, add an Authorized redirect URI to
250 `https://[publicUrl]/auth/google.callback`.
251 '';
252 default = null;
253 type = lib.types.nullOr (
254 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 };
265 }
266 );
267 };
268
269 azureAuthentication = lib.mkOption {
270 description = ''
271 To configure Microsoft/Azure auth, you'll need to create an OAuth
272 Client. See
273 [the guide](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4)
274 for details on setting up your Azure App.
275 '';
276 default = null;
277 type = lib.types.nullOr (
278 lib.types.submodule {
279 options = {
280 clientId = lib.mkOption {
281 type = lib.types.str;
282 description = "Authentication client identifier.";
283 };
284 clientSecretFile = lib.mkOption {
285 type = lib.types.str;
286 description = "File path containing the authentication secret.";
287 };
288 resourceAppId = lib.mkOption {
289 type = lib.types.str;
290 description = "Authentication application resource ID.";
291 };
292 };
293 }
294 );
295 };
296
297 discordAuthentication = lib.mkOption {
298 description = ''
299 To configure Discord auth, you'll need to create an application at
300 <https://discord.com/developers/applications/>
301
302 See <https://docs.getoutline.com/s/hosting/doc/discord-g4JdWFFub6>
303 for details on setting up your Discord app.
304 '';
305 default = null;
306 type = lib.types.nullOr (
307 lib.types.submodule {
308 options = {
309 clientId = lib.mkOption {
310 type = lib.types.str;
311 description = "Authentication client identifier.";
312 };
313 clientSecretFile = lib.mkOption {
314 type = lib.types.str;
315 description = "File path containing the authentication secret.";
316 };
317 serverId = lib.mkOption {
318 type = lib.types.str;
319 default = "";
320 description = ''
321 Restrict logins to a specific server (optional, but recommended).
322 You can find a Discord server's ID by right-clicking the server icon,
323 and select “Copy Server ID”.
324 '';
325 };
326 serverRoles = lib.mkOption {
327 type = lib.types.commas;
328 default = "";
329 description = "Optionally restrict logins to a comma-separated list of role IDs";
330 };
331 };
332 }
333 );
334 };
335
336 oidcAuthentication = lib.mkOption {
337 description = ''
338 To configure generic OIDC auth, you'll need some kind of identity
339 provider. See the documentation for whichever IdP you use to fill out
340 all the fields. The redirect URL is
341 `https://[publicUrl]/auth/oidc.callback`.
342 '';
343 default = null;
344 type = lib.types.nullOr (
345 lib.types.submodule {
346 options = {
347 clientId = lib.mkOption {
348 type = lib.types.str;
349 description = "Authentication client identifier.";
350 };
351 clientSecretFile = lib.mkOption {
352 type = lib.types.str;
353 description = "File path containing the authentication secret.";
354 };
355 authUrl = lib.mkOption {
356 type = lib.types.str;
357 description = "OIDC authentication URL endpoint.";
358 };
359 tokenUrl = lib.mkOption {
360 type = lib.types.str;
361 description = "OIDC token URL endpoint.";
362 };
363 userinfoUrl = lib.mkOption {
364 type = lib.types.str;
365 description = "OIDC userinfo URL endpoint.";
366 };
367 usernameClaim = lib.mkOption {
368 type = lib.types.str;
369 description = ''
370 Specify which claims to derive user information from. Supports any
371 valid JSON path with the JWT payload
372 '';
373 default = "preferred_username";
374 };
375 displayName = lib.mkOption {
376 type = lib.types.str;
377 description = "Display name for OIDC authentication.";
378 default = "OpenID";
379 };
380 scopes = lib.mkOption {
381 type = lib.types.listOf lib.types.str;
382 description = "OpenID authentication scopes.";
383 default = [
384 "openid"
385 "profile"
386 "email"
387 ];
388 };
389 };
390 }
391 );
392 };
393
394 #
395 # Optional configuration
396 #
397
398 sslKeyFile = lib.mkOption {
399 type = lib.types.nullOr lib.types.str;
400 default = null;
401 description = ''
402 File path that contains the Base64-encoded private key for HTTPS
403 termination. This is only required if you do not use an external reverse
404 proxy. See
405 [the documentation](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4).
406 '';
407 };
408 sslCertFile = lib.mkOption {
409 type = lib.types.nullOr lib.types.str;
410 default = null;
411 description = ''
412 File path that contains the Base64-encoded certificate for HTTPS
413 termination. This is only required if you do not use an external reverse
414 proxy. See
415 [the documentation](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4).
416 '';
417 };
418
419 cdnUrl = lib.mkOption {
420 type = lib.types.str;
421 default = "";
422 description = ''
423 If using a Cloudfront/Cloudflare distribution or similar it can be set
424 using this option. This will cause paths to JavaScript files,
425 stylesheets and images to be updated to the hostname defined here. In
426 your CDN configuration the origin server should be set to public URL.
427 '';
428 };
429
430 forceHttps = lib.mkOption {
431 type = lib.types.bool;
432 default = true;
433 description = ''
434 Auto-redirect to HTTPS in production. The default is
435 `true` but you may set this to `false`
436 if you can be sure that SSL is terminated at an external loadbalancer.
437 '';
438 };
439
440 enableUpdateCheck = lib.mkOption {
441 type = lib.types.bool;
442 default = false;
443 description = ''
444 Have the installation check for updates by sending anonymized statistics
445 to the maintainers.
446 '';
447 };
448
449 concurrency = lib.mkOption {
450 type = lib.types.int;
451 default = 1;
452 description = ''
453 How many processes should be spawned. For a rough estimate, divide your
454 server's available memory by 512.
455 '';
456 };
457
458 maximumImportSize = lib.mkOption {
459 type = lib.types.int;
460 default = 5120000;
461 description = ''
462 The maximum size of document imports. Overriding this could be required
463 if you have especially large Word documents with embedded imagery.
464 '';
465 };
466
467 debugOutput = lib.mkOption {
468 type = lib.types.nullOr (lib.types.enum [ "http" ]);
469 default = null;
470 description = "Set this to `http` log HTTP requests.";
471 };
472
473 slackIntegration = lib.mkOption {
474 description = ''
475 For a complete Slack integration with search and posting to channels
476 this configuration is also needed. See here for details:
477 <https://wiki.generaloutline.com/share/be25efd1-b3ef-4450-b8e5-c4a4fc11e02a>
478 '';
479 default = null;
480 type = lib.types.nullOr (
481 lib.types.submodule {
482 options = {
483 verificationTokenFile = lib.mkOption {
484 type = lib.types.str;
485 description = "File path containing the verification token.";
486 };
487 appId = lib.mkOption {
488 type = lib.types.str;
489 description = "Application ID.";
490 };
491 messageActions = lib.mkOption {
492 type = lib.types.bool;
493 default = true;
494 description = "Whether to enable message actions.";
495 };
496 };
497 }
498 );
499 };
500
501 googleAnalyticsId = lib.mkOption {
502 type = lib.types.nullOr lib.types.str;
503 default = null;
504 description = ''
505 Optionally enable Google Analytics to track page views in the knowledge
506 base.
507 '';
508 };
509
510 sentryDsn = lib.mkOption {
511 type = lib.types.nullOr lib.types.str;
512 default = null;
513 description = ''
514 Optionally enable [Sentry](https://sentry.io/) to
515 track errors and performance.
516 '';
517 };
518
519 sentryTunnel = lib.mkOption {
520 type = lib.types.nullOr lib.types.str;
521 default = null;
522 description = ''
523 Optionally add a
524 [Sentry proxy tunnel](https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option)
525 for bypassing ad blockers in the UI.
526 '';
527 };
528
529 logo = lib.mkOption {
530 type = lib.types.nullOr lib.types.str;
531 default = null;
532 description = ''
533 Custom logo displayed on the authentication screen. This will be scaled
534 to a height of 60px.
535 '';
536 };
537
538 smtp = lib.mkOption {
539 description = ''
540 To support sending outgoing transactional emails such as
541 "document updated" or "you've been invited" you'll need to provide
542 authentication for an SMTP server.
543 '';
544 default = null;
545 type = lib.types.nullOr (
546 lib.types.submodule {
547 options = {
548 host = lib.mkOption {
549 type = lib.types.str;
550 description = "Host name or IP address of the SMTP server.";
551 };
552 port = lib.mkOption {
553 type = lib.types.port;
554 description = "TCP port of the SMTP server.";
555 };
556 username = lib.mkOption {
557 type = lib.types.str;
558 description = "Username to authenticate with.";
559 };
560 passwordFile = lib.mkOption {
561 type = lib.types.str;
562 description = ''
563 File path containing the password to authenticate with.
564 '';
565 };
566 fromEmail = lib.mkOption {
567 type = lib.types.str;
568 description = "Sender email in outgoing mail.";
569 };
570 replyEmail = lib.mkOption {
571 type = lib.types.str;
572 description = "Reply address in outgoing mail.";
573 };
574 tlsCiphers = lib.mkOption {
575 type = lib.types.str;
576 default = "";
577 description = "Override SMTP cipher configuration.";
578 };
579 secure = lib.mkOption {
580 type = lib.types.bool;
581 default = true;
582 description = "Use a secure SMTP connection.";
583 };
584 };
585 }
586 );
587 };
588
589 defaultLanguage = lib.mkOption {
590 type = lib.types.enum [
591 "da_DK"
592 "de_DE"
593 "en_US"
594 "es_ES"
595 "fa_IR"
596 "fr_FR"
597 "it_IT"
598 "ja_JP"
599 "ko_KR"
600 "nl_NL"
601 "pl_PL"
602 "pt_BR"
603 "pt_PT"
604 "ru_RU"
605 "sv_SE"
606 "th_TH"
607 "vi_VN"
608 "zh_CN"
609 "zh_TW"
610 ];
611 default = "en_US";
612 description = ''
613 The default interface language. See
614 [translate.getoutline.com](https://translate.getoutline.com/)
615 for a list of available language codes and their rough percentage
616 translated.
617 '';
618 };
619
620 rateLimiter.enable = lib.mkEnableOption "rate limiter for the application web server";
621 rateLimiter.requests = lib.mkOption {
622 type = lib.types.int;
623 default = 5000;
624 description = "Maximum number of requests in a throttling window.";
625 };
626 rateLimiter.durationWindow = lib.mkOption {
627 type = lib.types.int;
628 default = 60;
629 description = "Length of a throttling window.";
630 };
631 };
632
633 config = lib.mkIf cfg.enable {
634 users.users = lib.optionalAttrs (cfg.user == defaultUser) {
635 ${defaultUser} = {
636 isSystemUser = true;
637 group = cfg.group;
638 };
639 };
640
641 users.groups = lib.optionalAttrs (cfg.group == defaultUser) {
642 ${defaultUser} = { };
643 };
644
645 systemd.tmpfiles.rules = [
646 "f ${cfg.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -"
647 "f ${cfg.utilsSecretFile} 0600 ${cfg.user} ${cfg.group} -"
648 (
649 if (cfg.storage.storageType == "s3") then
650 "f ${cfg.storage.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -"
651 else
652 "d ${cfg.storage.localRootDir} 0700 ${cfg.user} ${cfg.group} - -"
653 )
654 ];
655
656 services.postgresql = lib.mkIf (cfg.databaseUrl == "local") {
657 enable = true;
658 ensureUsers = [
659 {
660 name = "outline";
661 ensureDBOwnership = true;
662 }
663 ];
664 ensureDatabases = [ "outline" ];
665 };
666
667 services.redis.servers.outline = lib.mkIf (cfg.redisUrl == "local") {
668 enable = true;
669 user = config.services.outline.user;
670 port = 0; # Disable the TCP listener
671 };
672
673 systemd.services.outline =
674 let
675 localRedisUrl = "redis+unix:///run/redis-outline/redis.sock";
676 localPostgresqlUrl = "postgres://localhost/outline?host=/run/postgresql";
677 in
678 {
679 description = "Outline wiki and knowledge base";
680 wantedBy = [ "multi-user.target" ];
681 after = [
682 "networking.target"
683 ]
684 ++ lib.optional (cfg.databaseUrl == "local") "postgresql.target"
685 ++ lib.optional (cfg.redisUrl == "local") "redis-outline.service";
686 requires =
687 lib.optional (cfg.databaseUrl == "local") "postgresql.target"
688 ++ lib.optional (cfg.redisUrl == "local") "redis-outline.service";
689 path = [
690 pkgs.openssl # Required by the preStart script
691 ];
692
693 environment = lib.mkMerge [
694 {
695 NODE_ENV = "production";
696
697 REDIS_URL = if cfg.redisUrl == "local" then localRedisUrl else cfg.redisUrl;
698 URL = cfg.publicUrl;
699 PORT = builtins.toString cfg.port;
700
701 CDN_URL = cfg.cdnUrl;
702 FORCE_HTTPS = builtins.toString cfg.forceHttps;
703 ENABLE_UPDATES = builtins.toString cfg.enableUpdateCheck;
704 WEB_CONCURRENCY = builtins.toString cfg.concurrency;
705 DEBUG = cfg.debugOutput;
706 GOOGLE_ANALYTICS_ID = lib.optionalString (cfg.googleAnalyticsId != null) cfg.googleAnalyticsId;
707 SENTRY_DSN = lib.optionalString (cfg.sentryDsn != null) cfg.sentryDsn;
708 SENTRY_TUNNEL = lib.optionalString (cfg.sentryTunnel != null) cfg.sentryTunnel;
709 TEAM_LOGO = lib.optionalString (cfg.logo != null) cfg.logo;
710 DEFAULT_LANGUAGE = cfg.defaultLanguage;
711
712 RATE_LIMITER_ENABLED = builtins.toString cfg.rateLimiter.enable;
713 RATE_LIMITER_REQUESTS = builtins.toString cfg.rateLimiter.requests;
714 RATE_LIMITER_DURATION_WINDOW = builtins.toString cfg.rateLimiter.durationWindow;
715
716 FILE_STORAGE = cfg.storage.storageType;
717 FILE_STORAGE_IMPORT_MAX_SIZE = builtins.toString cfg.maximumImportSize;
718 FILE_STORAGE_UPLOAD_MAX_SIZE = builtins.toString cfg.storage.uploadMaxSize;
719 FILE_STORAGE_LOCAL_ROOT_DIR = cfg.storage.localRootDir;
720 }
721
722 (lib.mkIf (cfg.storage.storageType == "s3") {
723 AWS_ACCESS_KEY_ID = cfg.storage.accessKey;
724 AWS_REGION = cfg.storage.region;
725 AWS_S3_UPLOAD_BUCKET_URL = cfg.storage.uploadBucketUrl;
726 AWS_S3_UPLOAD_BUCKET_NAME = cfg.storage.uploadBucketName;
727 AWS_S3_FORCE_PATH_STYLE = builtins.toString cfg.storage.forcePathStyle;
728 AWS_S3_ACL = cfg.storage.acl;
729 })
730
731 (lib.mkIf (cfg.storage.storageType == "s3" && cfg.storage.accelerateUrl != null) {
732 AWS_S3_ACCELERATE_URL = cfg.storage.accelerateUrl;
733 })
734
735 (lib.mkIf (cfg.slackAuthentication != null) {
736 SLACK_CLIENT_ID = cfg.slackAuthentication.clientId;
737 })
738
739 (lib.mkIf (cfg.googleAuthentication != null) {
740 GOOGLE_CLIENT_ID = cfg.googleAuthentication.clientId;
741 })
742
743 (lib.mkIf (cfg.azureAuthentication != null) {
744 AZURE_CLIENT_ID = cfg.azureAuthentication.clientId;
745 AZURE_RESOURCE_APP_ID = cfg.azureAuthentication.resourceAppId;
746 })
747
748 (lib.mkIf (cfg.oidcAuthentication != null) {
749 OIDC_CLIENT_ID = cfg.oidcAuthentication.clientId;
750 OIDC_AUTH_URI = cfg.oidcAuthentication.authUrl;
751 OIDC_TOKEN_URI = cfg.oidcAuthentication.tokenUrl;
752 OIDC_USERINFO_URI = cfg.oidcAuthentication.userinfoUrl;
753 OIDC_USERNAME_CLAIM = cfg.oidcAuthentication.usernameClaim;
754 OIDC_DISPLAY_NAME = cfg.oidcAuthentication.displayName;
755 OIDC_SCOPES = lib.concatStringsSep " " cfg.oidcAuthentication.scopes;
756 })
757
758 (lib.mkIf (cfg.slackIntegration != null) {
759 SLACK_APP_ID = cfg.slackIntegration.appId;
760 SLACK_MESSAGE_ACTIONS = builtins.toString cfg.slackIntegration.messageActions;
761 })
762
763 (lib.mkIf (cfg.discordAuthentication != null) {
764 DISCORD_CLIENT_ID = cfg.discordAuthentication.clientId;
765 DISCORD_SERVER_ID = cfg.discordAuthentication.serverId;
766 DISCORD_SERVER_ROLES = cfg.discordAuthentication.serverRoles;
767 })
768
769 (lib.mkIf (cfg.smtp != null) {
770 SMTP_HOST = cfg.smtp.host;
771 SMTP_PORT = builtins.toString cfg.smtp.port;
772 SMTP_USERNAME = cfg.smtp.username;
773 SMTP_FROM_EMAIL = cfg.smtp.fromEmail;
774 SMTP_REPLY_EMAIL = cfg.smtp.replyEmail;
775 SMTP_TLS_CIPHERS = cfg.smtp.tlsCiphers;
776 SMTP_SECURE = builtins.toString cfg.smtp.secure;
777 })
778 ];
779
780 preStart = ''
781 if [ ! -s ${lib.escapeShellArg cfg.secretKeyFile} ]; then
782 openssl rand -hex 32 > ${lib.escapeShellArg cfg.secretKeyFile}
783 fi
784 if [ ! -s ${lib.escapeShellArg cfg.utilsSecretFile} ]; then
785 openssl rand -hex 32 > ${lib.escapeShellArg cfg.utilsSecretFile}
786 fi
787
788 '';
789
790 script = ''
791 export SECRET_KEY="$(head -n1 ${lib.escapeShellArg cfg.secretKeyFile})"
792 export UTILS_SECRET="$(head -n1 ${lib.escapeShellArg cfg.utilsSecretFile})"
793 ${lib.optionalString (cfg.storage.storageType == "s3") ''
794 export AWS_SECRET_ACCESS_KEY="$(head -n1 ${lib.escapeShellArg cfg.storage.secretKeyFile})"
795 ''}
796 ${lib.optionalString (cfg.slackAuthentication != null) ''
797 export SLACK_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.slackAuthentication.secretFile})"
798 ''}
799 ${lib.optionalString (cfg.googleAuthentication != null) ''
800 export GOOGLE_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.googleAuthentication.clientSecretFile})"
801 ''}
802 ${lib.optionalString (cfg.azureAuthentication != null) ''
803 export AZURE_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.azureAuthentication.clientSecretFile})"
804 ''}
805 ${lib.optionalString (cfg.oidcAuthentication != null) ''
806 export OIDC_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.oidcAuthentication.clientSecretFile})"
807 ''}
808 ${lib.optionalString (cfg.discordAuthentication != null) ''
809 export DISCORD_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.discordAuthentication.clientSecretFile})"
810 ''}
811 ${lib.optionalString (cfg.sslKeyFile != null) ''
812 export SSL_KEY="$(head -n1 ${lib.escapeShellArg cfg.sslKeyFile})"
813 ''}
814 ${lib.optionalString (cfg.sslCertFile != null) ''
815 export SSL_CERT="$(head -n1 ${lib.escapeShellArg cfg.sslCertFile})"
816 ''}
817 ${lib.optionalString (cfg.slackIntegration != null) ''
818 export SLACK_VERIFICATION_TOKEN="$(head -n1 ${lib.escapeShellArg cfg.slackIntegration.verificationTokenFile})"
819 ''}
820 ${lib.optionalString (cfg.smtp != null) ''
821 export SMTP_PASSWORD="$(head -n1 ${lib.escapeShellArg cfg.smtp.passwordFile})"
822 ''}
823
824 ${
825 if (cfg.databaseUrl == "local") then
826 ''
827 export DATABASE_URL=${lib.escapeShellArg localPostgresqlUrl}
828 export PGSSLMODE=disable
829 ''
830 else
831 ''
832 export DATABASE_URL=${lib.escapeShellArg cfg.databaseUrl}
833 ''
834 }
835
836 ${cfg.package}/bin/outline-server
837 '';
838
839 serviceConfig = {
840 User = cfg.user;
841 Group = cfg.group;
842 Restart = "always";
843 ProtectSystem = "strict";
844 PrivateTmp = true;
845 UMask = "0007";
846
847 StateDirectory = "outline";
848 StateDirectoryMode = "0750";
849 RuntimeDirectory = "outline";
850 RuntimeDirectoryMode = "0750";
851 # This working directory is required to find stuff like the set of
852 # onboarding files:
853 WorkingDirectory = "${cfg.package}/share/outline";
854 # In case this directory is not in /var/lib/outline, it needs to be made writable explicitly
855 ReadWritePaths = lib.mkIf (cfg.storage.storageType == "local") [ cfg.storage.localRootDir ];
856 };
857 };
858 };
859}