From b9ed67949a718fb970cd84d3265c575f38d54719 Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Thu, 1 Jan 2026 14:41:39 -0500 Subject: [PATCH] feat: add control endpoint --- .envrc | 1 + machines/terebithia/default.nix | 56 ++++++++++++++- modules/lib/mkService.nix | 12 +++- modules/nixos/services/control.nix | 109 +++++++++++++++++++++++++++++ secrets/control.age | Bin 0 -> 853 bytes secrets/secrets.nix | 3 + 6 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 modules/nixos/services/control.nix create mode 100644 secrets/control.age diff --git a/.envrc b/.envrc index e69de29..8b13789 100644 --- a/.envrc +++ b/.envrc @@ -0,0 +1 @@ + diff --git a/machines/terebithia/default.nix b/machines/terebithia/default.nix index 0fa553d..c6eec87 100644 --- a/machines/terebithia/default.nix +++ b/machines/terebithia/default.nix @@ -137,6 +137,10 @@ file = ../../secrets/l4.age; owner = "l4"; }; + control = { + file = ../../secrets/control.age; + owner = "control"; + }; "restic/env".file = ../../secrets/restic/env.age; "restic/repo".file = ../../secrets/restic/repo.age; "restic/password".file = ../../secrets/restic/password.age; @@ -310,10 +314,36 @@ header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" } - reverse_proxy localhost:8084 { - header_up X-Forwarded-Proto {scheme} - header_up X-Forwarded-For {remote} + + # Kill-check for protected endpoints via control panel + @protected path /sse /sse/* /tiles/*/markers/pl3xmap_players.json + handle @protected { + reverse_proxy localhost:3010 { + rewrite /kill-check + header_up X-Orig-Host {host} + header_up X-Orig-Path {path} + + @allowed status 200 + handle_response @allowed { + reverse_proxy localhost:8084 + } + handle_response { + respond "Temporarily disabled" 503 + } + } + } + + # Proxy settings.json through control to conditionally redact fields + handle /tiles/settings.json { + reverse_proxy localhost:3010 { + rewrite /proxy/settings.json + header_up X-Orig-Host {host} + header_up X-Orig-Path {path} + header_up X-Backend-Url http://localhost:8084{path} + } } + + reverse_proxy localhost:8084 ''; }; extraConfig = '' @@ -422,6 +452,26 @@ secretsFile = config.age.secrets.l4.path; }; + atelier.services.control = { + enable = true; + domain = "control.dunkirk.sh"; + deploy.repository = "https://tangled.org/dunkirk.sh/control"; + deploy.autoUpdate = true; + secretsFile = config.age.secrets.control.path; + + flags."map.dunkirk.sh" = { + name = "Map"; + flags = { + "block-tracking" = { + name = "Block Player Tracking"; + description = "Disable real-time player location updates"; + paths = [ "/sse" "/sse/*" "/tiles/*/markers/pl3xmap_players.json" ]; + redact."/tiles/settings.json" = [ "players" ]; + }; + }; + }; + }; + services.n8n = { enable = true; environment = { diff --git a/modules/lib/mkService.nix b/modules/lib/mkService.nix index c19a1c0..90ca2f1 100644 --- a/modules/lib/mkService.nix +++ b/modules/lib/mkService.nix @@ -196,7 +196,7 @@ in { inherit description; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; - path = [ pkgs.git ]; + path = [ pkgs.git pkgs.openssh ]; preStart = lib.optionalString (cfg.deploy.enable && cfg.deploy.repository != null) '' # Clone repository if not present @@ -245,7 +245,8 @@ in { }; serviceConfig.ExecStartPre = [ - "+${pkgs.writeShellScript "${name}-setup" '' + # Run before preStart, creates directories so WorkingDirectory exists + "!${pkgs.writeShellScript "${name}-setup" '' mkdir -p ${cfg.dataDir}/app/data mkdir -p ${cfg.dataDir}/data chown -R ${name}:services ${cfg.dataDir} @@ -254,6 +255,13 @@ in { ]; }; + # Ensure working directory exists before service starts + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0755 ${name} services -" + "d ${cfg.dataDir}/app 0755 ${name} services -" + "d ${cfg.dataDir}/data 0755 ${name} services -" + ]; + # Caddy reverse proxy services.caddy.virtualHosts.${cfg.domain} = lib.mkIf cfg.caddy.enable { extraConfig = '' diff --git a/modules/nixos/services/control.nix b/modules/nixos/services/control.nix new file mode 100644 index 0000000..944ad9d --- /dev/null +++ b/modules/nixos/services/control.nix @@ -0,0 +1,109 @@ +# Control Panel - Admin dashboard for Caddy toggles +# +# Protected by Indiko OAuth, allows toggling feature flags +# that control Caddy behavior. Uses SQLite for flag storage +# and exposes a /kill-check endpoint for Caddy to query. + +{ config, lib, pkgs, ... }: + +let + mkService = import ../../lib/mkService.nix; + cfg = config.atelier.services.control; + + # Generate flags.json from Nix config + flagsJson = pkgs.writeText "flags.json" (builtins.toJSON { + services = lib.mapAttrs (serviceId: serviceCfg: { + name = serviceCfg.name; + flags = lib.mapAttrs (flagId: flagCfg: { + name = flagCfg.name; + description = flagCfg.description; + paths = flagCfg.paths; + redact = flagCfg.redact; + }) serviceCfg.flags; + }) cfg.flags; + }); + + baseModule = mkService { + name = "control"; + description = "Control Panel - Admin dashboard for Caddy toggles"; + defaultPort = 3010; + runtime = "bun"; + entryPoint = "src/index.ts"; + + extraOptions = { + flags = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + description = "Display name for this service"; + }; + flags = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + description = "Display name for this flag"; + }; + description = lib.mkOption { + type = lib.types.str; + description = "Description of what this flag does"; + }; + paths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "URL paths this flag fully blocks"; + }; + redact = lib.mkOption { + type = lib.types.attrsOf (lib.types.listOf lib.types.str); + default = {}; + description = "Map of path -> fields to redact from JSON response"; + }; + }; + }); + default = {}; + description = "Flags for this service"; + }; + }; + }); + default = {}; + description = "Services and their flags"; + example = lib.literalExpression '' + { + "map.dunkirk.sh" = { + name = "Map"; + flags = { + "block-tracking" = { + name = "Block Player Tracking"; + description = "Disable real-time player location updates"; + paths = [ "/sse" "/tiles/world/markers/pl3xmap_players.json" ]; + redact = { + "/tiles/settings.json" = [ "players" ]; + }; + }; + }; + }; + } + ''; + }; + }; + + extraConfig = innerCfg: { + atelier.services.control.environment = { + INDIKO_URL = "https://indiko.dunkirk.sh"; + CLIENT_ID = "https://${innerCfg.domain}/"; + REDIRECT_URI = "https://${innerCfg.domain}/auth/callback"; + DATABASE_PATH = "${innerCfg.dataDir}/data/control.db"; + FLAGS_CONFIG = toString flagsJson; + }; + + # Data declarations for backup (SQLite database) + atelier.services.control.data = { + sqlite = "${innerCfg.dataDir}/data/control.db"; + }; + }; + }; +in +{ + imports = [ baseModule ]; +} diff --git a/secrets/control.age b/secrets/control.age new file mode 100644 index 0000000000000000000000000000000000000000..9de2efdcf37a210c1ba15d4c2b54e58a6d4d48b9 GIT binary patch literal 853 zcmYdHPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!*`Do#{zDNJ@Z2;|Bx4RcFRO!P{M z3@W$|~{ktuVKUa4k1U)J}7CbIHr{Pw}ZNa?DB(EXp%7vNT8w4#;zKPA$(3 zH+A7Ej4Vsn_A*b=k1Pof$f)$FD)cSPPA)LguMEzwGObAU%F3$p@=MNjay80#DR9j* zFAnl_4KnpePmVC~G4v1R()TNA&%umjC^bM#?^YZiX=c+8o%L$6~HVSqQ_ef86wam}S2naPb zGchnqi_FN-HgYn`a1AfYc1rVf&WcP+OLX)pGA%VWt@KLwjVShUFXb}xO|>u%^3E^_ z^DT^WH8M1FGBC(+wKOmZ3Uvfb4$+C4lWN1^G`I+t1vSQNH%auGc|}P^tXsK zE6OSjPE52cD>l#YDk;r(3J59i4|fT6)UGP8N(oCXwFnN%G0yig3FqR{)zwufch55S z4RJP($O_I2&T}p`(2jH}&Q3Qf&vZ=9_Tzf9*l*!BBHWPV%NnxB95&N{T{E-A2B@3rKi zbNOTEn{jUn>^)^CbMgdtUet zBbng)-MTLJjo;7ah)vnked5N}i1Qx