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