self: { config, lib, ... }: let cfg = config.services.jetpham-website; package = self.packages.x86_64-linux.default; qaApi = self.packages.x86_64-linux.qa-api; webhookSecretPath = if cfg.webhookSecretFile != null then cfg.webhookSecretFile else config.age.secrets.webhook-secret.path; in { options.services.jetpham-website = { enable = lib.mkEnableOption "Jet Pham's personal website"; domain = lib.mkOption { type = lib.types.str; default = "jetpham.com"; description = "Domain to serve the website on."; }; tor.enable = lib.mkEnableOption "Tor hidden service for the website"; qaNotifyEmail = lib.mkOption { type = lib.types.str; default = "jet@extremist.software"; description = "Email address to receive Q&A notifications."; }; qaMailDomain = lib.mkOption { type = lib.types.str; default = "extremist.software"; description = "Mail domain for Q&A reply addresses."; }; qaReplyDomain = lib.mkOption { type = lib.types.str; default = "extremist.software"; description = "Domain used in the static Q&A Reply-To address (`qa@...`). Use a dedicated subdomain and route only that mail into the webhook to avoid impacting your main inbox if the Q&A API fails."; }; webhookSecretFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "File containing the WEBHOOK_SECRET for MTA Hook authentication."; }; }; config = lib.mkIf cfg.enable { age.secrets.webhook-secret = lib.mkIf (cfg.webhookSecretFile == null) { file = "${self}/secrets/webhook-secret.age"; mode = "0400"; }; age.secrets.tor-onion-secret-key = lib.mkIf cfg.tor.enable { file = "${self}/secrets/tor-onion-secret-key.age"; owner = "tor"; group = "tor"; mode = "0400"; }; age.secrets.tor-onion-public-key = lib.mkIf cfg.tor.enable { file = "${self}/secrets/tor-onion-public-key.age"; owner = "tor"; group = "tor"; mode = "0444"; }; age.secrets.tor-onion-hostname = lib.mkIf cfg.tor.enable { file = "${self}/secrets/tor-onion-hostname.age"; owner = "tor"; group = "tor"; mode = "0444"; }; services.tor = lib.mkIf cfg.tor.enable { enable = true; relay.onionServices.jetpham-website = { map = [ { port = 80; target = { addr = "127.0.0.1"; port = 8888; }; } ]; }; }; systemd.services.tor-onion-keys = lib.mkIf cfg.tor.enable { description = "Copy Tor onion keys into place"; after = [ "agenix.service" ]; before = [ "tor.service" ]; wantedBy = [ "tor.service" ]; serviceConfig.Type = "oneshot"; script = '' dir="/var/lib/tor/onion/jetpham-website" mkdir -p "$dir" cp ${config.age.secrets.tor-onion-secret-key.path} "$dir/hs_ed25519_secret_key" cp ${config.age.secrets.tor-onion-public-key.path} "$dir/hs_ed25519_public_key" cp ${config.age.secrets.tor-onion-hostname.path} "$dir/hostname" chown -R tor:tor "$dir" chmod 700 "$dir" chmod 400 "$dir/hs_ed25519_secret_key" chmod 444 "$dir/hs_ed25519_public_key" "$dir/hostname" ''; }; # Q&A API systemd service systemd.services.jetpham-qa-api = { description = "Jet Pham Q&A API"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { DynamicUser = true; StateDirectory = "jetpham-qa"; Environment = [ "QA_DB_PATH=/var/lib/jetpham-qa/qa.db" "QA_NOTIFY_EMAIL=${cfg.qaNotifyEmail}" "QA_MAIL_DOMAIN=${cfg.qaMailDomain}" "QA_REPLY_DOMAIN=${cfg.qaReplyDomain}" ]; Restart = "on-failure"; RestartSec = 5; LoadCredential = "webhook-secret:${webhookSecretPath}"; }; script = '' if [ ! -s "$CREDENTIALS_DIRECTORY/webhook-secret" ]; then echo "WEBHOOK_SECRET credential is empty" >&2 exit 1 fi export WEBHOOK_SECRET="$(cat "$CREDENTIALS_DIRECTORY/webhook-secret")" exec ${qaApi}/bin/jetpham-qa-api ''; }; services.caddy.virtualHosts.${cfg.domain} = { extraConfig = '' header Cross-Origin-Opener-Policy "same-origin" header Cross-Origin-Embedder-Policy "require-corp" handle /api/* { reverse_proxy 127.0.0.1:3003 } handle /qa/rss.xml { reverse_proxy 127.0.0.1:3003 } handle { root * ${package} try_files {path} /index.html file_server } ''; }; services.caddy.virtualHosts."http://:8888" = lib.mkIf cfg.tor.enable { extraConfig = '' bind 127.0.0.1 header Cross-Origin-Opener-Policy "same-origin" header Cross-Origin-Embedder-Policy "require-corp" handle /api/* { reverse_proxy 127.0.0.1:3003 } handle /qa/rss.xml { reverse_proxy 127.0.0.1:3003 } handle { root * ${package} try_files {path} /index.html file_server } ''; }; }; }