2022-02-16 11:02:44 +00:00
|
|
|
{ config, lib, pkgs, ...}:
|
|
|
|
|
|
|
|
let
|
|
|
|
defaultUser = "outline";
|
|
|
|
cfg = config.services.outline;
|
|
|
|
in
|
|
|
|
{
|
|
|
|
# See here for a reference of all the options:
|
2022-11-21 08:33:08 +00:00
|
|
|
# https://github.com/outline/outline/blob/v0.67.0/.env.sample
|
|
|
|
# https://github.com/outline/outline/blob/v0.67.0/app.json
|
|
|
|
# https://github.com/outline/outline/blob/v0.67.0/server/env.ts
|
|
|
|
# https://github.com/outline/outline/blob/v0.67.0/shared/types.ts
|
2022-02-16 11:02:44 +00:00
|
|
|
# The order is kept the same here to make updating easier.
|
|
|
|
options.services.outline = {
|
2022-08-28 20:18:44 +01:00
|
|
|
enable = lib.mkEnableOption (lib.mdDoc "outline");
|
2022-02-16 11:02:44 +00:00
|
|
|
|
|
|
|
package = lib.mkOption {
|
|
|
|
default = pkgs.outline;
|
|
|
|
defaultText = lib.literalExpression "pkgs.outline";
|
|
|
|
type = lib.types.package;
|
|
|
|
example = lib.literalExpression ''
|
|
|
|
pkgs.outline.overrideAttrs (super: {
|
|
|
|
# Ignore the domain part in emails that come from OIDC. This is might
|
|
|
|
# be helpful if you want multiple users with different email providers
|
|
|
|
# to still land in the same team. Note that this effectively makes
|
|
|
|
# Outline a single-team instance.
|
|
|
|
patchPhase = ${"''"}
|
|
|
|
sed -i 's/const domain = parts\.length && parts\[1\];/const domain = "example.com";/g' server/routes/auth/providers/oidc.ts
|
|
|
|
${"''"};
|
|
|
|
})
|
|
|
|
'';
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Outline package to use.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
user = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = defaultUser;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
User under which the service should run. If this is the default value,
|
|
|
|
the user will be created, with the specified group as the primary
|
|
|
|
group.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
group = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = defaultUser;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
Group under which the service should run. If this is the default value,
|
|
|
|
the group will be created.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
sequelizeArguments = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = "";
|
|
|
|
example = "--env=production-ssl-disabled";
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
|
|
|
Optional arguments to pass to `sequelize` calls.
|
2022-02-16 11:02:44 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
#
|
|
|
|
# Required options
|
|
|
|
#
|
|
|
|
|
|
|
|
secretKeyFile = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = "/var/lib/outline/secret_key";
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
File path that contains the application secret key. It must be 32
|
|
|
|
bytes long and hex-encoded. If the file does not exist, a new key will
|
|
|
|
be generated and saved here.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
utilsSecretFile = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = "/var/lib/outline/utils_secret";
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
File path that contains the utility secret key. If the file does not
|
|
|
|
exist, a new key will be generated and saved here.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
databaseUrl = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = "local";
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
URI to use for the main PostgreSQL database. If this needs to include
|
|
|
|
credentials that shouldn't be world-readable in the Nix store, set an
|
|
|
|
environment file on the systemd service and override the
|
2022-08-28 20:18:44 +01:00
|
|
|
`DATABASE_URL` entry. Pass the string
|
|
|
|
`local` to setup a database on the local server.
|
2022-02-16 11:02:44 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
redisUrl = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = "local";
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
Connection to a redis server. If this needs to include credentials
|
|
|
|
that shouldn't be world-readable in the Nix store, set an environment
|
|
|
|
file on the systemd service and override the
|
2022-08-28 20:18:44 +01:00
|
|
|
`REDIS_URL` entry. Pass the string
|
|
|
|
`local` to setup a local Redis database.
|
2022-02-16 11:02:44 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
publicUrl = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = "http://localhost:3000";
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "The fully qualified, publicly accessible URL";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
port = lib.mkOption {
|
|
|
|
type = lib.types.port;
|
|
|
|
default = 3000;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Listening port.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
storage = lib.mkOption {
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
To support uploading of images for avatars and document attachments an
|
|
|
|
s3-compatible storage must be provided. AWS S3 is recommended for
|
2022-11-21 08:33:08 +00:00
|
|
|
redundancy however if you want to keep all file storage local an
|
2022-08-28 20:18:44 +01:00
|
|
|
alternative such as [minio](https://github.com/minio/minio)
|
2022-02-16 11:02:44 +00:00
|
|
|
can be used.
|
|
|
|
|
|
|
|
A more detailed guide on setting up S3 is available
|
2022-08-28 20:18:44 +01:00
|
|
|
[here](https://wiki.generaloutline.com/share/125de1cc-9ff6-424b-8415-0d58c809a40f).
|
2022-02-16 11:02:44 +00:00
|
|
|
'';
|
|
|
|
example = lib.literalExpression ''
|
|
|
|
{
|
|
|
|
accessKey = "...";
|
|
|
|
secretKeyFile = "/somewhere";
|
|
|
|
uploadBucketUrl = "https://minio.example.com";
|
|
|
|
uploadBucketName = "outline";
|
|
|
|
region = "us-east-1";
|
|
|
|
}
|
|
|
|
'';
|
|
|
|
type = lib.types.submodule {
|
|
|
|
options = {
|
|
|
|
accessKey = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "S3 access key.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
secretKeyFile = lib.mkOption {
|
|
|
|
type = lib.types.path;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "File path that contains the S3 secret key.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
region = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = "xx-xxxx-x";
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "AWS S3 region name.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
uploadBucketUrl = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
URL endpoint of an S3-compatible API where uploads should be
|
|
|
|
stored.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
uploadBucketName = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Name of the bucket where uploads should be stored.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
uploadMaxSize = lib.mkOption {
|
|
|
|
type = lib.types.int;
|
|
|
|
default = 26214400;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Maxmium file size for uploads.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
forcePathStyle = lib.mkOption {
|
|
|
|
type = lib.types.bool;
|
|
|
|
default = true;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Force S3 path style.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
acl = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = "private";
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "ACL setting.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
#
|
|
|
|
# Authentication
|
|
|
|
#
|
|
|
|
|
|
|
|
slackAuthentication = lib.mkOption {
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
To configure Slack auth, you'll need to create an Application at
|
|
|
|
https://api.slack.com/apps
|
|
|
|
|
2022-08-28 20:18:44 +01:00
|
|
|
When configuring the Client ID, add a redirect URL under "OAuth & Permissions"
|
|
|
|
to `https://[publicUrl]/auth/slack.callback`.
|
2022-02-16 11:02:44 +00:00
|
|
|
'';
|
|
|
|
default = null;
|
|
|
|
type = lib.types.nullOr (lib.types.submodule {
|
|
|
|
options = {
|
|
|
|
clientId = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Authentication key.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
secretFile = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "File path containing the authentication secret.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
googleAuthentication = lib.mkOption {
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
To configure Google auth, you'll need to create an OAuth Client ID at
|
|
|
|
https://console.cloud.google.com/apis/credentials
|
|
|
|
|
|
|
|
When configuring the Client ID, add an Authorized redirect URI to
|
2022-08-28 20:18:44 +01:00
|
|
|
`https://[publicUrl]/auth/google.callback`.
|
2022-02-16 11:02:44 +00:00
|
|
|
'';
|
|
|
|
default = null;
|
|
|
|
type = lib.types.nullOr (lib.types.submodule {
|
|
|
|
options = {
|
|
|
|
clientId = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Authentication client identifier.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
clientSecretFile = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "File path containing the authentication secret.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
azureAuthentication = lib.mkOption {
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
To configure Microsoft/Azure auth, you'll need to create an OAuth
|
|
|
|
Client. See
|
2022-08-28 20:18:44 +01:00
|
|
|
[the guide](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4)
|
2022-02-16 11:02:44 +00:00
|
|
|
for details on setting up your Azure App.
|
|
|
|
'';
|
|
|
|
default = null;
|
|
|
|
type = lib.types.nullOr (lib.types.submodule {
|
|
|
|
options = {
|
|
|
|
clientId = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Authentication client identifier.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
clientSecretFile = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "File path containing the authentication secret.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
resourceAppId = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Authentication application resource ID.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
oidcAuthentication = lib.mkOption {
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
To configure generic OIDC auth, you'll need some kind of identity
|
|
|
|
provider. See the documentation for whichever IdP you use to fill out
|
|
|
|
all the fields. The redirect URL is
|
2022-08-28 20:18:44 +01:00
|
|
|
`https://[publicUrl]/auth/oidc.callback`.
|
2022-02-16 11:02:44 +00:00
|
|
|
'';
|
|
|
|
default = null;
|
|
|
|
type = lib.types.nullOr (lib.types.submodule {
|
|
|
|
options = {
|
|
|
|
clientId = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Authentication client identifier.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
clientSecretFile = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "File path containing the authentication secret.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
authUrl = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "OIDC authentication URL endpoint.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
tokenUrl = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "OIDC token URL endpoint.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
userinfoUrl = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "OIDC userinfo URL endpoint.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
usernameClaim = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
Specify which claims to derive user information from. Supports any
|
|
|
|
valid JSON path with the JWT payload
|
|
|
|
'';
|
|
|
|
default = "preferred_username";
|
|
|
|
};
|
|
|
|
displayName = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Display name for OIDC authentication.";
|
2022-02-16 11:02:44 +00:00
|
|
|
default = "OpenID";
|
|
|
|
};
|
|
|
|
scopes = lib.mkOption {
|
|
|
|
type = lib.types.listOf lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "OpenID authentication scopes.";
|
2022-02-16 11:02:44 +00:00
|
|
|
default = [ "openid" "profile" "email" ];
|
|
|
|
};
|
|
|
|
};
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
#
|
|
|
|
# Optional configuration
|
|
|
|
#
|
|
|
|
|
|
|
|
sslKeyFile = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.str;
|
|
|
|
default = null;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
File path that contains the Base64-encoded private key for HTTPS
|
|
|
|
termination. This is only required if you do not use an external reverse
|
|
|
|
proxy. See
|
2022-08-28 20:18:44 +01:00
|
|
|
[the documentation](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4).
|
2022-02-16 11:02:44 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
sslCertFile = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.str;
|
|
|
|
default = null;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
File path that contains the Base64-encoded certificate for HTTPS
|
|
|
|
termination. This is only required if you do not use an external reverse
|
|
|
|
proxy. See
|
2022-08-28 20:18:44 +01:00
|
|
|
[the documentation](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4).
|
2022-02-16 11:02:44 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
cdnUrl = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = "";
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
If using a Cloudfront/Cloudflare distribution or similar it can be set
|
|
|
|
using this option. This will cause paths to JavaScript files,
|
|
|
|
stylesheets and images to be updated to the hostname defined here. In
|
|
|
|
your CDN configuration the origin server should be set to public URL.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
forceHttps = lib.mkOption {
|
|
|
|
type = lib.types.bool;
|
|
|
|
default = true;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
Auto-redirect to HTTPS in production. The default is
|
2022-08-28 20:18:44 +01:00
|
|
|
`true` but you may set this to `false`
|
2022-02-16 11:02:44 +00:00
|
|
|
if you can be sure that SSL is terminated at an external loadbalancer.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
enableUpdateCheck = lib.mkOption {
|
|
|
|
type = lib.types.bool;
|
|
|
|
default = false;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
Have the installation check for updates by sending anonymized statistics
|
|
|
|
to the maintainers.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
concurrency = lib.mkOption {
|
|
|
|
type = lib.types.int;
|
|
|
|
default = 1;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
How many processes should be spawned. For a rough estimate, divide your
|
|
|
|
server's available memory by 512.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
maximumImportSize = lib.mkOption {
|
|
|
|
type = lib.types.int;
|
|
|
|
default = 5120000;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
The maximum size of document imports. Overriding this could be required
|
|
|
|
if you have especially large Word documents with embedded imagery.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
debugOutput = lib.mkOption {
|
|
|
|
type = lib.types.nullOr (lib.types.enum [ "http" ]);
|
|
|
|
default = null;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Set this to `http` log HTTP requests.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
slackIntegration = lib.mkOption {
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
For a complete Slack integration with search and posting to channels
|
|
|
|
this configuration is also needed. See here for details:
|
|
|
|
https://wiki.generaloutline.com/share/be25efd1-b3ef-4450-b8e5-c4a4fc11e02a
|
|
|
|
'';
|
|
|
|
default = null;
|
|
|
|
type = lib.types.nullOr (lib.types.submodule {
|
|
|
|
options = {
|
|
|
|
verificationTokenFile = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "File path containing the verification token.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
appId = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Application ID.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
messageActions = lib.mkOption {
|
|
|
|
type = lib.types.bool;
|
|
|
|
default = true;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Whether to enable message actions.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
googleAnalyticsId = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.str;
|
|
|
|
default = null;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
Optionally enable Google Analytics to track page views in the knowledge
|
|
|
|
base.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
sentryDsn = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.str;
|
|
|
|
default = null;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
|
|
|
Optionally enable [Sentry](https://sentry.io/) to
|
2022-02-16 11:02:44 +00:00
|
|
|
track errors and performance.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2022-11-21 08:33:08 +00:00
|
|
|
sentryTunnel = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.str;
|
|
|
|
default = null;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Optionally add a
|
|
|
|
[Sentry proxy tunnel](https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option)
|
|
|
|
for bypassing ad blockers in the UI.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2022-02-16 11:02:44 +00:00
|
|
|
logo = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.str;
|
|
|
|
default = null;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
Custom logo displayed on the authentication screen. This will be scaled
|
|
|
|
to a height of 60px.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
smtp = lib.mkOption {
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
To support sending outgoing transactional emails such as
|
|
|
|
"document updated" or "you've been invited" you'll need to provide
|
|
|
|
authentication for an SMTP server.
|
|
|
|
'';
|
|
|
|
default = null;
|
|
|
|
type = lib.types.nullOr (lib.types.submodule {
|
|
|
|
options = {
|
|
|
|
host = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-12-18 00:31:14 +00:00
|
|
|
description = lib.mdDoc "Host name or IP address of the SMTP server.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
|
|
type = lib.types.port;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "TCP port of the SMTP server.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
username = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Username to authenticate with.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
passwordFile = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
File path containing the password to authenticate with.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
fromEmail = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Sender email in outgoing mail.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
replyEmail = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Reply address in outgoing mail.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
tlsCiphers = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = "";
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Override SMTP cipher configuration.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
secure = lib.mkOption {
|
|
|
|
type = lib.types.bool;
|
|
|
|
default = true;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Use a secure SMTP connection.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
defaultLanguage = lib.mkOption {
|
|
|
|
type = lib.types.enum [
|
|
|
|
"da_DK"
|
|
|
|
"de_DE"
|
|
|
|
"en_US"
|
|
|
|
"es_ES"
|
|
|
|
"fa_IR"
|
|
|
|
"fr_FR"
|
|
|
|
"it_IT"
|
|
|
|
"ja_JP"
|
|
|
|
"ko_KR"
|
|
|
|
"nl_NL"
|
|
|
|
"pl_PL"
|
|
|
|
"pt_BR"
|
|
|
|
"pt_PT"
|
|
|
|
"ru_RU"
|
|
|
|
"sv_SE"
|
|
|
|
"th_TH"
|
|
|
|
"vi_VN"
|
|
|
|
"zh_CN"
|
|
|
|
"zh_TW"
|
|
|
|
];
|
|
|
|
default = "en_US";
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc ''
|
2022-02-16 11:02:44 +00:00
|
|
|
The default interface language. See
|
2022-08-28 20:18:44 +01:00
|
|
|
[translate.getoutline.com](https://translate.getoutline.com/)
|
2022-02-16 11:02:44 +00:00
|
|
|
for a list of available language codes and their rough percentage
|
|
|
|
translated.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2022-08-28 20:18:44 +01:00
|
|
|
rateLimiter.enable = lib.mkEnableOption (lib.mdDoc "rate limiter for the application web server");
|
2022-02-16 11:02:44 +00:00
|
|
|
rateLimiter.requests = lib.mkOption {
|
|
|
|
type = lib.types.int;
|
|
|
|
default = 5000;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Maximum number of requests in a throttling window.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
rateLimiter.durationWindow = lib.mkOption {
|
|
|
|
type = lib.types.int;
|
|
|
|
default = 60;
|
2022-08-28 20:18:44 +01:00
|
|
|
description = lib.mdDoc "Length of a throttling window.";
|
2022-02-16 11:02:44 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
|
|
users.users = lib.optionalAttrs (cfg.user == defaultUser) {
|
|
|
|
${defaultUser} = {
|
|
|
|
isSystemUser = true;
|
|
|
|
group = cfg.group;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
users.groups = lib.optionalAttrs (cfg.group == defaultUser) {
|
|
|
|
${defaultUser} = { };
|
|
|
|
};
|
|
|
|
|
|
|
|
systemd.tmpfiles.rules = [
|
|
|
|
"f ${cfg.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -"
|
|
|
|
"f ${cfg.utilsSecretFile} 0600 ${cfg.user} ${cfg.group} -"
|
|
|
|
"f ${cfg.storage.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -"
|
|
|
|
];
|
|
|
|
|
|
|
|
services.postgresql = lib.mkIf (cfg.databaseUrl == "local") {
|
|
|
|
enable = true;
|
|
|
|
ensureUsers = [{
|
|
|
|
name = "outline";
|
|
|
|
ensurePermissions."DATABASE outline" = "ALL PRIVILEGES";
|
|
|
|
}];
|
|
|
|
ensureDatabases = [ "outline" ];
|
|
|
|
};
|
|
|
|
|
|
|
|
services.redis.servers.outline = lib.mkIf (cfg.redisUrl == "local") {
|
|
|
|
enable = true;
|
|
|
|
user = config.services.outline.user;
|
|
|
|
port = 0; # Disable the TCP listener
|
|
|
|
};
|
|
|
|
|
|
|
|
systemd.services.outline = let
|
|
|
|
localRedisUrl = "redis+unix:///run/redis-outline/redis.sock";
|
|
|
|
localPostgresqlUrl = "postgres://localhost/outline?host=/run/postgresql";
|
|
|
|
|
|
|
|
# Create an outline-sequalize wrapper (a wrapper around the wrapper) that
|
|
|
|
# has the config file's path baked in. This is necessary because there is
|
|
|
|
# at least one occurrence of outline calling this from its own code.
|
|
|
|
sequelize = pkgs.writeShellScriptBin "outline-sequelize" ''
|
|
|
|
exec ${cfg.package}/bin/outline-sequelize \
|
|
|
|
--config $RUNTIME_DIRECTORY/database.json \
|
|
|
|
${cfg.sequelizeArguments} \
|
|
|
|
"$@"
|
|
|
|
'';
|
|
|
|
in {
|
|
|
|
description = "Outline wiki and knowledge base";
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
after = [ "networking.target" ]
|
|
|
|
++ lib.optional (cfg.databaseUrl == "local") "postgresql.service"
|
|
|
|
++ lib.optional (cfg.redisUrl == "local") "redis-outline.service";
|
|
|
|
requires = lib.optional (cfg.databaseUrl == "local") "postgresql.service"
|
|
|
|
++ lib.optional (cfg.redisUrl == "local") "redis-outline.service";
|
|
|
|
path = [
|
|
|
|
pkgs.openssl # Required by the preStart script
|
|
|
|
sequelize
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
environment = lib.mkMerge [
|
|
|
|
{
|
|
|
|
NODE_ENV = "production";
|
|
|
|
|
|
|
|
REDIS_URL = if cfg.redisUrl == "local" then localRedisUrl else cfg.redisUrl;
|
|
|
|
URL = cfg.publicUrl;
|
|
|
|
PORT = builtins.toString cfg.port;
|
|
|
|
|
|
|
|
AWS_ACCESS_KEY_ID = cfg.storage.accessKey;
|
|
|
|
AWS_REGION = cfg.storage.region;
|
|
|
|
AWS_S3_UPLOAD_BUCKET_URL = cfg.storage.uploadBucketUrl;
|
|
|
|
AWS_S3_UPLOAD_BUCKET_NAME = cfg.storage.uploadBucketName;
|
|
|
|
AWS_S3_UPLOAD_MAX_SIZE = builtins.toString cfg.storage.uploadMaxSize;
|
|
|
|
AWS_S3_FORCE_PATH_STYLE = builtins.toString cfg.storage.forcePathStyle;
|
|
|
|
AWS_S3_ACL = cfg.storage.acl;
|
|
|
|
|
|
|
|
CDN_URL = cfg.cdnUrl;
|
|
|
|
FORCE_HTTPS = builtins.toString cfg.forceHttps;
|
|
|
|
ENABLE_UPDATES = builtins.toString cfg.enableUpdateCheck;
|
|
|
|
WEB_CONCURRENCY = builtins.toString cfg.concurrency;
|
|
|
|
MAXIMUM_IMPORT_SIZE = builtins.toString cfg.maximumImportSize;
|
|
|
|
DEBUG = cfg.debugOutput;
|
|
|
|
GOOGLE_ANALYTICS_ID = lib.optionalString (cfg.googleAnalyticsId != null) cfg.googleAnalyticsId;
|
|
|
|
SENTRY_DSN = lib.optionalString (cfg.sentryDsn != null) cfg.sentryDsn;
|
2022-11-21 08:33:08 +00:00
|
|
|
SENTRY_TUNNEL = lib.optionalString (cfg.sentryTunnel != null) cfg.sentryTunnel;
|
2022-02-16 11:02:44 +00:00
|
|
|
TEAM_LOGO = lib.optionalString (cfg.logo != null) cfg.logo;
|
|
|
|
DEFAULT_LANGUAGE = cfg.defaultLanguage;
|
|
|
|
|
|
|
|
RATE_LIMITER_ENABLED = builtins.toString cfg.rateLimiter.enable;
|
|
|
|
RATE_LIMITER_REQUESTS = builtins.toString cfg.rateLimiter.requests;
|
|
|
|
RATE_LIMITER_DURATION_WINDOW = builtins.toString cfg.rateLimiter.durationWindow;
|
|
|
|
}
|
|
|
|
|
|
|
|
(lib.mkIf (cfg.slackAuthentication != null) {
|
|
|
|
SLACK_CLIENT_ID = cfg.slackAuthentication.clientId;
|
|
|
|
})
|
|
|
|
|
|
|
|
(lib.mkIf (cfg.googleAuthentication != null) {
|
|
|
|
GOOGLE_CLIENT_ID = cfg.googleAuthentication.clientId;
|
|
|
|
})
|
|
|
|
|
|
|
|
(lib.mkIf (cfg.azureAuthentication != null) {
|
|
|
|
AZURE_CLIENT_ID = cfg.azureAuthentication.clientId;
|
|
|
|
AZURE_RESOURCE_APP_ID = cfg.azureAuthentication.resourceAppId;
|
|
|
|
})
|
|
|
|
|
|
|
|
(lib.mkIf (cfg.oidcAuthentication != null) {
|
|
|
|
OIDC_CLIENT_ID = cfg.oidcAuthentication.clientId;
|
|
|
|
OIDC_AUTH_URI = cfg.oidcAuthentication.authUrl;
|
|
|
|
OIDC_TOKEN_URI = cfg.oidcAuthentication.tokenUrl;
|
|
|
|
OIDC_USERINFO_URI = cfg.oidcAuthentication.userinfoUrl;
|
|
|
|
OIDC_USERNAME_CLAIM = cfg.oidcAuthentication.usernameClaim;
|
|
|
|
OIDC_DISPLAY_NAME = cfg.oidcAuthentication.displayName;
|
|
|
|
OIDC_SCOPES = lib.concatStringsSep " " cfg.oidcAuthentication.scopes;
|
|
|
|
})
|
|
|
|
|
|
|
|
(lib.mkIf (cfg.slackIntegration != null) {
|
|
|
|
SLACK_APP_ID = cfg.slackIntegration.appId;
|
|
|
|
SLACK_MESSAGE_ACTIONS = builtins.toString cfg.slackIntegration.messageActions;
|
|
|
|
})
|
|
|
|
|
|
|
|
(lib.mkIf (cfg.smtp != null) {
|
|
|
|
SMTP_HOST = cfg.smtp.host;
|
|
|
|
SMTP_PORT = builtins.toString cfg.smtp.port;
|
|
|
|
SMTP_USERNAME = cfg.smtp.username;
|
|
|
|
SMTP_FROM_EMAIL = cfg.smtp.fromEmail;
|
|
|
|
SMTP_REPLY_EMAIL = cfg.smtp.replyEmail;
|
|
|
|
SMTP_TLS_CIPHERS = cfg.smtp.tlsCiphers;
|
|
|
|
SMTP_SECURE = builtins.toString cfg.smtp.secure;
|
|
|
|
})
|
|
|
|
];
|
|
|
|
|
|
|
|
preStart = ''
|
|
|
|
if [ ! -s ${lib.escapeShellArg cfg.secretKeyFile} ]; then
|
|
|
|
openssl rand -hex 32 > ${lib.escapeShellArg cfg.secretKeyFile}
|
|
|
|
fi
|
|
|
|
if [ ! -s ${lib.escapeShellArg cfg.utilsSecretFile} ]; then
|
|
|
|
openssl rand -hex 32 > ${lib.escapeShellArg cfg.utilsSecretFile}
|
|
|
|
fi
|
|
|
|
|
|
|
|
# The config file is required for the CLI, the DATABASE_URL environment
|
|
|
|
# variable is read by the app.
|
|
|
|
${if (cfg.databaseUrl == "local") then ''
|
|
|
|
cat <<EOF > $RUNTIME_DIRECTORY/database.json
|
|
|
|
{
|
|
|
|
"production": {
|
|
|
|
"dialect": "postgres",
|
|
|
|
"host": "/run/postgresql",
|
|
|
|
"username": null,
|
|
|
|
"password": null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
EOF
|
|
|
|
export DATABASE_URL=${lib.escapeShellArg localPostgresqlUrl}
|
|
|
|
export PGSSLMODE=disable
|
|
|
|
'' else ''
|
|
|
|
cat <<EOF > $RUNTIME_DIRECTORY/database.json
|
|
|
|
{
|
|
|
|
"production": {
|
|
|
|
"use_env_variable": "DATABASE_URL",
|
|
|
|
"dialect": "postgres",
|
|
|
|
"dialectOptions": {
|
|
|
|
"ssl": {
|
|
|
|
"rejectUnauthorized": false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"production-ssl-disabled": {
|
|
|
|
"use_env_variable": "DATABASE_URL",
|
|
|
|
"dialect": "postgres"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
EOF
|
|
|
|
export DATABASE_URL=${lib.escapeShellArg cfg.databaseUrl}
|
|
|
|
''}
|
|
|
|
|
|
|
|
cd $RUNTIME_DIRECTORY
|
|
|
|
${sequelize}/bin/outline-sequelize db:migrate
|
|
|
|
'';
|
|
|
|
|
|
|
|
script = ''
|
|
|
|
export SECRET_KEY="$(head -n1 ${lib.escapeShellArg cfg.secretKeyFile})"
|
|
|
|
export UTILS_SECRET="$(head -n1 ${lib.escapeShellArg cfg.utilsSecretFile})"
|
|
|
|
export AWS_SECRET_ACCESS_KEY="$(head -n1 ${lib.escapeShellArg cfg.storage.secretKeyFile})"
|
|
|
|
${lib.optionalString (cfg.slackAuthentication != null) ''
|
|
|
|
export SLACK_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.slackAuthentication.secretFile})"
|
|
|
|
''}
|
|
|
|
${lib.optionalString (cfg.googleAuthentication != null) ''
|
|
|
|
export GOOGLE_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.googleAuthentication.clientSecretFile})"
|
|
|
|
''}
|
|
|
|
${lib.optionalString (cfg.azureAuthentication != null) ''
|
|
|
|
export AZURE_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.azureAuthentication.clientSecretFile})"
|
|
|
|
''}
|
|
|
|
${lib.optionalString (cfg.oidcAuthentication != null) ''
|
|
|
|
export OIDC_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.oidcAuthentication.clientSecretFile})"
|
|
|
|
''}
|
|
|
|
${lib.optionalString (cfg.sslKeyFile != null) ''
|
|
|
|
export SSL_KEY="$(head -n1 ${lib.escapeShellArg cfg.sslKeyFile})"
|
|
|
|
''}
|
|
|
|
${lib.optionalString (cfg.sslCertFile != null) ''
|
|
|
|
export SSL_CERT="$(head -n1 ${lib.escapeShellArg cfg.sslCertFile})"
|
|
|
|
''}
|
|
|
|
${lib.optionalString (cfg.slackIntegration != null) ''
|
|
|
|
export SLACK_VERIFICATION_TOKEN="$(head -n1 ${lib.escapeShellArg cfg.slackIntegration.verificationTokenFile})"
|
|
|
|
''}
|
|
|
|
${lib.optionalString (cfg.smtp != null) ''
|
|
|
|
export SMTP_PASSWORD="$(head -n1 ${lib.escapeShellArg cfg.smtp.passwordFile})"
|
|
|
|
''}
|
|
|
|
|
|
|
|
${if (cfg.databaseUrl == "local") then ''
|
|
|
|
export DATABASE_URL=${lib.escapeShellArg localPostgresqlUrl}
|
|
|
|
export PGSSLMODE=disable
|
|
|
|
'' else ''
|
|
|
|
export DATABASE_URL=${lib.escapeShellArg cfg.databaseUrl}
|
|
|
|
''}
|
|
|
|
|
|
|
|
${cfg.package}/bin/outline-server
|
|
|
|
'';
|
|
|
|
|
|
|
|
serviceConfig = {
|
|
|
|
User = cfg.user;
|
|
|
|
Group = cfg.group;
|
|
|
|
Restart = "always";
|
|
|
|
ProtectSystem = "strict";
|
|
|
|
PrivateHome = true;
|
|
|
|
PrivateTmp = true;
|
|
|
|
UMask = "0007";
|
|
|
|
|
|
|
|
StateDirectory = "outline";
|
|
|
|
StateDirectoryMode = "0750";
|
|
|
|
RuntimeDirectory = "outline";
|
|
|
|
RuntimeDirectoryMode = "0750";
|
|
|
|
# This working directory is required to find stuff like the set of
|
|
|
|
# onboarding files:
|
|
|
|
WorkingDirectory = "${cfg.package}/share/outline/build";
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|