at master 30 kB view raw
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}