From b605d07b52187c664623639647af38904327643f Mon Sep 17 00:00:00 2001 From: nhnn Date: Mon, 14 Apr 2025 14:32:42 +0300 Subject: [PATCH] feat: Vikunja to-do app (#128) Vikunja is fast self-hostable to-do app. Reviewed-on: https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config/pulls/128 Reviewed-by: Inex Code Co-authored-by: nhnn Co-committed-by: nhnn --- sp-modules/vikunja/config-paths-needed.json | 10 + sp-modules/vikunja/flake.lock | 27 +++ sp-modules/vikunja/flake.nix | 41 ++++ sp-modules/vikunja/icon.svg | 16 ++ .../vikunja/load-client-secret-from-env.patch | 33 ++++ sp-modules/vikunja/module.nix | 186 ++++++++++++++++++ 6 files changed, 313 insertions(+) create mode 100644 sp-modules/vikunja/config-paths-needed.json create mode 100644 sp-modules/vikunja/flake.lock create mode 100644 sp-modules/vikunja/flake.nix create mode 100644 sp-modules/vikunja/icon.svg create mode 100644 sp-modules/vikunja/load-client-secret-from-env.patch create mode 100644 sp-modules/vikunja/module.nix diff --git a/sp-modules/vikunja/config-paths-needed.json b/sp-modules/vikunja/config-paths-needed.json new file mode 100644 index 0000000..0426aa4 --- /dev/null +++ b/sp-modules/vikunja/config-paths-needed.json @@ -0,0 +1,10 @@ +[ + [ "selfprivacy", "domain" ], + [ "selfprivacy", "modules", "auth", "enable" ], + [ "selfprivacy", "modules", "vikunja" ], + [ "selfprivacy", "passthru", "auth", "mkOAuth2ClientSecretFP" ], + [ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ], + [ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ], + [ "selfprivacy", "sso", "enable" ], + [ "selfprivacy", "useBinds" ] +] diff --git a/sp-modules/vikunja/flake.lock b/sp-modules/vikunja/flake.lock new file mode 100644 index 0000000..c48dced --- /dev/null +++ b/sp-modules/vikunja/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs-24-11": { + "locked": { + "lastModified": 1744440957, + "narHash": "sha256-FHlSkNqFmPxPJvy+6fNLaNeWnF1lZSgqVCl/eWaJRc4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "26d499fc9f1d567283d5d56fcf367edd815dba1d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs-24-11": "nixpkgs-24-11" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/sp-modules/vikunja/flake.nix b/sp-modules/vikunja/flake.nix new file mode 100644 index 0000000..7558a21 --- /dev/null +++ b/sp-modules/vikunja/flake.nix @@ -0,0 +1,41 @@ +{ + description = "PoC SP module for Vikunja service"; + + inputs = { + nixpkgs-24-11.url = "github:NixOS/nixpkgs/nixos-24.11"; + }; + + outputs = {nixpkgs-24-11, ...}: { + nixosModules.default = import ./module.nix nixpkgs-24-11.legacyPackages.x86_64-linux; + configPathsNeeded = + builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = {lib, ...}: { + spModuleSchemaVersion = 1; + id = "vikunja"; + name = "Vikunja"; + description = "Vikunja, the fluffy, open-source, self-hostable to-do app."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = true; + isRequired = false; + backupDescription = "Tasks and attachments."; + systemdServices = [ + "vikunja.service" + ]; + folders = [ + "/var/lib/vikunja" + ]; + postgreDatabases = [ + "vikunja" + ]; + license = [ + lib.licenses.agpl3Plus + ]; + homepage = "https://vikunja.io"; + sourcePage = "https://github.com/go-vikunja/vikunja"; + supportLevel = "normal"; + sso = { + userGroup = "sp.vikunja.users"; + }; + }; + }; +} diff --git a/sp-modules/vikunja/icon.svg b/sp-modules/vikunja/icon.svg new file mode 100644 index 0000000..b763ee8 --- /dev/null +++ b/sp-modules/vikunja/icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/sp-modules/vikunja/load-client-secret-from-env.patch b/sp-modules/vikunja/load-client-secret-from-env.patch new file mode 100644 index 0000000..c8f4e17 --- /dev/null +++ b/sp-modules/vikunja/load-client-secret-from-env.patch @@ -0,0 +1,33 @@ +diff --git a/pkg/modules/auth/openid/providers.go b/pkg/modules/auth/openid/providers.go +index 5e14c1b31..769dc96e8 100644 +--- a/pkg/modules/auth/openid/providers.go ++++ b/pkg/modules/auth/openid/providers.go +@@ -17,6 +17,7 @@ + package openid + + import ( ++ "os" + "regexp" + "strconv" + "strings" +@@ -129,12 +130,19 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro + if scope == "" { + scope = "openid profile email" + } ++ ++ clientsecret, err := os.ReadFile(os.Getenv("SP_VIKUNJA_CLIENT_SECRET_PATH")) ++ ++ if err != nil { ++ panic(err) ++ } ++ + provider = &Provider{ + Name: pi["name"].(string), + Key: k, + AuthURL: pi["authurl"].(string), + OriginalAuthURL: pi["authurl"].(string), +- ClientSecret: pi["clientsecret"].(string), ++ ClientSecret: strings.TrimSuffix(string(clientsecret), "\n"), + LogoutURL: logoutURL, + Scope: scope, + } diff --git a/sp-modules/vikunja/module.nix b/sp-modules/vikunja/module.nix new file mode 100644 index 0000000..ee22a52 --- /dev/null +++ b/sp-modules/vikunja/module.nix @@ -0,0 +1,186 @@ +latestPkgs: { + config, + lib, + ... +}: let + sp = config.selfprivacy; + cfg = sp.modules.vikunja; + oauthClientID = "vikunja"; + auth-passthru = config.selfprivacy.passthru.auth; + oauth2-provider-name = auth-passthru.oauth2-provider-name; + oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID; + + # SelfPrivacy uses SP Module ID to identify the group! + usersGroup = "sp.vikunja.users"; + + oauthClientSecretFP = + auth-passthru.mkOAuth2ClientSecretFP oauthClientID; + + vikunjaPackage = latestPkgs.vikunja.overrideAttrs (old: { + doCheck = false; # Tests are slow. + patches = + (old.patches or []) + ++ [ + ./load-client-secret-from-env.patch + ]; + }); +in { + options.selfprivacy.modules.vikunja = { + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable Vikunja"; + }) + // { + meta = { + type = "enable"; + }; + }; + location = + (lib.mkOption { + type = lib.types.str; + description = "Vikunja location"; + }) + // { + meta = { + type = "location"; + }; + }; + subdomain = + (lib.mkOption { + default = "vikunja"; + 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; + }; + }; + }; + + config = + lib.mkIf cfg.enable + { + assertions = [ + { + assertion = sp.sso.enable; + message = "Vikunja cannot be enabled when SSO is disabled."; + } + ]; + + fileSystems = lib.mkIf sp.useBinds { + "/var/lib/vikunja" = { + device = "/volumes/${cfg.location}/vikunja"; + options = ["bind"]; + }; + }; + + users = { + users.vikunja = { + isSystemUser = true; + group = "vikunja"; + }; + groups.vikunja = {}; + }; + + services.postgresql = { + ensureDatabases = ["vikunja"]; + ensureUsers = [ + { + name = "vikunja"; + ensureDBOwnership = true; + } + ]; + }; + + services.vikunja = { + enable = true; + package = vikunjaPackage; + frontendScheme = "https"; + frontendHostname = "${cfg.subdomain}.${sp.domain}"; + port = 4835; + + database = { + type = "postgres"; + host = "/run/postgresql"; + }; + + settings = { + service = { + enableregistration = false; + enabletotp = false; + enableuserdeletion = true; + }; + + auth = { + local.enabled = false; + openid = { + enabled = true; + providers = [ + { + name = oauth2-provider-name; + authurl = lib.strings.removeSuffix "/.well-known/openid-configuration" oauthDiscoveryURL; + clientid = oauthClientID; + clientsecret = ""; # There's patch for our Vikunja to make it load client secret from environment variable. + scope = "openid profile email"; + } + ]; + }; + }; + }; + }; + + services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { + useACMEHost = sp.domain; + forceSSL = true; + extraConfig = '' + add_header Strict-Transport-Security $hsts_header; + #add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always; + add_header 'Referrer-Policy' 'origin-when-cross-origin'; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; + ''; + locations = { + "/" = { + proxyPass = "http://127.0.0.1:4835"; + }; + }; + }; + + systemd = { + services.vikunja = { + unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/vikunja"; + serviceConfig = { + Slice = "vikunja.slice"; + LoadCredential = "oauth2-secret:${oauthClientSecretFP}"; + DynamicUser = lib.mkForce false; + User = "vikunja"; + Group = "vikunja"; + }; + environment.SP_VIKUNJA_CLIENT_SECRET_PATH = "%d/oauth2-secret"; + }; + slices.vikunja = { + description = "Vikunja service slice"; + }; + }; + + selfprivacy.auth.clients.${oauthClientID} = { + inherit usersGroup; + subdomain = cfg.subdomain; + isTokenNeeded = true; + originLanding = "https://${cfg.subdomain}.${sp.domain}/"; + originUrl = "https://${cfg.subdomain}.${sp.domain}/auth/openid/${lib.strings.toLower oauth2-provider-name}"; + clientSystemdUnits = ["vikunja.service"]; + enablePkce = false; + linuxUserOfClient = "vikunja"; + linuxGroupOfClient = "vikunja"; + }; + }; +}