diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 17c21be22f1..b128568bdf5 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -683,6 +683,7 @@
./services/web-apps/atlassian/confluence.nix
./services/web-apps/atlassian/crowd.nix
./services/web-apps/atlassian/jira.nix
+ ./services/web-apps/codimd.nix
./services/web-apps/frab.nix
./services/web-apps/mattermost.nix
./services/web-apps/nexus.nix
diff --git a/nixos/modules/services/web-apps/codimd.nix b/nixos/modules/services/web-apps/codimd.nix
new file mode 100644
index 00000000000..5368e8b0e66
--- /dev/null
+++ b/nixos/modules/services/web-apps/codimd.nix
@@ -0,0 +1,958 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.codimd;
+
+ prettyJSON = conf:
+ pkgs.runCommand "codimd-config.json" { } ''
+ 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;
+ default = "";
+ description = ''
+ Minio access key.
+ '';
+ };
+ secretKey = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ Minio secret key.
+ '';
+ };
+ endpoint = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ default = "";
+ description = ''
+ AWS access key id.
+ '';
+ };
+ secretAccessKey = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ AWS access key.
+ '';
+ };
+ region = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ default = "";
+ description = ''
+ Azure Blob Storage connection string.
+ '';
+ };
+ container = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ default = "";
+ description = ''
+ Specify the OAuth authorization URL.
+ '';
+ };
+ tokenURL = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ Specify the OAuth token URL.
+ '';
+ };
+ clientID = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ Specify the OAuth client ID.
+ '';
+ };
+ clientSecret = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ default = "";
+ description = ''
+ Facebook API client ID.
+ '';
+ };
+ clientSecret = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ default = "";
+ description = ''
+ Twitter API consumer key.
+ '';
+ };
+ consumerSecret = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ default = "";
+ description = ''
+ GitHub API client ID.
+ '';
+ };
+ clientSecret = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ default = "";
+ description = ''
+ GitLab API client ID.
+ '';
+ };
+ clientSecret = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ default = "";
+ description = ''
+ Mattermost authentication endpoint.
+ '';
+ };
+ clientID = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ Mattermost API client ID.
+ '';
+ };
+ clientSecret = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ default = "";
+ description = ''
+ Dropbox API client ID.
+ '';
+ };
+ clientSecret = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ Dropbox API client secret.
+ '';
+ };
+ appKey = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ default = "";
+ description = ''
+ Google API client ID.
+ '';
+ };
+ clientSecret = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ default = "";
+ example = "ldap://localhost";
+ description = ''
+ URL of LDAP server.
+ '';
+ };
+ bindDn = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ Bind DN for LDAP access.
+ '';
+ };
+ bindCredentials = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ Bind credentials for LDAP access.
+ '';
+ };
+ searchBase = mkOption {
+ type = types.str;
+ default = "";
+ example = "o=users,dc=example,dc=com";
+ description = ''
+ LDAP directory to begin search from.
+ '';
+ };
+ searchFilter = mkOption {
+ type = types.str;
+ default = "";
+ example = "(uid={{username}})";
+ description = ''
+ LDAP filter to search with.
+ '';
+ };
+ searchAttributes = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ 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;
+ default = "";
+ example = "uid";
+ description = ''
+ LDAP field which is a unique identifier for users on CodiMD.
+ '';
+ };
+ tlsca = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ default = "";
+ example = "https://idp.example.com/sso";
+ description = ''
+ IdP authentication endpoint.
+ '';
+ };
+ idPCert = mkOption {
+ type = types.str;
+ default = "";
+ 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;
+ };
+ };
+ };
+}