Online-Banking: Einrichtung in zwei Schritten
Die Bank lässt sich erst nach ihren TAN-Verfahren fragen, wenn die Zugangsdaten gespeichert sind. Das Anlegen führt deshalb über „Weiter“ direkt auf die Verfahrensseite, und die holt die Liste selbst – ein Knopf davor wäre ein Schritt, dessen Ergebnis feststeht. Scheitert die Abfrage, bleibt das Ergebnis in der Sitzung, damit nicht jeder Seitenaufruf wieder in dieselbe Zeitüberschreitung läuft; die ist für die Abfrage kürzer gesetzt als in der Bibliothek. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4bd7ffed1b
commit
9111eeeb91
8 changed files with 207 additions and 94 deletions
15
Changes.md
15
Changes.md
|
|
@ -60,9 +60,18 @@ Damit ist der geplante Funktionsumfang beisammen: Von der Kontenübersicht bis z
|
||||||
- Umsätze, die vor einer TAN-Unterbrechung geschrieben wurden, bleiben erhalten.
|
- Umsätze, die vor einer TAN-Unterbrechung geschrieben wurden, bleiben erhalten.
|
||||||
|
|
||||||
**Wahl des TAN-Verfahrens** (`Services\FinTsVerfahren`, Migration `009_tan_verfahren.sql`)
|
**Wahl des TAN-Verfahrens** (`Services\FinTsVerfahren`, Migration `009_tan_verfahren.sql`)
|
||||||
- Eigene Seite am Zugang (`/banking/{id}/verfahren`): Der Knopf *Verfahren bei der Bank
|
- **Einrichtung in zwei Schritten**: Zugangsdaten erfassen und mit *Weiter* speichern, dann
|
||||||
abfragen* holt die Liste über `getTanModes()` – das kommt ohne Anmeldung aus und verlangt
|
das TAN-Verfahren. Vorher lässt sich die Bank nicht fragen – ohne gespeicherte
|
||||||
selbst keine TAN. TAN-Medien werden nur für die Verfahren geholt, die eines brauchen.
|
Zugangsdaten gibt es keine Verbindung.
|
||||||
|
- Der zweite Schritt holt die Liste beim Aufruf selbst (`getTanModes()`, ohne Anmeldung und
|
||||||
|
ohne TAN); ein Knopf davor wäre ein Schritt, dessen Ergebnis feststeht. Ein erneuter
|
||||||
|
Anlauf steht daneben. TAN-Medien werden nur für die Verfahren geholt, die eines brauchen.
|
||||||
|
- Das Ergebnis liegt in der Sitzung, auch ein gescheitertes – sonst liefe jeder
|
||||||
|
Seitenaufruf wieder in dieselbe Zeitüberschreitung. Die sind für die Abfrage kürzer
|
||||||
|
gesetzt als in der Bibliothek (8/20 statt 15/30 Sekunden), weil sie an einem
|
||||||
|
Seitenaufruf hängt.
|
||||||
|
- Scheitert die Abfrage, erklärt die Seite, wo der Fehler meist steckt, und verlinkt zurück
|
||||||
|
auf die Zugangsdaten; überspringen lässt sich der Schritt ebenfalls.
|
||||||
- Abgefragt statt eingetippt: Die Kennungen sind bankspezifisch, 913 heißt nicht überall
|
- Abgefragt statt eingetippt: Die Kennungen sind bankspezifisch, 913 heißt nicht überall
|
||||||
chipTAN QR. Der Name wird mitgespeichert (`tan_verfahren_name`), damit das Formular ihn
|
chipTAN QR. Der Name wird mitgespeichert (`tan_verfahren_name`), damit das Formular ihn
|
||||||
ohne Bankverbindung zeigen kann.
|
ohne Bankverbindung zeigen kann.
|
||||||
|
|
|
||||||
|
|
@ -58,22 +58,39 @@ ausdrücklich aus. Sie wird bei jedem Request neu aus der Datenbank geholt und e
|
||||||
fragt die Anwendung auf Knopfdruck nach (`checkDecoupledSubmission`) – steht die Freigabe
|
fragt die Anwendung auf Knopfdruck nach (`checkDecoupledSubmission`) – steht die Freigabe
|
||||||
noch aus, bleibt die Maske stehen.
|
noch aus, bleibt die Maske stehen.
|
||||||
|
|
||||||
### Das Verfahren wählen
|
### Einrichtung in zwei Schritten
|
||||||
|
|
||||||
Fast jede Bank bietet mehrere Verfahren an. Welches gilt, steht am Zugang unter
|
Fast jede Bank bietet mehrere Verfahren an, und welche das sind, weiß nur sie. Fragen lässt
|
||||||
*TAN-Verfahren einstellen* (`/banking/{id}/verfahren`).
|
sie sich aber erst, wenn Bankleitzahl, FinTS-Adresse, Benutzerkennung und PIN vorliegen –
|
||||||
|
deshalb zerfällt das Anlegen einer Bankverbindung in zwei Schritte:
|
||||||
|
|
||||||
|
1. **Zugangsdaten** (`/banking/neu`) – *Weiter* speichert sie.
|
||||||
|
2. **TAN-Verfahren** (`/banking/{id}/verfahren`) – die Seite fragt die Bank beim Aufruf
|
||||||
|
selbst und zeigt die Liste zur Wahl.
|
||||||
|
|
||||||
|
Der zweite Schritt holt die Liste **ohne Knopfdruck**: Wer hier landet, will genau das, und
|
||||||
|
ein Knopf, dessen Ergebnis feststeht, wäre ein Schritt zu viel. Ein erneuter Anlauf steht
|
||||||
|
trotzdem bereit, wenn sich etwas geändert hat.
|
||||||
|
|
||||||
|
Später erreicht man dieselbe Seite über *TAN-Verfahren einstellen* am Zugang.
|
||||||
|
|
||||||
**Abgefragt, nicht eingetippt.** Die Kennungen sind bankspezifisch – 913 heißt nicht
|
**Abgefragt, nicht eingetippt.** Die Kennungen sind bankspezifisch – 913 heißt nicht
|
||||||
überall chipTAN QR –, und niemand soll sie in einer PDF der Bank suchen müssen. Der Knopf
|
überall chipTAN QR –, und niemand soll sie in einer PDF der Bank suchen müssen.
|
||||||
*Verfahren bei der Bank abfragen* holt die Liste über `getTanModes()`. Das kommt ohne
|
`getTanModes()` kommt ohne Anmeldung aus (die Bibliothek braucht dafür nur die
|
||||||
Anmeldung aus (die Bibliothek braucht dafür nur die Bankparameterdaten) und verlangt
|
Bankparameterdaten) und verlangt deshalb selbst keine TAN.
|
||||||
deshalb selbst keine TAN.
|
|
||||||
|
|
||||||
`getTanMedia()` wird nur für die Verfahren aufgerufen, die ein Medium brauchen: Jeder
|
`getTanMedia()` wird nur für die Verfahren aufgerufen, die ein Medium brauchen: Jeder
|
||||||
Aufruf ist ein eigener Dialog mit der Bank.
|
Aufruf ist ein eigener Dialog mit der Bank.
|
||||||
|
|
||||||
Die abgefragte Liste liegt in der Sitzung, nicht in der Datenbank – sie ist eine
|
Die Zeitüberschreitungen sind kürzer gesetzt als in der Bibliothek (8 statt 15 Sekunden zum
|
||||||
Momentaufnahme. Beim Übernehmen wird gegen sie geprüft: Eine Kennung, die die Bank nicht
|
Verbinden, 20 statt 30 zur Antwort): Die Abfrage hängt an einem Seitenaufruf.
|
||||||
|
|
||||||
|
Das Ergebnis liegt in der Sitzung, **auch ein gescheitertes** – sonst liefe jeder
|
||||||
|
Seitenaufruf erneut in dieselbe Zeitüberschreitung. Scheitert die Abfrage, erklärt die
|
||||||
|
Seite, wo der Fehler meist steckt, und der Weg zurück zu den Zugangsdaten steht daneben.
|
||||||
|
Die Wahl lässt sich überspringen; die Verbindung ist auch ohne sie benutzbar.
|
||||||
|
|
||||||
|
Beim Übernehmen wird gegen die abgefragte Liste geprüft: Eine Kennung, die die Bank nicht
|
||||||
angeboten hat, wäre beim nächsten Abruf wirkungslos.
|
angeboten hat, wäre beim nächsten Abruf wirkungslos.
|
||||||
|
|
||||||
**Ohne Wahl** nimmt der erste Abruf das erste Verfahren, das die Bank meldet, und hält es
|
**Ohne Wahl** nimmt der erste Abruf das erste Verfahren, das die Bank meldet, und hält es
|
||||||
|
|
@ -81,8 +98,7 @@ fest; sonst stünde im Formular nichts. Eine **ausdrückliche** Wahl wird dagege
|
||||||
überschrieben: Bietet die Bank sie einmal nicht an, sagt das Protokoll das und die
|
überschrieben: Bietet die Bank sie einmal nicht an, sagt das Protokoll das und die
|
||||||
Einstellung bleibt stehen.
|
Einstellung bleibt stehen.
|
||||||
|
|
||||||
Das Verfahren steht bewusst nicht im Zugangsformular – dort ließe es sich nur als Zahl
|
Das Zugangsformular fasst Verfahren und Medium nicht an: Ohne diesen Vorbehalt setzte jedes
|
||||||
eintragen. Das Formular fasst es deshalb auch nicht an: Ohne diesen Vorbehalt setzte jedes
|
|
||||||
Speichern die Wahl zurück.
|
Speichern die Wahl zurück.
|
||||||
|
|
||||||
Abruf und Überweisung benutzen dieselbe Wahl (`Services\FinTsVerfahren::anwenden()`) – eine
|
Abruf und Überweisung benutzen dieselbe Wahl (`Services\FinTsVerfahren::anwenden()`) – eine
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,10 @@ form.inline { display: inline; margin: 0; }
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phasenmarke {
|
/* Kleine Marke: Zustand (Monatsabschluss), Dateiname (Import) oder der Schritt
|
||||||
|
eines mehrteiligen Dialogs (Einrichtung einer Bankverbindung). */
|
||||||
|
.phasenmarke,
|
||||||
|
.schrittmarke {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: var(--akzent-schwach);
|
background: var(--akzent-schwach);
|
||||||
color: var(--akzent);
|
color: var(--akzent);
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,11 @@ class BankingController extends BaseController
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Weiter zum zweiten Schritt: Erst jetzt – mit gespeicherten Zugangsdaten –
|
||||||
|
// lässt sich die Bank nach ihren TAN-Verfahren fragen.
|
||||||
View::redirect(
|
View::redirect(
|
||||||
'/banking/' . $id,
|
'/banking/' . $id . '/verfahren?einrichtung=1',
|
||||||
'Bankverbindung angelegt. Beim ersten Abruf werden die Konten der Bank ermittelt.'
|
'Zugangsdaten gespeichert. Jetzt das TAN-Verfahren wählen.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,9 +129,15 @@ class BankingController extends BaseController
|
||||||
// ── TAN-Verfahren ────────────────────────────────────────────────────────
|
// ── TAN-Verfahren ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Die abgefragte Verfahrensliste. Sie liegt in der Sitzung, damit ein
|
* Der zweite Schritt der Einrichtung: das TAN-Verfahren.
|
||||||
* erneutes Anzeigen der Seite – etwa nach einer Fehlermeldung – nicht jedes
|
*
|
||||||
* Mal wieder die Bank behelligt.
|
* Die Liste wird beim Aufruf **selbst geholt** – erst mit gespeicherten
|
||||||
|
* Zugangsdaten lässt sich die Bank fragen, und genau dann steht der Nutzer
|
||||||
|
* hier. Ein eigener Knopf davor wäre ein Schritt, dessen Ergebnis ohnehin
|
||||||
|
* feststeht.
|
||||||
|
*
|
||||||
|
* Das Ergebnis bleibt in der Sitzung, auch ein gescheitertes: Sonst liefe
|
||||||
|
* jeder Seitenaufruf erneut in dieselbe Zeitüberschreitung.
|
||||||
*
|
*
|
||||||
* @param array<string,string> $params
|
* @param array<string,string> $params
|
||||||
*/
|
*/
|
||||||
|
|
@ -144,33 +152,32 @@ class BankingController extends BaseController
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$abfrage = $this->gemerkteAbfrage($id) ?? $this->verfahrenHolen($id);
|
||||||
|
|
||||||
$this->render('banking/verfahren.php', [
|
$this->render('banking/verfahren.php', [
|
||||||
'titel' => 'TAN-Verfahren – ' . $zugang['name'],
|
'titel' => 'TAN-Verfahren – ' . $zugang['name'],
|
||||||
'zugang' => $zugang,
|
'zugang' => $zugang,
|
||||||
'medium' => Crypto::entschluessle((string)($zugang['tan_medium'] ?? '')),
|
'medium' => Crypto::entschluessle((string)($zugang['tan_medium'] ?? '')),
|
||||||
'verfahren' => $this->gemerkteVerfahren($id),
|
'verfahren' => $abfrage['liste'],
|
||||||
|
'fehler' => $abfrage['status'] === 'fehler' ? $abfrage['meldung'] : '',
|
||||||
|
'einrichtung' => $this->query('einrichtung') === '1',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string,string> $params */
|
/**
|
||||||
|
* Ein erneuter Anlauf, wenn der erste scheiterte oder sich etwas geändert hat.
|
||||||
|
*
|
||||||
|
* @param array<string,string> $params
|
||||||
|
*/
|
||||||
public function verfahrenAbfragen(array $params): void
|
public function verfahrenAbfragen(array $params): void
|
||||||
{
|
{
|
||||||
$this->verlangeAnmeldung();
|
$this->verlangeAnmeldung();
|
||||||
|
|
||||||
$id = (int)$params['id'];
|
$id = (int)$params['id'];
|
||||||
$ergebnis = FinTsVerfahren::abfragen($id);
|
$abfrage = $this->verfahrenHolen($id);
|
||||||
|
$ziel = '/banking/' . $id . '/verfahren' . ($this->post('einrichtung') === '1' ? '?einrichtung=1' : '');
|
||||||
|
|
||||||
Auth::startSession();
|
View::redirect($ziel, $abfrage['meldung'], $abfrage['status'] === 'fehler' ? 'fehler' : 'erfolg');
|
||||||
$_SESSION[self::SITZUNG_VERFAHREN] = [
|
|
||||||
'zugang_id' => $id,
|
|
||||||
'liste' => $ergebnis['verfahren'],
|
|
||||||
];
|
|
||||||
|
|
||||||
View::redirect(
|
|
||||||
'/banking/' . $id . '/verfahren',
|
|
||||||
$ergebnis['meldung'],
|
|
||||||
$ergebnis['status'] === 'fehler' ? 'fehler' : 'erfolg'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string,string> $params */
|
/** @param array<string,string> $params */
|
||||||
|
|
@ -180,6 +187,7 @@ class BankingController extends BaseController
|
||||||
|
|
||||||
$id = (int)$params['id'];
|
$id = (int)$params['id'];
|
||||||
$verfahren = (int)$this->post('verfahren_id');
|
$verfahren = (int)$this->post('verfahren_id');
|
||||||
|
$abfrage = $this->gemerkteAbfrage($id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
FinTsVerfahren::uebernehmen(
|
FinTsVerfahren::uebernehmen(
|
||||||
|
|
@ -188,31 +196,61 @@ class BankingController extends BaseController
|
||||||
// Das Medium des angekreuzten Verfahrens, nicht das irgendeiner
|
// Das Medium des angekreuzten Verfahrens, nicht das irgendeiner
|
||||||
// anderen Zeile.
|
// anderen Zeile.
|
||||||
$this->post('tan_medium_' . $verfahren),
|
$this->post('tan_medium_' . $verfahren),
|
||||||
$this->gemerkteVerfahren($id)
|
$abfrage === null ? [] : $abfrage['liste']
|
||||||
);
|
);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
View::redirect('/banking/' . $id . '/verfahren', $e->getMessage(), 'fehler');
|
View::redirect('/banking/' . $id . '/verfahren', $e->getMessage(), 'fehler');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
View::redirect('/banking/' . $id, 'TAN-Verfahren übernommen.');
|
View::redirect(
|
||||||
|
'/banking/' . $id,
|
||||||
|
$this->post('einrichtung') === '1'
|
||||||
|
? 'Bankverbindung eingerichtet. Der erste Abruf ermittelt jetzt die Konten.'
|
||||||
|
: 'TAN-Verfahren übernommen.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragt die Bank und merkt sich das Ergebnis für diese Sitzung.
|
||||||
|
*
|
||||||
|
* @return array{status: string, meldung: string, liste: list<array<string,mixed>>}
|
||||||
|
*/
|
||||||
|
private function verfahrenHolen(int $zugangId): array
|
||||||
|
{
|
||||||
|
$ergebnis = FinTsVerfahren::abfragen($zugangId);
|
||||||
|
|
||||||
|
$abfrage = [
|
||||||
|
'status' => $ergebnis['status'],
|
||||||
|
'meldung' => $ergebnis['meldung'],
|
||||||
|
'liste' => $ergebnis['verfahren'],
|
||||||
|
];
|
||||||
|
|
||||||
|
Auth::startSession();
|
||||||
|
$_SESSION[self::SITZUNG_VERFAHREN] = ['zugang_id' => $zugangId] + $abfrage;
|
||||||
|
|
||||||
|
return $abfrage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Die zuletzt abgefragte Liste – aber nur die zu diesem Zugang.
|
* Die zuletzt abgefragte Liste – aber nur die zu diesem Zugang.
|
||||||
*
|
*
|
||||||
* @return list<array<string,mixed>>
|
* @return array{status: string, meldung: string, liste: list<array<string,mixed>>}|null
|
||||||
*/
|
*/
|
||||||
private function gemerkteVerfahren(int $zugangId): array
|
private function gemerkteAbfrage(int $zugangId): ?array
|
||||||
{
|
{
|
||||||
Auth::startSession();
|
Auth::startSession();
|
||||||
$gemerkt = $_SESSION[self::SITZUNG_VERFAHREN] ?? null;
|
$gemerkt = $_SESSION[self::SITZUNG_VERFAHREN] ?? null;
|
||||||
|
|
||||||
if (!is_array($gemerkt) || (int)($gemerkt['zugang_id'] ?? 0) !== $zugangId) {
|
if (!is_array($gemerkt) || (int)($gemerkt['zugang_id'] ?? 0) !== $zugangId) {
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return is_array($gemerkt['liste'] ?? null) ? $gemerkt['liste'] : [];
|
return [
|
||||||
|
'status' => (string)($gemerkt['status'] ?? 'fehler'),
|
||||||
|
'meldung' => (string)($gemerkt['meldung'] ?? ''),
|
||||||
|
'liste' => is_array($gemerkt['liste'] ?? null) ? $gemerkt['liste'] : [],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Abruf ────────────────────────────────────────────────────────────────
|
// ── Abruf ────────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,12 @@ class FinTsVerfahren
|
||||||
$optionen->productName = Einstellung::get('banking_produkt_id') ?: self::PRODUKT;
|
$optionen->productName = Einstellung::get('banking_produkt_id') ?: self::PRODUKT;
|
||||||
$optionen->productVersion = self::PRODUKT_VERSION;
|
$optionen->productVersion = self::PRODUKT_VERSION;
|
||||||
|
|
||||||
|
// Kürzer als die Vorgabe der Bibliothek (15/30): Die Abfrage hängt an
|
||||||
|
// einem Seitenaufruf, und eine Bank, die nicht antwortet, soll die Seite
|
||||||
|
// nicht minutenlang aufhalten.
|
||||||
|
$optionen->timeoutConnect = 8;
|
||||||
|
$optionen->timeoutResponse = 20;
|
||||||
|
|
||||||
$fints = FinTs::new(
|
$fints = FinTs::new(
|
||||||
$optionen,
|
$optionen,
|
||||||
Credentials::create((string)$zugang['benutzerkennung'], (string)$zugang['pin'])
|
Credentials::create((string)$zugang['benutzerkennung'], (string)$zugang['pin'])
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ $wert = static fn(string $feld, string $standard = ''): string => (string)($zu
|
||||||
include __DIR__ . '/../layout/kopf.php';
|
include __DIR__ . '/../layout/kopf.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<?php if ($neu): ?>
|
||||||
|
<p class="schrittmarke">Schritt 1 von 2 · Zugangsdaten</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="seitenaktionen">
|
<div class="seitenaktionen">
|
||||||
<a href="/banking" class="knopf">Zurück zu den Bankverbindungen</a>
|
<a href="/banking" class="knopf">Zurück zu den Bankverbindungen</a>
|
||||||
<?php if (!$neu): ?>
|
<?php if (!$neu): ?>
|
||||||
|
|
@ -95,9 +99,7 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
Die Wahl steht auf einer eigenen Seite, weil dafür die Bank gefragt werden muss:
|
Die Wahl steht auf einer eigenen Seite, weil dafür die Bank gefragt werden muss:
|
||||||
<a href="/banking/<?= (int)$zugang['id'] ?>/verfahren">TAN-Verfahren einstellen</a>.
|
<a href="/banking/<?= (int)$zugang['id'] ?>/verfahren">TAN-Verfahren einstellen</a>.
|
||||||
</p>
|
</p>
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (!$neu): ?>
|
|
||||||
<h3>Konten bei dieser Bank</h3>
|
<h3>Konten bei dieser Bank</h3>
|
||||||
<?php if ($konten === []): ?>
|
<?php if ($konten === []): ?>
|
||||||
<p class="platzhalter">
|
<p class="platzhalter">
|
||||||
|
|
@ -153,8 +155,15 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($neu): ?>
|
||||||
|
<p class="feldhinweis">
|
||||||
|
Nach dem Speichern fragt der Finanzmanager die Bank, welche TAN-Verfahren sie für
|
||||||
|
diese Benutzerkennung anbietet – ohne gespeicherte Zugangsdaten geht das nicht.
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="knopfleiste">
|
<div class="knopfleiste">
|
||||||
<button type="submit" class="knopf primaer"><?= $neu ? 'Anlegen' : 'Speichern' ?></button>
|
<button type="submit" class="knopf primaer"><?= $neu ? 'Weiter' : 'Speichern' ?></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,74 +3,81 @@
|
||||||
use Finanzmanager\View;
|
use Finanzmanager\View;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Die Wahl des TAN-Verfahrens für einen Bankzugang.
|
* Schritt 2 der Einrichtung: das TAN-Verfahren.
|
||||||
|
*
|
||||||
|
* Die Liste hat der Controller schon geholt – hier steht nur noch die Wahl.
|
||||||
*
|
*
|
||||||
* @var array<string,mixed> $zugang
|
* @var array<string,mixed> $zugang
|
||||||
* @var string $medium Das gespeicherte TAN-Medium im Klartext
|
* @var string $medium Das gespeicherte TAN-Medium im Klartext
|
||||||
* @var list<array{id:int, name:string, entkoppelt:bool, medium_noetig:bool,
|
* @var list<array{id:int, name:string, entkoppelt:bool, medium_noetig:bool,
|
||||||
* medien:list<string>, hinweis:string}> $verfahren
|
* medien:list<string>, hinweis:string}> $verfahren
|
||||||
|
* @var string $fehler Meldung, wenn die Abfrage scheiterte
|
||||||
|
* @var bool $einrichtung Teil des Einrichtungsablaufs?
|
||||||
* @var string $csrfToken
|
* @var string $csrfToken
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$gewaehlt = $zugang['tan_verfahren_id'] === null ? null : (int)$zugang['tan_verfahren_id'];
|
$gewaehlt = $zugang['tan_verfahren_id'] === null ? null : (int)$zugang['tan_verfahren_id'];
|
||||||
|
$zugangId = (int)$zugang['id'];
|
||||||
|
|
||||||
include __DIR__ . '/../layout/kopf.php';
|
include __DIR__ . '/../layout/kopf.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<?php if ($einrichtung): ?>
|
||||||
|
<p class="schrittmarke">Schritt 2 von 2 · TAN-Verfahren</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="seitenaktionen">
|
<div class="seitenaktionen">
|
||||||
<a href="/banking/<?= (int)$zugang['id'] ?>" class="knopf">Zurück zur Bankverbindung</a>
|
<a href="/banking/<?= $zugangId ?>" class="knopf">
|
||||||
<form method="post" action="/banking/<?= (int)$zugang['id'] ?>/verfahren/abfragen" class="inline">
|
<?= $einrichtung ? 'Später einstellen' : 'Zurück zur Bankverbindung' ?>
|
||||||
|
</a>
|
||||||
|
<form method="post" action="/banking/<?= $zugangId ?>/verfahren/abfragen" class="inline">
|
||||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||||
<button type="submit" class="knopf primaer">
|
<input type="hidden" name="einrichtung" value="<?= $einrichtung ? '1' : '0' ?>">
|
||||||
<?= $verfahren === [] ? 'Verfahren bei der Bank abfragen' : 'Erneut abfragen' ?>
|
<button type="submit" class="knopf">Erneut bei der Bank abfragen</button>
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="feldhinweis">
|
<?php if ($fehler !== ''): ?>
|
||||||
Welche Verfahren zur Verfügung stehen, weiß nur die Bank – die Kennungen sind
|
<p class="meldung meldung-fehler" role="status">
|
||||||
von Haus zu Haus verschieden. Die Abfrage kommt ohne Anmeldung aus und verlangt
|
<?= View::h($fehler) ?>
|
||||||
deshalb selbst keine TAN.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>Zurzeit eingestellt</h3>
|
|
||||||
<?php if ($gewaehlt === null): ?>
|
|
||||||
<p class="platzhalter">
|
|
||||||
Noch nichts gewählt. Beim nächsten Abruf nimmt der Finanzmanager das erste
|
|
||||||
Verfahren, das die Bank meldet, und merkt es sich – das muss nicht das sein,
|
|
||||||
mit dem du sonst arbeitest.
|
|
||||||
</p>
|
</p>
|
||||||
<?php else: ?>
|
<p class="feldhinweis">
|
||||||
<p>
|
Meist stimmt eine Angabe im ersten Schritt nicht – Bankleitzahl, FinTS-Adresse,
|
||||||
<strong>
|
Benutzerkennung oder PIN.
|
||||||
<?= View::h(($zugang['tan_verfahren_name'] ?? '') !== ''
|
<a href="/banking/<?= $zugangId ?>">Zugangsdaten prüfen</a>.
|
||||||
? $zugang['tan_verfahren_name'] : 'Verfahren ' . $gewaehlt) ?>
|
Die Wahl lässt sich auch später nachholen; ohne sie nimmt der erste Abruf das erste
|
||||||
</strong>
|
Verfahren, das die Bank meldet.
|
||||||
<span class="nebentext">Kennung <?= $gewaehlt ?></span>
|
|
||||||
</p>
|
</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($verfahren === []): ?>
|
<?php if ($verfahren === []): ?>
|
||||||
<p class="platzhalter">
|
<?php if ($fehler === ''): ?>
|
||||||
Noch keine Liste abgefragt. Der Knopf oben holt sie – dafür müssen
|
<p class="platzhalter">Die Bank hat kein Verfahren genannt.</p>
|
||||||
Bankleitzahl, FinTS-Adresse, Benutzerkennung und PIN gespeichert sein.
|
<?php endif; ?>
|
||||||
</p>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<h3>Von der Bank angeboten</h3>
|
<p class="feldhinweis">
|
||||||
<form method="post" action="/banking/<?= (int)$zugang['id'] ?>/verfahren" class="formular breit">
|
Welche Verfahren zur Verfügung stehen, weiß nur die Bank – die Kennungen sind von
|
||||||
|
Haus zu Haus verschieden. Zu wählen ist das, mit dem du sonst arbeitest.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post" action="/banking/<?= $zugangId ?>/verfahren" class="formular breit">
|
||||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||||
|
<input type="hidden" name="einrichtung" value="<?= $einrichtung ? '1' : '0' ?>">
|
||||||
|
|
||||||
<table class="liste">
|
<table class="liste">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Wahl</th><th>Verfahren</th><th>Kennung</th><th>TAN-Medium</th></tr>
|
<tr><th>Wahl</th><th>Verfahren</th><th>Kennung</th><th>TAN-Medium</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($verfahren as $eintrag): ?>
|
<?php foreach ($verfahren as $nummer => $eintrag): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<?php /* Ohne Vorwahl bliebe das Formular leer abschickbar; die
|
||||||
|
erste Zeile ist die naheliegende Voreinstellung. */ ?>
|
||||||
<input type="radio" name="verfahren_id" id="verfahren-<?= (int)$eintrag['id'] ?>"
|
<input type="radio" name="verfahren_id" id="verfahren-<?= (int)$eintrag['id'] ?>"
|
||||||
value="<?= (int)$eintrag['id'] ?>"
|
value="<?= (int)$eintrag['id'] ?>"
|
||||||
<?= $gewaehlt === (int)$eintrag['id'] ? 'checked' : '' ?>>
|
<?= $gewaehlt === (int)$eintrag['id'] || ($gewaehlt === null && $nummer === 0)
|
||||||
|
? 'checked' : '' ?>>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<label for="verfahren-<?= (int)$eintrag['id'] ?>">
|
<label for="verfahren-<?= (int)$eintrag['id'] ?>">
|
||||||
|
|
@ -107,9 +114,30 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="knopfleiste">
|
<div class="knopfleiste">
|
||||||
<button type="submit" class="knopf primaer">Verfahren übernehmen</button>
|
<button type="submit" class="knopf primaer">
|
||||||
|
<?= $einrichtung ? 'Einrichtung abschließen' : 'Verfahren übernehmen' ?>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!$einrichtung): ?>
|
||||||
|
<h3>Zurzeit eingestellt</h3>
|
||||||
|
<?php if ($gewaehlt === null): ?>
|
||||||
|
<p class="platzhalter">
|
||||||
|
Noch nichts gewählt. Beim nächsten Abruf nimmt der Finanzmanager das erste
|
||||||
|
Verfahren, das die Bank meldet, und merkt es sich.
|
||||||
|
</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
<?= View::h(($zugang['tan_verfahren_name'] ?? '') !== ''
|
||||||
|
? $zugang['tan_verfahren_name'] : 'Verfahren ' . $gewaehlt) ?>
|
||||||
|
</strong>
|
||||||
|
<span class="nebentext">Kennung <?= $gewaehlt ?><?= $medium !== ''
|
||||||
|
? ', Medium ' . View::h($medium) : '' ?></span>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php include __DIR__ . '/../layout/fuss.php'; ?>
|
<?php include __DIR__ . '/../layout/fuss.php'; ?>
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,11 @@ test.describe('Module der Phasen 4 bis 7', () => {
|
||||||
['/verwaltung', 'Verwaltung'],
|
['/verwaltung', 'Verwaltung'],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Jeder Bereich hat sein Modul: Die Überschrift gehört zum Modul, nicht
|
||||||
|
// zur früheren Platzhalterseite.
|
||||||
for (const [pfad, titel] of punkte) {
|
for (const [pfad, titel] of punkte) {
|
||||||
await page.goto(pfad)
|
await page.goto(pfad)
|
||||||
await expect(page.locator('h1')).toContainText(titel)
|
await expect(page.locator('h1')).toContainText(titel)
|
||||||
// Kein Platzhalter mehr: Alle Bereiche haben ihr Modul.
|
|
||||||
await expect(page.locator('.phasenmarke')).toHaveCount(0)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -94,24 +94,28 @@ test.describe('Module der Phasen 4 bis 7', () => {
|
||||||
await page.fill('#fints_url', 'https://banking.example.org/fints')
|
await page.fill('#fints_url', 'https://banking.example.org/fints')
|
||||||
await page.fill('#benutzerkennung', 'e2e-kunde')
|
await page.fill('#benutzerkennung', 'e2e-kunde')
|
||||||
await page.fill('#pin', 'geheim')
|
await page.fill('#pin', 'geheim')
|
||||||
await page.click('button:has-text("Anlegen")')
|
|
||||||
|
|
||||||
await expect(page.locator('.meldung')).toContainText('angelegt')
|
// Schritt 1 speichert nur die Zugangsdaten.
|
||||||
|
await expect(page.locator('.schrittmarke')).toContainText('Schritt 1 von 2')
|
||||||
|
await page.click('button:has-text("Weiter")')
|
||||||
|
|
||||||
|
// Schritt 2 fragt die Bank selbst nach ihren TAN-Verfahren. Die Adresse
|
||||||
|
// im Test führt nirgendwohin – geprüft wird deshalb der Fehlerweg: Die
|
||||||
|
// Seite erklärt, was zu tun ist, statt stehen zu bleiben.
|
||||||
|
await expect(page.locator('.schrittmarke')).toContainText('Schritt 2 von 2')
|
||||||
|
await expect(page.locator('h1')).toContainText('E2E-Testbank')
|
||||||
|
await expect(page.locator('.meldung-fehler')).toBeVisible()
|
||||||
|
await expect(page.locator('a:has-text("Zugangsdaten prüfen")')).toHaveCount(1)
|
||||||
|
|
||||||
|
await page.click('a:has-text("Später einstellen")')
|
||||||
await expect(page.locator('h1')).toContainText('E2E-Testbank')
|
await expect(page.locator('h1')).toContainText('E2E-Testbank')
|
||||||
|
|
||||||
// Die PIN wird nie wieder angezeigt.
|
// Die PIN wird nie wieder angezeigt.
|
||||||
await expect(page.locator('#pin')).toHaveValue('')
|
await expect(page.locator('#pin')).toHaveValue('')
|
||||||
await expect(page.locator('#pin')).toHaveAttribute('placeholder', /unverändert/)
|
await expect(page.locator('#pin')).toHaveAttribute('placeholder', /unverändert/)
|
||||||
|
|
||||||
// Das TAN-Verfahren steht auf einer eigenen Seite. Ohne Abfrage bei der
|
// Ohne Wahl bleibt der Hinweis stehen, und die Seite ist wieder erreichbar.
|
||||||
// Bank ist dort nichts gewählt – abgefragt wird hier nicht, das bräuchte
|
await expect(page.locator('a:has-text("TAN-Verfahren einstellen")')).toHaveCount(1)
|
||||||
// eine echte Bankverbindung.
|
|
||||||
await page.click('a:has-text("TAN-Verfahren einstellen")')
|
|
||||||
await expect(page.locator('h1')).toContainText('TAN-Verfahren')
|
|
||||||
await expect(page.locator('.platzhalter').first()).toContainText('Noch nichts gewählt')
|
|
||||||
await expect(page.locator('button:has-text("Verfahren bei der Bank abfragen")')).toHaveCount(1)
|
|
||||||
|
|
||||||
await page.click('a:has-text("Zurück zur Bankverbindung")')
|
|
||||||
|
|
||||||
page.once('dialog', dialog => dialog.accept())
|
page.once('dialog', dialog => dialog.accept())
|
||||||
await page.click('button:has-text("Bankverbindung löschen")')
|
await page.click('button:has-text("Bankverbindung löschen")')
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue