{ config, lib, pkgs, ... }: with lib; let cfg = config.services.codimd; prettyJSON = conf: pkgs.runCommand "codimd-config.json" { preferLocalBuild = true; } '' echo '${builtins.toJSON conf}' | ${pkgs.jq}/bin/jq \ '{production:del(.[]|nulls)|del(.[][]?|nulls)}' > $out ''; in { options.services.codimd = { enable = mkEnableOption "the CodiMD Markdown Editor"; groups = mkOption { type = types.listOf types.str; default = []; description = '' Groups to which the codimd user should be added. ''; }; workDir = mkOption { type = types.path; default = "/var/lib/codimd"; description = '' Working directory for the CodiMD service. ''; }; configuration = { debug = mkEnableOption "debug mode"; domain = mkOption { type = types.nullOr types.str; default = null; example = "codimd.org"; description = '' Domain name for the CodiMD instance. ''; }; urlPath = mkOption { type = types.nullOr types.str; default = null; example = "/url/path/to/codimd"; description = '' Path under which CodiMD 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 = "/var/run/codimd.sock"; description = '' Specify where a UNIX domain socket should be placed. ''; }; allowOrigin = mkOption { type = types.listOf types.str; default = []; example = [ "localhost" "codimd.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 = '' Wheter 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 = literalExample '' { 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 = true; 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. CodiMD 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 = literalExample '' { dialect = "sqlite"; storage = "/var/lib/codimd/db.codimd.sqlite"; } ''; description = '' Specify the configuration for sequelize. CodiMD 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/codimd/codimd.key"; description = '' Path to the SSL key. Needed when is enabled. ''; }; sslCertPath = mkOption { type = types.nullOr types.str; default = null; example = "/var/lib/codimd/codimd.crt"; description = '' Path to the SSL cert. Needed when is enabled. ''; }; sslCAPath = mkOption { type = types.listOf types.str; default = []; example = [ "/var/lib/codimd/ca.crt" ]; description = '' SSL ca chain. Needed when is enabled. ''; }; dhParamPath = mkOption { type = types.nullOr types.str; default = null; example = "/var/lib/codimd/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 CodiMD should use. Note that is enabled for the CodiMD systemd service by default. (Non-canonical paths are relative to CodiMD'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 CodiMD'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 CodiMD'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 CodiMD'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 CodiMD's base directory) ''; }; errorPath = mkOption { type = types.nullOr types.str; default = null; defaultText = "./public/views/error.ejs"; description = '' Path to the error template file. (Non-canonical paths are relative to CodiMD's base directory) ''; }; prettyPath = mkOption { type = types.nullOr types.str; default = null; defaultText = "./public/views/pretty.ejs"; description = '' Path to the pretty template file. (Non-canonical paths are relative to CodiMD's base directory) ''; }; slidePath = mkOption { type = types.nullOr types.str; default = null; defaultText = "./public/views/slide.hbs"; description = '' Path to the slide template file. (Non-canonical paths are relative to CodiMD's base directory) ''; }; uploadsPath = mkOption { type = types.str; default = "${cfg.workDir}/uploads"; defaultText = "/var/lib/codimd/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 = '' Wether 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. ''; }; 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 CodiMD. By default is used. ''; }; useridField = mkOption { type = types.str; example = "uid"; description = '' LDAP field which is a unique identifier for users on CodiMD. ''; }; 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 = [ "Hackmd-users" "Codimd-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."; }; }; }; config = mkIf cfg.enable { assertions = [ { assertion = cfg.configuration.db == {} -> ( cfg.configuration.dbURL != "" && cfg.configuration.dbURL != null ); message = "Database configuration for CodiMD missing."; } ]; users.groups.codimd = {}; users.users.codimd = { description = "CodiMD service user"; group = "codimd"; extraGroups = cfg.groups; home = cfg.workDir; createHome = true; }; systemd.services.codimd = { description = "CodiMD Service"; wantedBy = [ "multi-user.target" ]; after = [ "networking.target" ]; preStart = '' mkdir -p ${cfg.workDir} chown -R codimd: ${cfg.workDir} ''; serviceConfig = { WorkingDirectory = cfg.workDir; ExecStart = "${pkgs.codimd}/bin/codimd"; Environment = [ "CMD_CONFIG_FILE=${prettyJSON cfg.configuration}" "NODE_ENV=production" ]; Restart = "always"; User = "codimd"; PermissionsStartOnly = true; PrivateTmp = true; }; }; }; }