{ config, lib, pkgs, ... }: with lib; let cfg = config.services.hedgedoc; # 21.03 will not be an official release - it was instead 21.05. This # versionAtLeast statement remains set to 21.03 for backwards compatibility. # See https://github.com/NixOS/nixpkgs/pull/108899 and # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md. name = if versionAtLeast config.system.stateVersion "21.03" then "hedgedoc" else "codimd"; prettyJSON = conf: pkgs.runCommandLocal "hedgedoc-config.json" { nativeBuildInputs = [ pkgs.jq ]; } '' echo '${builtins.toJSON conf}' | jq \ '{production:del(.[]|nulls)|del(.[][]?|nulls)}' > $out ''; in { imports = [ (mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ]) ]; options.services.hedgedoc = { enable = mkEnableOption "the HedgeDoc Markdown Editor"; groups = mkOption { type = types.listOf types.str; default = []; description = '' Groups to which the service user should be added. ''; }; workDir = mkOption { type = types.path; default = "/var/lib/${name}"; description = '' Working directory for the HedgeDoc service. ''; }; configuration = { debug = mkEnableOption "debug mode"; domain = mkOption { type = types.nullOr types.str; default = null; example = "hedgedoc.org"; description = '' Domain name for the HedgeDoc instance. ''; }; urlPath = mkOption { type = types.nullOr types.str; default = null; example = "/url/path/to/hedgedoc"; description = '' Path under which HedgeDoc is accessible. ''; }; host = mkOption { type = types.str; default = "localhost"; description = '' Address to listen on. ''; }; port = mkOption { type = types.int; default = 3000; example = 80; description = '' Port to listen on. ''; }; path = mkOption { type = types.nullOr types.str; default = null; example = "/run/hedgedoc.sock"; description = '' Specify where a UNIX domain socket should be placed. ''; }; allowOrigin = mkOption { type = types.listOf types.str; default = []; example = [ "localhost" "hedgedoc.org" ]; description = '' List of domains to whitelist. ''; }; useSSL = mkOption { type = types.bool; default = false; description = '' Enable to use SSL server. This will also enable . ''; }; hsts = { enable = mkOption { type = types.bool; default = true; description = '' Whether to enable HSTS if HTTPS is also enabled. ''; }; maxAgeSeconds = mkOption { type = types.int; default = 31536000; description = '' Max duration for clients to keep the HSTS status. ''; }; includeSubdomains = mkOption { type = types.bool; default = true; description = '' Whether to include subdomains in HSTS. ''; }; preload = mkOption { type = types.bool; default = true; description = '' Whether to allow preloading of the site's HSTS status. ''; }; }; csp = mkOption { type = types.nullOr types.attrs; default = null; example = literalExpression '' { enable = true; directives = { scriptSrc = "trustworthy.scripts.example.com"; }; upgradeInsecureRequest = "auto"; addDefaults = true; } ''; description = '' Specify the Content Security Policy which is passed to Helmet. For configuration details see https://helmetjs.github.io/docs/csp/. ''; }; protocolUseSSL = mkOption { type = types.bool; default = false; description = '' Enable to use TLS for resource paths. This only applies when is set. ''; }; urlAddPort = mkOption { type = types.bool; default = false; description = '' Enable to add the port to callback URLs. This only applies when is set and only for ports other than 80 and 443. ''; }; useCDN = mkOption { type = types.bool; default = false; description = '' Whether to use CDN resources or not. ''; }; allowAnonymous = mkOption { type = types.bool; default = true; description = '' Whether to allow anonymous usage. ''; }; allowAnonymousEdits = mkOption { type = types.bool; default = false; description = '' Whether to allow guests to edit existing notes with the `freely' permission, when is enabled. ''; }; allowFreeURL = mkOption { type = types.bool; default = false; description = '' Whether to allow note creation by accessing a nonexistent note URL. ''; }; defaultPermission = mkOption { type = types.enum [ "freely" "editable" "limited" "locked" "private" ]; default = "editable"; description = '' Default permissions for notes. This only applies for signed-in users. ''; }; dbURL = mkOption { type = types.nullOr types.str; default = null; example = '' postgres://user:pass@host:5432/dbname ''; description = '' Specify which database to use. HedgeDoc supports mysql, postgres, sqlite and mssql. See https://sequelize.readthedocs.io/en/v3/ for more information. Note: This option overrides . ''; }; db = mkOption { type = types.attrs; default = {}; example = literalExpression '' { dialect = "sqlite"; storage = "/var/lib/${name}/db.${name}.sqlite"; } ''; description = '' Specify the configuration for sequelize. HedgeDoc supports mysql, postgres, sqlite and mssql. See https://sequelize.readthedocs.io/en/v3/ for more information. Note: This option overrides . ''; }; sslKeyPath= mkOption { type = types.nullOr types.str; default = null; example = "/var/lib/hedgedoc/hedgedoc.key"; description = '' Path to the SSL key. Needed when is enabled. ''; }; sslCertPath = mkOption { type = types.nullOr types.str; default = null; example = "/var/lib/hedgedoc/hedgedoc.crt"; description = '' Path to the SSL cert. Needed when is enabled. ''; }; sslCAPath = mkOption { type = types.listOf types.str; default = []; example = [ "/var/lib/hedgedoc/ca.crt" ]; description = '' SSL ca chain. Needed when is enabled. ''; }; dhParamPath = mkOption { type = types.nullOr types.str; default = null; example = "/var/lib/hedgedoc/dhparam.pem"; description = '' Path to the SSL dh params. Needed when is enabled. ''; }; tmpPath = mkOption { type = types.str; default = "/tmp"; description = '' Path to the temp directory HedgeDoc should use. Note that is enabled for the HedgeDoc systemd service by default. (Non-canonical paths are relative to HedgeDoc's base directory) ''; }; defaultNotePath = mkOption { type = types.nullOr types.str; default = "./public/default.md"; description = '' Path to the default Note file. (Non-canonical paths are relative to HedgeDoc's base directory) ''; }; docsPath = mkOption { type = types.nullOr types.str; default = "./public/docs"; description = '' Path to the docs directory. (Non-canonical paths are relative to HedgeDoc's base directory) ''; }; indexPath = mkOption { type = types.nullOr types.str; default = "./public/views/index.ejs"; description = '' Path to the index template file. (Non-canonical paths are relative to HedgeDoc's base directory) ''; }; hackmdPath = mkOption { type = types.nullOr types.str; default = "./public/views/hackmd.ejs"; description = '' Path to the hackmd template file. (Non-canonical paths are relative to HedgeDoc's base directory) ''; }; errorPath = mkOption { type = types.nullOr types.str; default = null; defaultText = literalExpression "./public/views/error.ejs"; description = '' Path to the error template file. (Non-canonical paths are relative to HedgeDoc's base directory) ''; }; prettyPath = mkOption { type = types.nullOr types.str; default = null; defaultText = literalExpression "./public/views/pretty.ejs"; description = '' Path to the pretty template file. (Non-canonical paths are relative to HedgeDoc's base directory) ''; }; slidePath = mkOption { type = types.nullOr types.str; default = null; defaultText = literalExpression "./public/views/slide.hbs"; description = '' Path to the slide template file. (Non-canonical paths are relative to HedgeDoc's base directory) ''; }; uploadsPath = mkOption { type = types.str; default = "${cfg.workDir}/uploads"; defaultText = literalExpression "/var/lib/${name}/uploads"; description = '' Path under which uploaded files are saved. ''; }; sessionName = mkOption { type = types.str; default = "connect.sid"; description = '' Specify the name of the session cookie. ''; }; sessionSecret = mkOption { type = types.nullOr types.str; default = null; description = '' Specify the secret used to sign the session cookie. If unset, one will be generated on startup. ''; }; sessionLife = mkOption { type = types.int; default = 1209600000; description = '' Session life time in milliseconds. ''; }; heartbeatInterval = mkOption { type = types.int; default = 5000; description = '' Specify the socket.io heartbeat interval. ''; }; heartbeatTimeout = mkOption { type = types.int; default = 10000; description = '' Specify the socket.io heartbeat timeout. ''; }; documentMaxLength = mkOption { type = types.int; default = 100000; description = '' Specify the maximum document length. ''; }; email = mkOption { type = types.bool; default = true; description = '' Whether to enable email sign-in. ''; }; allowEmailRegister = mkOption { type = types.bool; default = true; description = '' Whether to enable email registration. ''; }; allowGravatar = mkOption { type = types.bool; default = true; description = '' Whether to use gravatar as profile picture source. ''; }; imageUploadType = mkOption { type = types.enum [ "imgur" "s3" "minio" "filesystem" ]; default = "filesystem"; description = '' Specify where to upload images. ''; }; minio = mkOption { type = types.nullOr (types.submodule { options = { accessKey = mkOption { type = types.str; description = '' Minio access key. ''; }; secretKey = mkOption { type = types.str; description = '' Minio secret key. ''; }; endpoint = mkOption { type = types.str; description = '' Minio endpoint. ''; }; port = mkOption { type = types.int; default = 9000; description = '' Minio listen port. ''; }; secure = mkOption { type = types.bool; default = true; description = '' Whether to use HTTPS for Minio. ''; }; }; }); default = null; description = "Configure the minio third-party integration."; }; s3 = mkOption { type = types.nullOr (types.submodule { options = { accessKeyId = mkOption { type = types.str; description = '' AWS access key id. ''; }; secretAccessKey = mkOption { type = types.str; description = '' AWS access key. ''; }; region = mkOption { type = types.str; description = '' AWS S3 region. ''; }; }; }); default = null; description = "Configure the s3 third-party integration."; }; s3bucket = mkOption { type = types.nullOr types.str; default = null; description = '' Specify the bucket name for upload types s3 and minio. ''; }; allowPDFExport = mkOption { type = types.bool; default = true; description = '' Whether to enable PDF exports. ''; }; imgur.clientId = mkOption { type = types.nullOr types.str; default = null; description = '' Imgur API client ID. ''; }; azure = mkOption { type = types.nullOr (types.submodule { options = { connectionString = mkOption { type = types.str; description = '' Azure Blob Storage connection string. ''; }; container = mkOption { type = types.str; description = '' Azure Blob Storage container name. It will be created if non-existent. ''; }; }; }); default = null; description = "Configure the azure third-party integration."; }; oauth2 = mkOption { type = types.nullOr (types.submodule { options = { authorizationURL = mkOption { type = types.str; description = '' Specify the OAuth authorization URL. ''; }; tokenURL = mkOption { type = types.str; description = '' Specify the OAuth token URL. ''; }; baseURL = mkOption { type = with types; nullOr str; default = null; description = '' Specify the OAuth base URL. ''; }; userProfileURL = mkOption { type = with types; nullOr str; default = null; description = '' Specify the OAuth userprofile URL. ''; }; userProfileUsernameAttr = mkOption { type = with types; nullOr str; default = null; description = '' Specify the name of the attribute for the username from the claim. ''; }; userProfileDisplayNameAttr = mkOption { type = with types; nullOr str; default = null; description = '' Specify the name of the attribute for the display name from the claim. ''; }; userProfileEmailAttr = mkOption { type = with types; nullOr str; default = null; description = '' Specify the name of the attribute for the email from the claim. ''; }; scope = mkOption { type = with types; nullOr str; default = null; description = '' Specify the OAuth scope. ''; }; providerName = mkOption { type = with types; nullOr str; default = null; description = '' Specify the name to be displayed for this strategy. ''; }; rolesClaim = mkOption { type = with types; nullOr str; default = null; description = '' Specify the role claim name. ''; }; accessRole = mkOption { type = with types; nullOr str; default = null; description = '' Specify role which should be included in the ID token roles claim to grant access ''; }; clientID = mkOption { type = types.str; description = '' Specify the OAuth client ID. ''; }; clientSecret = mkOption { type = types.str; description = '' Specify the OAuth client secret. ''; }; }; }); default = null; description = "Configure the OAuth integration."; }; facebook = mkOption { type = types.nullOr (types.submodule { options = { clientID = mkOption { type = types.str; description = '' Facebook API client ID. ''; }; clientSecret = mkOption { type = types.str; description = '' Facebook API client secret. ''; }; }; }); default = null; description = "Configure the facebook third-party integration"; }; twitter = mkOption { type = types.nullOr (types.submodule { options = { consumerKey = mkOption { type = types.str; description = '' Twitter API consumer key. ''; }; consumerSecret = mkOption { type = types.str; description = '' Twitter API consumer secret. ''; }; }; }); default = null; description = "Configure the Twitter third-party integration."; }; github = mkOption { type = types.nullOr (types.submodule { options = { clientID = mkOption { type = types.str; description = '' GitHub API client ID. ''; }; clientSecret = mkOption { type = types.str; description = '' Github API client secret. ''; }; }; }); default = null; description = "Configure the GitHub third-party integration."; }; gitlab = mkOption { type = types.nullOr (types.submodule { options = { baseURL = mkOption { type = types.str; default = ""; description = '' GitLab API authentication endpoint. Only needed for other endpoints than gitlab.com. ''; }; clientID = mkOption { type = types.str; description = '' GitLab API client ID. ''; }; clientSecret = mkOption { type = types.str; description = '' GitLab API client secret. ''; }; scope = mkOption { type = types.enum [ "api" "read_user" ]; default = "api"; description = '' GitLab API requested scope. GitLab snippet import/export requires api scope. ''; }; }; }); default = null; description = "Configure the GitLab third-party integration."; }; mattermost = mkOption { type = types.nullOr (types.submodule { options = { baseURL = mkOption { type = types.str; description = '' Mattermost authentication endpoint. ''; }; clientID = mkOption { type = types.str; description = '' Mattermost API client ID. ''; }; clientSecret = mkOption { type = types.str; description = '' Mattermost API client secret. ''; }; }; }); default = null; description = "Configure the Mattermost third-party integration."; }; dropbox = mkOption { type = types.nullOr (types.submodule { options = { clientID = mkOption { type = types.str; description = '' Dropbox API client ID. ''; }; clientSecret = mkOption { type = types.str; description = '' Dropbox API client secret. ''; }; appKey = mkOption { type = types.str; description = '' Dropbox app key. ''; }; }; }); default = null; description = "Configure the Dropbox third-party integration."; }; google = mkOption { type = types.nullOr (types.submodule { options = { clientID = mkOption { type = types.str; description = '' Google API client ID. ''; }; clientSecret = mkOption { type = types.str; description = '' Google API client secret. ''; }; }; }); default = null; description = "Configure the Google third-party integration."; }; ldap = mkOption { type = types.nullOr (types.submodule { options = { providerName = mkOption { type = types.str; default = ""; description = '' Optional name to be displayed at login form, indicating the LDAP provider. ''; }; url = mkOption { type = types.str; example = "ldap://localhost"; description = '' URL of LDAP server. ''; }; bindDn = mkOption { type = types.str; description = '' Bind DN for LDAP access. ''; }; bindCredentials = mkOption { type = types.str; description = '' Bind credentials for LDAP access. ''; }; searchBase = mkOption { type = types.str; example = "o=users,dc=example,dc=com"; description = '' LDAP directory to begin search from. ''; }; searchFilter = mkOption { type = types.str; example = "(uid={{username}})"; description = '' LDAP filter to search with. ''; }; searchAttributes = mkOption { type = types.listOf types.str; example = [ "displayName" "mail" ]; description = '' LDAP attributes to search with. ''; }; userNameField = mkOption { type = types.str; default = ""; description = '' LDAP field which is used as the username on HedgeDoc. By default is used. ''; }; useridField = mkOption { type = types.str; example = "uid"; description = '' LDAP field which is a unique identifier for users on HedgeDoc. ''; }; tlsca = mkOption { type = types.str; example = "server-cert.pem,root.pem"; description = '' Root CA for LDAP TLS in PEM format. ''; }; }; }); default = null; description = "Configure the LDAP integration."; }; saml = mkOption { type = types.nullOr (types.submodule { options = { idpSsoUrl = mkOption { type = types.str; example = "https://idp.example.com/sso"; description = '' IdP authentication endpoint. ''; }; idpCert = mkOption { type = types.path; example = "/path/to/cert.pem"; description = '' Path to IdP certificate file in PEM format. ''; }; issuer = mkOption { type = types.str; default = ""; description = '' Optional identity of the service provider. This defaults to the server URL. ''; }; identifierFormat = mkOption { type = types.str; default = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"; description = '' Optional name identifier format. ''; }; groupAttribute = mkOption { type = types.str; default = ""; example = "memberOf"; description = '' Optional attribute name for group list. ''; }; externalGroups = mkOption { type = types.listOf types.str; default = []; example = [ "Temporary-staff" "External-users" ]; description = '' Excluded group names. ''; }; requiredGroups = mkOption { type = types.listOf types.str; default = []; example = [ "Hedgedoc-Users" ]; description = '' Required group names. ''; }; attribute = { id = mkOption { type = types.str; default = ""; description = '' Attribute map for `id'. Defaults to `NameID' of SAML response. ''; }; username = mkOption { type = types.str; default = ""; description = '' Attribute map for `username'. Defaults to `NameID' of SAML response. ''; }; email = mkOption { type = types.str; default = ""; description = '' Attribute map for `email'. Defaults to `NameID' of SAML response if has the default value. ''; }; }; }; }); default = null; description = "Configure the SAML integration."; }; }; environmentFile = mkOption { type = with types; nullOr path; default = null; example = "/var/lib/hedgedoc/hedgedoc.env"; description = '' Environment file as defined in systemd.exec5 . Secrets may be passed to the service without adding them to the world-readable Nix store, by specifying placeholder variables as the option value in Nix and setting these variables accordingly in the environment file. # snippet of HedgeDoc-related config services.hedgedoc.configuration.dbURL = "postgres://hedgedoc:\''${DB_PASSWORD}@db-host:5432/hedgedocdb"; services.hedgedoc.configuration.minio.secretKey = "$MINIO_SECRET_KEY"; # content of the environment file DB_PASSWORD=verysecretdbpassword MINIO_SECRET_KEY=verysecretminiokey Note that this file needs to be available on the host on which HedgeDoc is running. ''; }; package = mkOption { type = types.package; default = pkgs.hedgedoc; defaultText = literalExpression "pkgs.hedgedoc"; description = '' Package that provides HedgeDoc. ''; }; }; config = mkIf cfg.enable { assertions = [ { assertion = cfg.configuration.db == {} -> ( cfg.configuration.dbURL != "" && cfg.configuration.dbURL != null ); message = "Database configuration for HedgeDoc missing."; } ]; users.groups.${name} = {}; users.users.${name} = { description = "HedgeDoc service user"; group = name; extraGroups = cfg.groups; home = cfg.workDir; createHome = true; isSystemUser = true; }; systemd.services.hedgedoc = { description = "HedgeDoc Service"; wantedBy = [ "multi-user.target" ]; after = [ "networking.target" ]; preStart = '' ${pkgs.envsubst}/bin/envsubst \ -o ${cfg.workDir}/config.json \ -i ${prettyJSON cfg.configuration} ''; serviceConfig = { WorkingDirectory = cfg.workDir; StateDirectory = [ cfg.workDir cfg.configuration.uploadsPath ]; ExecStart = "${cfg.package}/bin/hedgedoc"; EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; Environment = [ "CMD_CONFIG_FILE=${cfg.workDir}/config.json" "NODE_ENV=production" ]; Restart = "always"; User = name; PrivateTmp = true; }; }; }; }