self: { config, lib, pkgs, ... }: let cfg = config.services.puppy-tracker; staticPkg = self.packages.${pkgs.system}.static; serverPkg = self.packages.${pkgs.system}.server; in { options.services.puppy-tracker = { enable = lib.mkEnableOption "Puppy Tracker (offline-first puppy tracking app with sync server)"; address = lib.mkOption { type = lib.types.str; default = "0.0.0.0"; description = "Address the server listens on."; }; port = lib.mkOption { type = lib.types.port; default = 8080; description = "TCP port the server listens on."; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Whether to open the configured port in the firewall."; }; inviteCodeFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; example = "/run/secrets/puppy-invite-code"; description = '' Path to an EnvironmentFile containing the shared registration secret as `PUPPY_INVITE_CODE=...`. Kept out of the Nix store so the code stays secret. When null, registration is disabled (existing accounts can still log in). ''; }; vapidKeyFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; example = "/run/secrets/puppy-vapid-key"; description = '' Path to an EnvironmentFile containing the Web Push signing key as `PUPPY_VAPID_KEY=...` (a base64url P-256 private scalar). When null the server generates one on first start and keeps it in its state directory, which is fine for a single host. Note that browsers pin this key when they subscribe: replacing it silently breaks every existing reminder subscription until each device re-enables notifications. ''; }; secureCookies = lib.mkOption { type = lib.types.bool; default = false; description = '' Mark session cookies Secure. Enable once the service is reached over HTTPS (e.g. behind a TLS-terminating reverse proxy); leave off for plain HTTP on a LAN, or browsers will drop the cookie and logins won't stick. ''; }; package = lib.mkOption { type = lib.types.package; default = serverPkg; defaultText = lib.literalExpression "puppy-tracker.packages.\${system}.server"; description = "The puppy-tracker server package."; }; staticPackage = lib.mkOption { type = lib.types.package; default = staticPkg; defaultText = lib.literalExpression "puppy-tracker.packages.\${system}.static"; description = "The puppy-tracker static-site package (HTML/CSS/JS)."; }; }; config = lib.mkIf cfg.enable { systemd.services.puppy-tracker = { description = "Puppy Tracker server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; serviceConfig = { ExecStart = lib.concatStringsSep " " ([ "${cfg.package}/bin/puppy-tracker-server" "-addr ${cfg.address}:${toString cfg.port}" "-static ${cfg.staticPackage}/share/puppy-tracker" "-data /var/lib/puppy-tracker/puppy.db" ] ++ lib.optional cfg.secureCookies "-secure-cookies"); # Secrets (registration code, Web Push key) are read from env files kept # out of the store, exposed to the server as PUPPY_INVITE_CODE and # PUPPY_VAPID_KEY. EnvironmentFile = lib.filter (f: f != null) [ cfg.inviteCodeFile cfg.vapidKeyFile ]; DynamicUser = true; StateDirectory = "puppy-tracker"; StateDirectoryMode = "0750"; Restart = "on-failure"; RestartSec = "2s"; # Hardening ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; PrivateDevices = true; NoNewPrivileges = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectKernelLogs = true; ProtectControlGroups = true; RestrictSUIDSGID = true; RestrictRealtime = true; LockPersonality = true; MemoryDenyWriteExecute = true; SystemCallArchitectures = "native"; SystemCallFilter = [ "@system-service" "~@privileged @resources" ]; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; }; }; networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ]; }; }