From 74af9fd5acd71a9c403a2a7293fb7f68aa6be6e8 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Sun, 26 Apr 2026 20:58:10 +0200 Subject: [PATCH] wip(sgx): firefly-sparda-fetch service + timer (DISABLED) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end FinTS pipeline against Sparda Südwest is wired up but disabled — aqbanking 6.8.2's `-P pinfile` flag does not consume the file content correctly on this build (verified: pinfile bytes match the manually-typed PIN exactly, yet the bank receives a wrong PIN). Three rejected attempts locked the access at Sparda; do not re-arm the timer until the auth path is replaced (likely python-fints). What works: - aqbanking config and FinTS dialog (manual PIN entry) - getaccsepa workaround for HKCAZ "Mussfeld 9160" rejection - custom CSV profile (decimal amounts + IBAN columns) wired in - Firefly importer auto-upload settings + sops secret slot - inbox + profile-symlink tmpfiles What's broken: - Headless PIN delivery via aqbanking-cli -P - Timer left wantedBy=[] so it cannot fire post-deploy Co-Authored-By: Claude Opus 4.7 (1M context) --- systems/x86_64-linux/sgx/firefly.nix | 116 +++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/systems/x86_64-linux/sgx/firefly.nix b/systems/x86_64-linux/sgx/firefly.nix index 8a077b7..44c412f 100644 --- a/systems/x86_64-linux/sgx/firefly.nix +++ b/systems/x86_64-linux/sgx/firefly.nix @@ -2,8 +2,24 @@ let domain = "firefly.hoyer.world"; importDomain = "firefly-import.hoyer.world"; - aqHome = "/var/lib/firefly-aqbanking"; - inbox = "/var/lib/firefly-iii-data-importer/inbox"; + importerHome = "/var/lib/firefly-iii-data-importer"; + inbox = "${importerHome}/inbox"; + configFile = "${importerHome}/sparda-config.json"; + + bankCode = "55090500"; + userId = "5987838198"; + giroAccountId = "3"; + + # aqbanking 6.8.2 ships only an "import" profile and a "full" export + # profile that renders amounts as fractions ("-499/100"). Firefly's CSV + # importer needs decimal amounts and benefits from localIban/remoteIban + # columns, so derive a profile that combines "full"'s columns with + # decimal value formatting. + fireflyCsvProfile = pkgs.runCommand "aqbanking-csv-firefly-profile" { } '' + sed 's/name="full"/name="firefly"/; s/valueFormat="rational"/valueFormat="float"/' \ + ${pkgs.aqbanking}/share/aqbanking/imexporters/csv/profiles/full.conf > $out + ''; + vhostBase = { enableACME = false; useACMEHost = "internal.hoyer.world"; @@ -20,14 +36,97 @@ in sopsFile = ../../../.secrets/sgx/firefly.yaml; owner = "firefly-iii-data-importer"; }; + "firefly/auto_import_secret" = { + sopsFile = ../../../.secrets/sgx/firefly.yaml; + owner = "firefly-iii-data-importer"; + }; }; environment.systemPackages = [ pkgs.aqbanking ]; - systemd.tmpfiles.rules = [ - "d ${aqHome} 0700 firefly-iii-data-importer firefly-iii-data-importer -" - "d ${inbox} 0700 firefly-iii-data-importer firefly-iii-data-importer -" - ]; + systemd = { + tmpfiles.rules = [ + "d ${inbox} 0700 firefly-iii-data-importer nginx -" + "d ${importerHome}/.aqbanking/imexporters/csv/profiles 0700 firefly-iii-data-importer nginx -" + "L+ ${importerHome}/.aqbanking/imexporters/csv/profiles/firefly.conf - - - - ${fireflyCsvProfile}" + ]; + + services.firefly-sparda-fetch = { + description = "Fetch Sparda transactions via FinTS and trigger Firefly auto-import"; + after = [ + "network-online.target" + "phpfpm-firefly-iii-data-importer.service" + ]; + wants = [ "network-online.target" ]; + path = with pkgs; [ + aqbanking + curl + coreutils + ]; + + serviceConfig = { + Type = "oneshot"; + User = "firefly-iii-data-importer"; + Group = "nginx"; + RuntimeDirectory = "firefly-sparda-fetch"; + LoadCredential = [ + "pin:${config.sops.secrets."firefly/sparda_pin".path}" + "secret:${config.sops.secrets."firefly/auto_import_secret".path}" + ]; + ProtectSystem = "strict"; + ReadWritePaths = [ importerHome ]; + ProtectHome = true; + PrivateTmp = true; + NoNewPrivileges = true; + TimeoutStartSec = "3min"; + }; + + script = '' + set -euo pipefail + + pinfile=$RUNTIME_DIRECTORY/pinfile + umask 077 + printf 'PIN_%s_%s = "%s"\n' "${bankCode}" "${userId}" \ + "$(<"$CREDENTIALS_DIRECTORY/pin")" >"$pinfile" + + ts=$(date +%Y%m%d-%H%M%S) + ctx=$RUNTIME_DIRECTORY/ctx-$ts.aqb + out=${inbox}/sparda-$ts.csv + + # Refresh SEPA account list — Atruvia/Sparda rejects HKCAZ + # ("Mussfeld 9160") if this metadata isn't fresh in the dialog. + aqhbci-tool4 -n -A -P "$pinfile" getaccsepa -u ${giroAccountId} + + fromdate=$(date --date='35 days ago' +%Y%m%d) + aqbanking-cli -n -A -P "$pinfile" request \ + --transactions --fromdate="$fromdate" \ + --aid=${giroAccountId} -c "$ctx" + + aqbanking-cli export \ + --exporter=csv --profile=firefly \ + -c "$ctx" -o "$out" + + secret=$(<"$CREDENTIALS_DIRECTORY/secret") + curl -fsS -X POST \ + "https://${importDomain}/autoupload?secret=$secret" \ + -F "json=@${configFile}" \ + -F "importable=@$out" + ''; + }; + + # Timer disabled while we work around aqbanking 6.8.2's broken + # `-P pinfile` handling. The fetch service authenticates with a wrong + # PIN against the bank — three runs locked the access at Sparda. Do + # not re-enable until the auth path is replaced (likely python-fints). + timers.firefly-sparda-fetch = { + wantedBy = [ ]; + timerConfig = { + OnCalendar = "daily"; + Persistent = true; + RandomizedDelaySec = "1h"; + }; + }; + }; services = { firefly-iii = { @@ -54,6 +153,10 @@ in FIREFLY_III_URL = "https://${domain}"; VANITY_URL = "https://${importDomain}"; TZ = "Europe/Berlin"; + CAN_POST_FILES = "true"; + CAN_POST_AUTOIMPORT = "true"; + IMPORT_DIR_ALLOWLIST = inbox; + AUTO_IMPORT_SECRET_FILE = config.sops.secrets."firefly/auto_import_secret".path; }; }; @@ -62,4 +165,5 @@ in ${importDomain} = vhostBase; }; }; + }