{ self, inputs, ... }: { flake.modules.nixos.forgejo = {config, pkgs, lib, ... }: let cfg = config.forgejo; needsPrivilegedPort = cfg.port < 1024; 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" ]; 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; }; 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 config.services.forgejo.package}" forgejo_config="${config.services.forgejo.customDir}/conf/app.ini" forgejo_work_path="${config.services.forgejo.stateDir}" auth_id="$( "$forgejo_bin" --config "$forgejo_config" --work-path "$forgejo_work_path" admin auth list \ | ${lib.getExe' pkgs.gawk "awk"} '$2 == "'"$provider_name"'" { print $1; exit }' )" if [ -n "$auth_id" ]; then "$forgejo_bin" --config "$forgejo_config" --work-path "$forgejo_work_path" 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" --config "$forgejo_config" --work-path "$forgejo_work_path" 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 = let systemctl = lib.getExe' pkgs.systemd "systemctl"; clean-forgejo = (pkgs.writeShellScriptBin "clean-forgejo" '' set -euo pipefail sudo ${systemctl} stop forgejo.service ${lib.getExe' pkgs.coreutils "echo"} "Stopped Forgejo" sudo ${lib.getExe' pkgs.coreutils "rm"} -rf ${config.services.forgejo.stateDir} ${lib.getExe' pkgs.coreutils "echo"} "Removed ${config.services.forgejo.stateDir}" ''); clean-postgres = (pkgs.writeShellScriptBin "clean-postgres" '' set -euo pipefail sudo ${systemctl} stop postgresql.service ${lib.getExe' pkgs.coreutils "echo"} "Stopped PostgreSQL" sudo ${lib.getExe' pkgs.coreutils "rm"} -rf ${config.services.postgresql.dataDir} ${lib.getExe' pkgs.coreutils "echo"} "Removed ${config.services.postgresql.dataDir}" ''); in [ clean-forgejo clean-postgres (pkgs.writeShellScriptBin "clean-all" '' set -euo pipefail GREEN_CHECK="\e[32m✔\e[0m" YELLOW_BANG="\e[33m!\e[0m" ${lib.getExe' pkgs.coreutils "echo"} -n -e "$YELLOW_BANG Remove everything related to Forgejo and the PostgreSQL database behind it?" read -p " (y/n) " -n 1 -r if [[ $REPLY =~ ^[Yy]$ ]]; then ${lib.getExe clean-forgejo} ${lib.getExe clean-postgres} ${lib.getExe' pkgs.coreutils "echo"} -e "$GREEN_CHECK Removed everything related to forgejo" fi '') ]; }; }; }