From 45c96d34722c5cd2e97d53211bffcfec855de6ef Mon Sep 17 00:00:00 2001 From: Thary Date: Fri, 5 Sep 2025 15:33:30 +0300 Subject: [PATCH] feat: HedgeDoc module(#168) Reviewed-on: https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config/pulls/168 Reviewed-by: Inex Code Co-authored-by: Thary Co-committed-by: Thary --- sp-modules/hedgedoc/config-paths-needed.json | 11 ++ sp-modules/hedgedoc/flake.nix | 39 ++++ .../hedgedoc/hedgedocClientSecretAsFile.patch | 13 ++ sp-modules/hedgedoc/icon.svg | 6 + sp-modules/hedgedoc/module.nix | 186 ++++++++++++++++++ 5 files changed, 255 insertions(+) create mode 100644 sp-modules/hedgedoc/config-paths-needed.json create mode 100644 sp-modules/hedgedoc/flake.nix create mode 100644 sp-modules/hedgedoc/hedgedocClientSecretAsFile.patch create mode 100644 sp-modules/hedgedoc/icon.svg create mode 100644 sp-modules/hedgedoc/module.nix diff --git a/sp-modules/hedgedoc/config-paths-needed.json b/sp-modules/hedgedoc/config-paths-needed.json new file mode 100644 index 0000000..0b9c963 --- /dev/null +++ b/sp-modules/hedgedoc/config-paths-needed.json @@ -0,0 +1,11 @@ +[ + [ "selfprivacy", "domain" ], + [ "selfprivacy", "modules", "auth", "enable" ], + [ "selfprivacy", "modules", "hedgedoc" ], + [ "selfprivacy", "passthru", "auth", "mkOAuth2ClientSecretFP" ], + [ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ], + [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], + [ "selfprivacy", "sso", "enable" ], + [ "selfprivacy", "useBinds" ], + [ "services", "kanidm", "serverSettings", "origin" ] +] diff --git a/sp-modules/hedgedoc/flake.nix b/sp-modules/hedgedoc/flake.nix new file mode 100644 index 0000000..65c8600 --- /dev/null +++ b/sp-modules/hedgedoc/flake.nix @@ -0,0 +1,39 @@ +{ + description = "HedgeDoc module"; + + outputs = { ... }: + { + nixosModules.default = import ./module.nix; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: + { + spModuleSchemaVersion = 1; + id = "hedgedoc"; + name = "HedgeDoc"; + description = "HedgeDoc is an open-source, web-based, self-hosted, collaborative markdown editor."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = true; + isRequired = false; + backupDescription = "Notes and attachments."; + systemdServices = [ + "hedgedoc.service" + ]; + folders = [ + "/var/lib/hedgedoc" + ]; + postgreDatabases = [ + "hedgedoc" + ]; + license = [ + lib.licenses.agpl3Only + ]; + homepage = "https://hedgedoc.org"; + sourcePage = "https://github.com/hedgedoc/hedgedoc"; + supportLevel = "normal"; + sso = { + userGroup = "sp.hedgedoc.users"; + }; + }; + }; +} diff --git a/sp-modules/hedgedoc/hedgedocClientSecretAsFile.patch b/sp-modules/hedgedoc/hedgedocClientSecretAsFile.patch new file mode 100644 index 0000000..679a828 --- /dev/null +++ b/sp-modules/hedgedoc/hedgedocClientSecretAsFile.patch @@ -0,0 +1,13 @@ +diff --git a/lib/web/auth/oauth2/index.js b/lib/web/auth/oauth2/index.js +index b0ffa5e8a..01e16e152 100644 +--- a/lib/web/auth/oauth2/index.js ++++ b/lib/web/auth/oauth2/index.js +@@ -134,7 +134,7 @@ passport.use(new OAuth2CustomStrategy({ + authorizationURL: config.oauth2.authorizationURL, + tokenURL: config.oauth2.tokenURL, + clientID: config.oauth2.clientID, +- clientSecret: config.oauth2.clientSecret, ++ clientSecret: require("fs").readFileSync(config.oauth2.clientSecret, 'utf8').split("\n")[0], + callbackURL: config.serverURL + '/auth/oauth2/callback', + userProfileURL: config.oauth2.userProfileURL, + scope: config.oauth2.scope, diff --git a/sp-modules/hedgedoc/icon.svg b/sp-modules/hedgedoc/icon.svg new file mode 100644 index 0000000..af6bd81 --- /dev/null +++ b/sp-modules/hedgedoc/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/sp-modules/hedgedoc/module.nix b/sp-modules/hedgedoc/module.nix new file mode 100644 index 0000000..8f0ad33 --- /dev/null +++ b/sp-modules/hedgedoc/module.nix @@ -0,0 +1,186 @@ +{ + config, + lib, + pkgs, + ... +}: +let + sp = config.selfprivacy; + cfg = sp.modules.hedgedoc; + + oauthClientID = "hedgedoc"; + auth-passthru = config.selfprivacy.passthru.auth; + oauth2-provider-origin = config.services.kanidm.serverSettings.origin; + usersGroup = "sp.hedgedoc.users"; + oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP oauthClientID; +in +{ + options.selfprivacy.modules.hedgedoc = { + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable HedgeDoc"; + }) + // { + meta = { + type = "enable"; + }; + }; + location = + (lib.mkOption { + type = lib.types.str; + description = "HedgeDoc location"; + }) + // { + meta = { + type = "location"; + }; + }; + subdomain = + (lib.mkOption { + default = "hedgedoc"; + type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; + description = "Subdomain"; + }) + // { + meta = { + widget = "subdomain"; + type = "string"; + regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; + weight = 0; + }; + }; + allowAnonymous = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Allow usage (e.g., notes creation) without an account"; + }) + // { + meta = { + type = "bool"; + weight = 1; + }; + }; + allowLibravatar = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Use Libravatar as profile picture source"; + }) + // { + meta = { + type = "bool"; + weight = 2; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = sp.sso.enable; + message = "HedgeDoc cannot be enabled when SSO is disabled."; + } + ]; + + fileSystems = lib.mkIf sp.useBinds { + "/var/lib/hedgedoc" = { + device = "/volumes/${cfg.location}/hedgedoc"; + options = [ "bind" ]; + }; + }; + + services.postgresql = { + ensureDatabases = [ "hedgedoc" ]; + ensureUsers = [ + { + name = "hedgedoc"; + ensureDBOwnership = true; + } + ]; + }; + + services.hedgedoc = { + enable = true; + package = pkgs.hedgedoc.overrideAttrs (old: { + doCheck = true; + patches = (old.patches or [ ]) ++ [ + ./hedgedocClientSecretAsFile.patch + ]; + }); + + + settings = { + port = 3001; + protocolUseSSL = true; + useSSL = false; + domain = "${cfg.subdomain}.${sp.domain}"; + host = "127.0.0.1"; + + # HedgeDoc saves the old option name for compatibility, even though it fetches avatars from Libravatar + allowGravatar = cfg.allowLibravatar; + + db = { + username = "hedgedoc"; + database = "hedgedoc"; + host = "/run/postgresql"; + dialect = "postgresql"; + }; + uploadsPath = "/var/lib/hedgedoc/uploads"; + + allowEmailRegister = false; + email = false; + allowAnonymous = cfg.allowAnonymous; + + oauth2 = { + baseURL = oauth2-provider-origin; + clientID = "hedgedoc"; + clientSecret = oauthClientSecretFP; + authorizationURL = "${oauth2-provider-origin}/ui/oauth2"; + tokenURL = "${oauth2-provider-origin}/oauth2/token"; + userProfileURL = "${oauth2-provider-origin}/oauth2/openid/${oauthClientID}/userinfo"; + scope = "openid email profile"; + userProfileUsernameAttr = "sub"; + userProfileDisplayNameAttr = "name"; + userProfileEmailAttr = "email"; + }; + + loglevel = "error"; + }; + }; + + services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { + useACMEHost = sp.domain; + forceSSL = true; + locations = { + "/" = { + proxyPass = "http://127.0.0.1:3001"; + proxyWebsockets = true; + }; + }; + }; + + systemd = { + services.hedgedoc = { + unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/hedgedoc"; + serviceConfig.Slice = "hedgedoc.slice"; + }; + slices.hedgedoc = { + description = "HedgeDoc service slice"; + }; + }; + + selfprivacy.auth.clients.${oauthClientID} = { + inherit usersGroup; + subdomain = cfg.subdomain; + originLanding = "https://${cfg.subdomain}.${sp.domain}/"; + originUrl = "https://${cfg.subdomain}.${sp.domain}/auth/oauth2/callback"; + clientSystemdUnits = [ "hedgedoc.service" ]; + enablePkce = false; + linuxUserOfClient = "hedgedoc"; + linuxGroupOfClient = "hedgedoc"; + }; + }; +}