Merge pull request 'Fix: FinTS-Konto-Auswahl speichern + Bearbeiten-Dialog mit Checkboxen' (#83) from fds2610-bot/Steuerkram:main into main
Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/83
This commit is contained in:
commit
fdc4da79f8
5 changed files with 176 additions and 11 deletions
|
|
@ -2,6 +2,15 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-05-22 – Fix: FinTS-Konto-Auswahl wird jetzt gespeichert + Bearbeiten-Dialog mit Checkboxen
|
||||
|
||||
- **Bug:** Beim ersten Abruf erschien das Konto-Auswahl-Modal, das Speichern blieb aber hängen. Ursache war PHP-Session-Locking: der SSE-Stream `/bank/direkt/{id}/run` hielt die Session-Datei während des `awaitAccountSelection()`-Polling-Loops gesperrt, der parallele POST `/bank/direkt/{id}/accounts` blockierte beim `session_start()` → Deadlock bis Timeout
|
||||
- **Fix:** Im SSE-Endpoint `session_write_close()` direkt nach dem Auth-Check; entstört zugleich die analog gelagerte TAN-Eingabe
|
||||
- **UI:** Im Bearbeiten-Dialog einer FinTS-Verbindung erscheinen die gespeicherten Konten jetzt als Checkbox-Liste (statt nur als Text). Abhaken entfernt den Identifier beim Speichern aus `account_filter`; alle abgehakt verhält sich wie der Reset-Toggle
|
||||
- Filter-Berechnung als `BankSource::computeFilterFromPost()` extrahiert, neuer Test `38_BankSourceFilterFromPostTest` deckt Reset, Teilauswahl, leere Auswahl und Schutz gegen Unterschieben fremder Identifier ab
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-22 – Feature: APP_KEY Status & Generator in den Einstellungen
|
||||
|
||||
- Neuer Settings-Bereich unter dem Betriebsmodus zeigt Status des Verschlüsselungsschlüssels (`APP_KEY`): **Gesetzt** (grün, 64 Hex-Zeichen valide), **Ungültig** (falsches Format) oder **Fehlt** (leer)
|
||||
|
|
|
|||
|
|
@ -462,6 +462,12 @@ class BankController extends BaseController
|
|||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Session-Lock freigeben, damit parallele POSTs (TAN-/Konto-Auswahl)
|
||||
// während des Pollings nicht blockieren.
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
$send = function (string $event, string $data): void {
|
||||
foreach (explode("\n", $data) as $line) {
|
||||
echo 'event: ' . $event . "\n";
|
||||
|
|
@ -537,9 +543,12 @@ class BankController extends BaseController
|
|||
'benutzerkennung' => trim($_POST['benutzerkennung'] ?? '') ?: null,
|
||||
'pin' => $newPin !== '' ? $newPin : $source['pin'],
|
||||
];
|
||||
// Reset-Button: Konto-Filter explizit löschen → beim nächsten Abruf neu auswählen
|
||||
if (isset($_POST['reset_account_filter'])) {
|
||||
$data['account_filter'] = null;
|
||||
$filterUpdate = BankSource::computeFilterFromPost(
|
||||
$source['account_filter'] ?? null,
|
||||
$_POST
|
||||
);
|
||||
if ($filterUpdate['update']) {
|
||||
$data['account_filter'] = $filterUpdate['value'];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,6 +90,49 @@ class BankSource
|
|||
Database::execute('UPDATE bank_sources SET last_fetched_at = NOW() WHERE id = ?', [$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet einen neuen account_filter aus dem bisherigen Filter und einem
|
||||
* POST-Payload des Bearbeiten-Dialogs.
|
||||
*
|
||||
* Rückgabe:
|
||||
* ['update' => true, 'value' => null] – Filter löschen (Reset)
|
||||
* ['update' => true, 'value' => '[...]'] – neuen JSON-Filter setzen
|
||||
* ['update' => false, 'value' => null] – Filter unverändert lassen
|
||||
*
|
||||
* Nur Identifier, die bereits im bisherigen Filter standen, werden übernommen
|
||||
* (kein Unterschieben fremder Konten via POST).
|
||||
*/
|
||||
public static function computeFilterFromPost(?string $currentJson, array $post): array
|
||||
{
|
||||
if (isset($post['reset_account_filter'])) {
|
||||
return ['update' => true, 'value' => null];
|
||||
}
|
||||
if (!array_key_exists('active_accounts', $post)) {
|
||||
return ['update' => false, 'value' => null];
|
||||
}
|
||||
|
||||
$known = [];
|
||||
if ($currentJson !== null && $currentJson !== '') {
|
||||
$decoded = json_decode($currentJson, true);
|
||||
if (is_array($decoded)) {
|
||||
$known = array_values(array_map('strval', $decoded));
|
||||
}
|
||||
}
|
||||
|
||||
$postedRaw = is_array($post['active_accounts']) ? $post['active_accounts'] : [];
|
||||
$posted = array_map('strval', $postedRaw);
|
||||
$kept = array_values(array_intersect($known, $posted));
|
||||
|
||||
if ($kept === [] && $known !== []) {
|
||||
// Alle abgehakt → wie Reset behandeln (beim nächsten Abruf neu fragen)
|
||||
return ['update' => true, 'value' => null];
|
||||
}
|
||||
if ($kept !== []) {
|
||||
return ['update' => true, 'value' => json_encode($kept, JSON_UNESCAPED_UNICODE)];
|
||||
}
|
||||
return ['update' => false, 'value' => null];
|
||||
}
|
||||
|
||||
/** Aktualisiert sort_order anhand der übergebenen ID-Reihenfolge (Position 0..N). */
|
||||
public static function updateOrder(array $orderedIds): void
|
||||
{
|
||||
|
|
|
|||
|
|
@ -274,7 +274,10 @@ $connectionsJson = json_encode(array_map(fn($c) => [
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label>Aktive Konten</label>
|
||||
<div id="edit-account-list" class="text-sm" style="padding:.4rem 0;color:#475569">–</div>
|
||||
<div id="edit-account-list" style="display:flex;flex-direction:column;gap:.3rem;padding:.2rem 0"></div>
|
||||
<div id="edit-account-empty" class="text-sm text-muted" style="display:none;padding:.4rem 0">
|
||||
(wird beim nächsten Abruf abgefragt)
|
||||
</div>
|
||||
<label style="display:flex;align-items:center;gap:.5rem;font-weight:normal;cursor:pointer;margin-top:.4rem">
|
||||
<input type="checkbox" id="edit-reset-accounts" name="reset_account_filter" value="1" style="width:auto;margin:0">
|
||||
<span>Auswahl zurücksetzen (beim nächsten Abruf neu fragen)</span>
|
||||
|
|
@ -799,18 +802,50 @@ $connectionsJson = json_encode(array_map(fn($c) => [
|
|||
editPin.value = '';
|
||||
editFintsUrl.value = conn.fints_url || '';
|
||||
editResetAcc.checked = false;
|
||||
let accountsText = '–';
|
||||
|
||||
// Konto-Checkboxen aus account_filter aufbauen
|
||||
editAccountList.innerHTML = '';
|
||||
let accounts = [];
|
||||
if (conn.account_filter) {
|
||||
try {
|
||||
const arr = JSON.parse(conn.account_filter);
|
||||
if (Array.isArray(arr) && arr.length > 0) {
|
||||
accountsText = arr.join(', ');
|
||||
}
|
||||
if (Array.isArray(arr)) accounts = arr.map(String);
|
||||
} catch (e) { /* ignore */ }
|
||||
} else {
|
||||
accountsText = '(wird beim nächsten Abruf abgefragt)';
|
||||
}
|
||||
editAccountList.textContent = accountsText;
|
||||
const editAccountEmpty = document.getElementById('edit-account-empty');
|
||||
if (accounts.length === 0) {
|
||||
editAccountList.style.display = 'none';
|
||||
editAccountEmpty.style.display = 'block';
|
||||
} else {
|
||||
editAccountList.style.display = 'flex';
|
||||
editAccountEmpty.style.display = 'none';
|
||||
accounts.forEach(id => {
|
||||
const row = document.createElement('label');
|
||||
row.style.display = 'flex';
|
||||
row.style.alignItems = 'center';
|
||||
row.style.gap = '.5rem';
|
||||
row.style.cursor = 'pointer';
|
||||
row.style.fontWeight = 'normal';
|
||||
row.style.margin = '0';
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.name = 'active_accounts[]';
|
||||
cb.value = id;
|
||||
cb.checked = true;
|
||||
cb.style.width = 'auto';
|
||||
cb.style.margin = '0';
|
||||
const span = document.createElement('span');
|
||||
// IBAN lesbarer machen (4er-Gruppen), AN:-Identifier wie geliefert
|
||||
span.textContent = /^[A-Z]{2}\d{2}/.test(id)
|
||||
? id.replace(/(.{4})/g, '$1 ').trim()
|
||||
: id;
|
||||
span.style.fontFamily = 'monospace';
|
||||
span.style.fontSize = '.85rem';
|
||||
row.appendChild(cb);
|
||||
row.appendChild(span);
|
||||
editAccountList.appendChild(row);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
modalEdit.style.display = 'flex';
|
||||
|
|
|
|||
69
tests/38_BankSourceFilterFromPostTest.php
Normal file
69
tests/38_BankSourceFilterFromPostTest.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Steuerkram\Models\BankSource;
|
||||
|
||||
/**
|
||||
* Test 38: BankSource::computeFilterFromPost – Logik des Bearbeiten-Dialogs
|
||||
* (Checkbox-Auswahl der aktiven Konten + Reset-Toggle).
|
||||
*/
|
||||
class BankSourceFilterFromPostTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 38: BankSource::computeFilterFromPost – Filter aus POST';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$current = json_encode(['DE53120300000000123456', 'AN:42:']);
|
||||
|
||||
// 1. Reset-Flag hat Vorrang und liefert null
|
||||
$r = BankSource::computeFilterFromPost($current, [
|
||||
'reset_account_filter' => '1',
|
||||
'active_accounts' => ['DE53120300000000123456'], // wird ignoriert
|
||||
]);
|
||||
$this->assert($r['update'] === true, 'Reset löst Update aus');
|
||||
$this->assert($r['value'] === null, 'Reset liefert null');
|
||||
|
||||
// 2. Eine Checkbox abgehakt – nur die übrig bleibende wird gespeichert
|
||||
$r = BankSource::computeFilterFromPost($current, [
|
||||
'active_accounts' => ['DE53120300000000123456'],
|
||||
]);
|
||||
$this->assert($r['update'] === true, 'Teilauswahl löst Update aus');
|
||||
$decoded = json_decode((string)$r['value'], true);
|
||||
$this->assertEquals(['DE53120300000000123456'], $decoded, 'Nur angehakter Wert verbleibt');
|
||||
|
||||
// 3. Alle Checkboxen abgehakt → Reset-Verhalten (null)
|
||||
$r = BankSource::computeFilterFromPost($current, [
|
||||
'active_accounts' => [],
|
||||
]);
|
||||
$this->assert($r['update'] === true, 'Alles abgehakt löst Update aus');
|
||||
$this->assert($r['value'] === null, 'Alles abgehakt → null (wie Reset)');
|
||||
|
||||
// 4. Kein active_accounts-Schlüssel im POST → unverändert lassen
|
||||
$r = BankSource::computeFilterFromPost($current, []);
|
||||
$this->assert($r['update'] === false, 'Ohne active_accounts kein Update');
|
||||
|
||||
// 5. POST enthält fremden Identifier, der nicht im aktuellen Filter steht → wird verworfen
|
||||
$r = BankSource::computeFilterFromPost($current, [
|
||||
'active_accounts' => ['DE53120300000000123456', 'FREMD:99:'],
|
||||
]);
|
||||
$decoded = json_decode((string)$r['value'], true);
|
||||
$this->assertEquals(
|
||||
['DE53120300000000123456'],
|
||||
$decoded,
|
||||
'Nur Identifier aus aktuellem Filter werden übernommen'
|
||||
);
|
||||
|
||||
// 6. Aktueller Filter leer (null) + leeres active_accounts → kein Update
|
||||
$r = BankSource::computeFilterFromPost(null, [
|
||||
'active_accounts' => [],
|
||||
]);
|
||||
$this->assert($r['update'] === false, 'Leerer Filter + leere Auswahl → kein Update');
|
||||
|
||||
// 7. Aktueller Filter ist ungültiges JSON → wie leerer Filter behandeln
|
||||
$r = BankSource::computeFilterFromPost('not-json', [
|
||||
'active_accounts' => ['DE53120300000000123456'],
|
||||
]);
|
||||
$this->assert($r['update'] === false, 'Ungültiges Current-JSON führt zu leerer kept-Liste → kein Update');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue