at 23.11-pre 28 kB view raw
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}