{ self, inputs, ... }: { flake.modules.nixos.forgejo = {config, pkgs, lib, ... }: let cfg = config.forgejo; needsPrivilegedPort = cfg.port < 1024; wrappedForgejo = (inputs.self.wrappers.forgejo.forgejo.apply { inherit pkgs; serviceConfig = config.services.forgejo; }).wrapper; cleanForgejo = (inputs.self.wrappers.forgejo.clean.apply { inherit pkgs; serviceConfig = config.services.forgejo; }).wrapper; cleanPostgres = (inputs.self.wrappers.forgejo.clean-postgres.apply { inherit pkgs; serviceConfig = config.services.postgresql; }).wrapper; in { options.forgejo = { enable = lib.mkEnableOption "Enable Forgejo backed with PostgreSQL"; root_url = lib.mkOption { type = lib.types.str; }; pocketId = { enable = lib.mkEnableOption "Configure Pocket ID as a Forgejo OIDC provider"; discoveryUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "https://id.example.com"; description = "OpenID Connect discovery URL for Pocket ID."; }; name = lib.mkOption { type = lib.types.str; default = "pocket-id"; description = "Forgejo external auth source name for Pocket ID."; }; }; port = lib.mkOption { type = lib.types.port; default = 3000; description = "TCP port for the Forgejo web interface."; }; openFirewall = lib.mkOption { type = lib.types.bool; default = true; description = "Open the Forgejo web interface port in the firewall."; }; https = lib.mkEnableOption "Open the Forgejo web interface port in the firewall."; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = !cfg.pocketId.enable || cfg.pocketId.discoveryUrl != null; message = "forgejo.pocketId.discoveryUrl must be set when forgejo.pocketId.enable is true."; } ]; networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ cfg.port ]; mtls = lib.mkIf cfg.https { certReaders = lib.mkAfter [ config.services.forgejo.user ]; renew.reloadUnits = lib.mkAfter [ "forgejo.service" ]; }; systemd.services.forgejo.serviceConfig = lib.mkIf needsPrivilegedPort { AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; PrivateUsers = lib.mkForce false; }; sops.secrets = { "forgejo/secret_key".owner = config.services.forgejo.user; "forgejo/internal_token".owner = config.services.forgejo.user; "forgejo/jwt_secret".owner = config.services.forgejo.user; "forgejo/lfs_jwt_secret".owner = config.services.forgejo.user; } // lib.optionalAttrs cfg.pocketId.enable { "forgejo/pocket_id/client_id".owner = config.services.forgejo.user; "forgejo/pocket_id/client_secret".owner = config.services.forgejo.user; }; services = { forgejo = { enable = true; lfs.enable = true; database.type = "postgres"; settings = { DEFAULT.RUN_MODE = "dev"; server = lib.mkMerge [ { HTTP_PORT = cfg.port; DISABLE_SSH = true; ROOT_URL = cfg.root_url; } (lib.mkIf cfg.https { PROTOCOL = "https"; COOKIE_SECURE = true; KEY_FILE = config.mtls.keyFile; CERT_FILE = config.mtls.certFile; }) ]; oauth2_client = { ENABLE_AUTO_REGISTRATION = true; USERNAME = "nickname"; UPDATE_AVATAR = true; ACCOUNT_LINKING = "login"; REGISTER_EMAIL_CONFIRM = false; }; repository.ENABLE_PUSH_CREATE_USER = true; ui.SHOW_USER_EMAIL = false; markup.ENABLED = true; }; secrets = { security = { SECRET_KEY = lib.mkForce config.sops.secrets."forgejo/secret_key".path; INTERNAL_TOKEN = lib.mkForce config.sops.secrets."forgejo/internal_token".path; }; oauth2.JWT_SECRET = lib.mkForce config.sops.secrets."forgejo/jwt_secret".path; server.LFS_JWT_SECRET = lib.mkForce config.sops.secrets."forgejo/lfs_jwt_secret".path; }; dump = { enable = true; type = "tar"; interval = "*-*-* 00/12:00:00"; }; }; postgresql = { enable = true; enableTCPIP = false; }; }; # https://forgejo.org/docs/latest/admin/command-line/#dump systemd.services.forgejo-dump.serviceConfig.ExecStart = lib.mkForce '' ${lib.getExe config.services.forgejo.package} dump --verbose \ --type ${config.services.forgejo.dump.type} \ --database postgres \ --work-path ${config.services.forgejo.dump.backupDir} ''; systemd.services.forgejo-pocket-id-oidc = lib.mkIf cfg.pocketId.enable { description = "Ensure Pocket ID OIDC auth source exists in Forgejo"; after = [ "forgejo.service" "network-online.target" ]; wants = [ "network-online.target" ]; requires = [ "forgejo.service" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; User = config.services.forgejo.user; Group = config.services.forgejo.group; WorkingDirectory = config.services.forgejo.stateDir; Restart = "on-failure"; RestartSec = "30s"; StartLimitIntervalSec = 0; }; script = '' set -euo pipefail client_id="$(<${config.sops.secrets."forgejo/pocket_id/client_id".path})" client_secret="$(<${config.sops.secrets."forgejo/pocket_id/client_secret".path})" provider_name="${cfg.pocketId.name}" forgejo_bin="${lib.getExe wrappedForgejo}" auth_id="$( "$forgejo_bin" admin auth list \ | ${lib.getExe' pkgs.gawk "awk"} '$2 == "'"$provider_name"'" { print $1; exit }' )" if [ -n "$auth_id" ]; then "$forgejo_bin" admin auth update-oauth \ --id "$auth_id" \ --name "$provider_name" \ --provider openidConnect \ --key "$client_id" \ --secret "$client_secret" \ --auto-discover-url "${cfg.pocketId.discoveryUrl}" \ --scopes openid \ --scopes profile \ --scopes email else "$forgejo_bin" admin auth add-oauth \ --name "$provider_name" \ --provider openidConnect \ --key "$client_id" \ --secret "$client_secret" \ --auto-discover-url "${cfg.pocketId.discoveryUrl}" \ --scopes openid \ --scopes profile \ --scopes email fi ''; }; environment.systemPackages = [ wrappedForgejo cleanForgejo cleanPostgres ]; }; }; flake.wrappers.forgejo = { forgejo = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: { options = { serviceConfig = lib.mkOption { type = lib.types.attrs; }; }; config = let cfg = config.serviceConfig; forgejo_config="${cfg.customDir}/conf/app.ini"; forgejo_work_path="${cfg.stateDir}"; in { binName = "my-forgejo"; package = cfg.package; args = [ "--config" forgejo_config "--work-path" forgejo_work_path ]; }; }); clean = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: { options = { serviceConfig = lib.mkOption { type = lib.types.attrs; }; }; config = let cfg = config.serviceConfig; pkgs = config.pkgs; in { binName = "forgejo-clean"; package = pkgs.coreutils; exePath = lib.getExe' pkgs.coreutils "rm"; extraPackages = [ pkgs.systemd ]; preHook = '' sudo systemctl stop forgejo.service echo "Stopped Forgejo" ''; args = [ "-rf" cfg.stateDir ]; postHook = ''echo "Removed ${cfg.stateDir}"''; }; }); clean-postgres = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: { options = { serviceConfig = lib.mkOption { type = lib.types.attrs; }; }; config = let cfg = config.serviceConfig; pkgs = config.pkgs; in { binName = "postgres-clean"; package = pkgs.coreutils; exePath = lib.getExe' pkgs.coreutils "rm"; extraPackages = [ pkgs.systemd ]; preHook = '' sudo systemctl stop postgresql.service echo "Stopped Postgres" ''; args = [ "-rf" cfg.dataDir ]; postHook = ''echo "Removed ${cfg.dataDir}"''; }; }); }; }