Authorize consents for the account-holder type the bank supports

Kontist authorized but shared no accounts: psu_type was hardcoded to
personal, and Enable Banking documents that a psu_type mismatch can
yield a consent without the expected accounts. The bank listing now
reports each institution's supported psu_types, the connect form offers
only those, the chosen type reaches POST /auth, and an unsupported
combination is refused before the user is sent to a bank. The choice is
stored per consent so reconnecting reuses it; consents predating the
choice stay personal.

Also repairs the frontend derivation, which the Montserrat dependency
broke: npmDepsHash was stale and web/public was missing from the
fileset, so the traced duck icon never reached the built assets.
This commit is contained in:
Lars Nolden
2026-09-11 13:39:45 +02:00
parent 35d91a5c48
commit c33e8d5573
11 changed files with 282 additions and 61 deletions
+20 -6
View File
@@ -174,7 +174,10 @@ func (a *App) Backfill(ctx context.Context, rev, accountID string, historyMonths
}
return result, nil
}
func (a *App) Authorize(ctx context.Context, institution, country string, historyMonths int) (string, error) {
// Authorize starts a consent for one account-holder kind. An empty psuType
// keeps the previous personal default for existing API callers.
func (a *App) Authorize(ctx context.Context, institution, country, psuType string, historyMonths int) (string, error) {
a.mu.Lock()
defer a.mu.Unlock()
if historyMonths < 1 || historyMonths > 120 {
@@ -191,17 +194,24 @@ func (a *App) Authorize(ctx context.Context, institution, country string, histor
if len(country) != 2 {
return "", errors.New("country must be a two-letter code")
}
if psuType == "" {
psuType = banking.PSUPersonal
}
if !banking.ValidPSUType(psuType) {
return "", errors.New("account type must be personal or business")
}
for state, auth := range a.authStates {
if time.Now().After(auth.Expires) {
delete(a.authStates, state)
}
}
state := domain.NewID("auth")
url, err := a.bank.Authorize(ctx, institution, country, state)
if err == nil {
a.authStates[state] = authorization{Expires: time.Now().Add(15 * time.Minute), Institution: institution, Country: country, HistoryMonths: historyMonths}
url, err := a.bank.Authorize(ctx, institution, country, psuType, state)
if err != nil {
return "", bankFailure(err, "bank authorization unavailable; retry connecting")
}
return url, err
a.authStates[state] = authorization{Expires: time.Now().Add(15 * time.Minute), Institution: institution, Country: country, PSUType: psuType, HistoryMonths: historyMonths}
return url, nil
}
// Institutions lists connectable banks for the country so the UI can offer
@@ -276,7 +286,7 @@ func (a *App) Callback(ctx context.Context, code, state string) (int, error) {
return 0, errors.New("the bank authorized the connection but shared no accounts, so nothing was linked; accounts of another type (for example business) may need a separate consent")
}
a.ops.Sessions = append(a.ops.Sessions, session)
a.ops.Consents[session.ID] = Consent{Institution: auth.Institution, Country: auth.Country, HistoryMonths: auth.HistoryMonths}
a.ops.Consents[session.ID] = Consent{Institution: auth.Institution, Country: auth.Country, PSUType: auth.PSUType, HistoryMonths: auth.HistoryMonths}
if err = a.saveOps(); err != nil {
return 0, err
}
@@ -359,6 +369,10 @@ func bankFailure(err error, fallback string) error {
if errors.As(err, &api) {
return api
}
var consent *banking.ConsentError
if errors.As(err, &consent) {
return consent
}
return errors.New(fallback)
}