at 25.11-pre 28 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 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}