libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

account.rs (66183B)


      1 /*
      2 * This file is part of LibEuFin.
      3 * Copyright (C) 2026 Taler Systems S.A.
      4 
      5 * LibEuFin is free software; you can redistribute it and/or modify
      6 * it under the terms of the GNU Affero General Public License as
      7 * published by the Free Software Foundation; either version 3, or
      8 * (at your option) any later version.
      9 
     10 * LibEuFin is distributed in the hope that it will be useful, but
     11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13 * Public License for more details.
     14 
     15 * You should have received a copy of the GNU Affero General Public
     16 * License along with LibEuFin; see the file COPYING.  If not, see
     17 * <http://www.gnu.org/licenses/>
     18 */
     19 
     20 use std::{
     21     fmt::Debug,
     22     str::FromStr,
     23     sync::{Arc, LazyLock},
     24 };
     25 
     26 use axum::{
     27     Json, Router,
     28     extract::State,
     29     http::StatusCode,
     30     response::{IntoResponse, NoContent},
     31     routing::{get, patch, post},
     32 };
     33 use compact_str::{CompactString, format_compact};
     34 use regex::Regex;
     35 use serde::{Deserialize, Serialize};
     36 use sqlx::{Database, PgPool};
     37 use taler_api::{
     38     error::{ApiResult, bad_request, failure, failure_code, failure_status},
     39     extract::{Query, Req},
     40 };
     41 use taler_common::{
     42     api::params::{Page, PageParams, ParamsErr},
     43     error_code::ErrorCode::{self},
     44     types::{
     45         amount::Amount,
     46         base32::Base32,
     47         iban::{Country, IBAN},
     48         payto::{BankID, IbanPayto, Payto, PaytoURI},
     49     },
     50 };
     51 use taler_macros::EnumMeta;
     52 
     53 use crate::{
     54     TanChannel,
     55     api::{BankState, conversion::ConversionRate},
     56     auth::{AdminRAuth, RegistrationAuth, UserAuth, UserRAuth, require_admin},
     57     config::{BankCfg, WireMethod},
     58     db::{
     59         self,
     60         account::{
     61             CreationResult, DeletionResult, PatchAuthResult, PatchResult, by_username, page_admin,
     62             page_public, reconfig, reconfig_password,
     63         },
     64     },
     65     mfa::{AccountDeletionOp, AccountPasswordOp, AccountReconfigOp, MfaReq, Tans},
     66     payto::{FullBankPayto, LibeufinId, XTalerBank},
     67     pw::checkpw,
     68 };
     69 
     70 #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
     71 pub struct ChallengeContactData {
     72     #[serde(default)]
     73     pub email: Maybe<CompactString>,
     74     #[serde(default)]
     75     pub phone: Maybe<CompactString>,
     76 }
     77 
     78 impl ChallengeContactData {
     79     pub fn validate(&self) -> ApiResult<()> {
     80         static EMAIL_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
     81             Regex::new("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$").unwrap()
     82         });
     83         static PHONE_PATTERN: LazyLock<Regex> =
     84             LazyLock::new(|| Regex::new("^\\+?[0-9]+$").unwrap());
     85 
     86         if let Maybe::Some(email) = &self.email
     87             && !EMAIL_PATTERN.is_match(email)
     88         {
     89             return Err(bad_request(format_args!(
     90                 "email contact data '{email}' is malformed"
     91             )));
     92         }
     93         if let Maybe::Some(phone) = &self.phone
     94             && !PHONE_PATTERN.is_match(phone)
     95         {
     96             return Err(bad_request(format_args!(
     97                 "phone contact data '{phone}' is malformed"
     98             )));
     99         }
    100         Ok(())
    101     }
    102 }
    103 
    104 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    105 pub struct RegisterAccountRequest {
    106     pub username: CompactString,
    107     pub password: CompactString,
    108     pub name: CompactString,
    109     #[serde(default)]
    110     pub is_public: bool,
    111     #[serde(default)]
    112     pub is_taler_exchange: bool,
    113     #[serde(default)]
    114     pub contact_data: ChallengeContactData,
    115     pub cashout_payto_uri: Option<IbanPayto>,
    116     pub payto_uri: Option<Payto<LibeufinId>>,
    117     pub debit_threshold: Option<Amount>,
    118     pub tan_channel: Option<TanChannel>,
    119     pub tan_channels: Option<Vec<TanChannel>>,
    120     pub conversion_rate_class_id: Option<u64>,
    121 }
    122 
    123 impl RegisterAccountRequest {
    124     pub fn validate(&self) -> ApiResult<()> {
    125         static USERNAME_REGEX: LazyLock<Regex> =
    126             LazyLock::new(|| Regex::new("^[a-zA-Z0-9-._~]{1,126}$").unwrap());
    127         if !USERNAME_REGEX.is_match(&self.username) {
    128             return Err(bad_request(format_args!(
    129                 "username '{}' is malformed, must match [a-zA-Z0-9-._~]{{1,126}}",
    130                 self.username
    131             )));
    132         }
    133         if self.tan_channel.is_some() && self.tan_channels.is_some() {
    134             return Err(bad_request(
    135                 "you must only use either tan_channel or tan_channels",
    136             ));
    137         }
    138         self.contact_data.validate()?;
    139         Ok(())
    140     }
    141 
    142     pub fn channels(&self) -> &[TanChannel] {
    143         if let Some(many) = &self.tan_channels {
    144             many
    145         } else if let Some(one) = &self.tan_channel {
    146             std::slice::from_ref(one)
    147         } else {
    148             &[]
    149         }
    150     }
    151 }
    152 
    153 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    154 pub struct RegisterAccountResponse {
    155     pub internal_payto_uri: FullBankPayto,
    156 }
    157 
    158 #[derive(Debug, Clone, PartialEq, Eq, Default)]
    159 pub enum Maybe<T> {
    160     #[default]
    161     Missing,
    162     Null,
    163     Some(T),
    164 }
    165 
    166 impl<T> Maybe<T> {
    167     pub fn opt(&self) -> Option<&T> {
    168         match self {
    169             Maybe::Missing | Maybe::Null => None,
    170             Maybe::Some(v) => Some(v),
    171         }
    172     }
    173 
    174     pub fn is_some(&self) -> bool {
    175         matches!(self, Maybe::Some(_))
    176     }
    177 
    178     pub fn inner(&self) -> Option<Option<&T>> {
    179         match self {
    180             Maybe::Missing => None,
    181             Maybe::Null => Some(None),
    182             Maybe::Some(v) => Some(Some(v)),
    183         }
    184     }
    185 }
    186 
    187 impl<T: FromStr> FromStr for Maybe<T> {
    188     type Err = T::Err;
    189 
    190     fn from_str(s: &str) -> Result<Self, Self::Err> {
    191         if s.is_empty() {
    192             Ok(Self::Null)
    193         } else {
    194             Ok(Self::Some(s.parse()?))
    195         }
    196     }
    197 }
    198 
    199 impl<'de, T: Deserialize<'de>> Deserialize<'de> for Maybe<T> {
    200     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    201     where
    202         D: serde::Deserializer<'de>,
    203     {
    204         Ok(match Option::<T>::deserialize(deserializer)? {
    205             None => Maybe::Null,
    206             Some(v) => Maybe::Some(v),
    207         })
    208     }
    209 }
    210 
    211 impl<DB: Database, T: sqlx::Type<DB>> sqlx::Type<DB> for Maybe<T> {
    212     fn type_info() -> DB::TypeInfo {
    213         Option::<T>::type_info()
    214     }
    215 }
    216 
    217 impl<'r, DB: Database, T: sqlx::Decode<'r, DB>> sqlx::Decode<'r, DB> for Maybe<T> {
    218     fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
    219         Ok(match Option::<T>::decode(value)? {
    220             None => Maybe::Null,
    221             Some(v) => Maybe::Some(v),
    222         })
    223     }
    224 }
    225 
    226 impl<T: Serialize> Serialize for Maybe<T> {
    227     fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
    228         self.opt().serialize(serializer)
    229     }
    230 }
    231 
    232 impl<T> From<Option<T>> for Maybe<T> {
    233     fn from(value: Option<T>) -> Self {
    234         match value {
    235             None => Maybe::Null,
    236             Some(v) => Maybe::Some(v),
    237         }
    238     }
    239 }
    240 
    241 impl<T> From<Option<Maybe<T>>> for Maybe<T> {
    242     fn from(value: Option<Maybe<T>>) -> Self {
    243         match value {
    244             None => Maybe::Missing,
    245             Some(v) => v,
    246         }
    247     }
    248 }
    249 
    250 pub trait TanInfo: Debug {
    251     fn phone(&self) -> Option<&str>;
    252     fn email(&self) -> Option<&str>;
    253     fn channels(&self) -> &[TanChannel];
    254     fn mfa(&self) -> Tans {
    255         self.channels()
    256             .iter()
    257             .filter_map(|it| {
    258                 match it {
    259                     TanChannel::email => self.email(),
    260                     TanChannel::sms => self.phone(),
    261                 }
    262                 .map(|info| (*it, info.into()))
    263             })
    264             .collect()
    265     }
    266 }
    267 
    268 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    269 pub struct AccountReconfiguration {
    270     #[serde(default)]
    271     pub contact_data: ChallengeContactData,
    272     #[serde(default)]
    273     pub cashout_payto_uri: Maybe<IbanPayto>,
    274     pub name: Option<CompactString>,
    275     pub is_public: Option<bool>,
    276     pub debit_threshold: Option<Amount>,
    277     #[serde(default)]
    278     pub tan_channel: Maybe<TanChannel>,
    279     #[serde(default)]
    280     pub tan_channels: Option<Vec<TanChannel>>,
    281     pub is_taler_exchange: Option<bool>,
    282     #[serde(default)]
    283     pub conversion_rate_class_id: Maybe<u64>,
    284 }
    285 
    286 impl TanInfo for AccountReconfiguration {
    287     fn phone(&self) -> Option<&str> {
    288         self.contact_data.phone.opt().map(|it| it.as_str())
    289     }
    290 
    291     fn email(&self) -> Option<&str> {
    292         self.contact_data.email.opt().map(|it| it.as_str())
    293     }
    294 
    295     fn channels(&self) -> &[TanChannel] {
    296         if let Some(many) = &self.tan_channels {
    297             many
    298         } else if let Some(one) = self.tan_channel.opt() {
    299             std::slice::from_ref(one)
    300         } else {
    301             &[]
    302         }
    303     }
    304 }
    305 
    306 impl AccountReconfiguration {
    307     pub fn check(&self) -> ApiResult<()> {
    308         if self.tan_channel.is_some() && self.tan_channels.is_some() {
    309             return Err(bad_request(
    310                 "you must only use either tan_channel or tan_channels",
    311             ));
    312         }
    313 
    314         Ok(())
    315     }
    316 
    317     pub fn channels(&self) -> Option<&[TanChannel]> {
    318         if let Some(many) = &self.tan_channels {
    319             Some(many)
    320         } else if let Some(value) = self.tan_channel.inner() {
    321             match value {
    322                 Some(one) => Some(std::slice::from_ref(one)),
    323                 None => Some(&[]),
    324             }
    325         } else {
    326             None
    327         }
    328     }
    329 
    330     pub fn required_validation(&self, current: &impl TanInfo) -> ApiResult<Tans> {
    331         // Tan channels are either the new ones or the current one
    332         let channels = self.channels().unwrap_or_else(|| current.channels());
    333         let validated = current.mfa();
    334 
    335         channels
    336             .iter()
    337             .filter_map(|channel| {
    338                 // Info are either the new one or the current ones
    339                 let info = match channel {
    340                     TanChannel::sms => self.phone().or(current.phone()),
    341                     TanChannel::email => self.email().or(current.email()),
    342                 };
    343 
    344                 let Some(info) = info else {
    345                     return Some(Err(failure(
    346                         ErrorCode::BANK_MISSING_TAN_INFO,
    347                         format_args!("missing info for tan channel {channel}"),
    348                     )));
    349                 };
    350 
    351                 let tan = (*channel, info.into());
    352 
    353                 // Check if tan is already used and therefore already validated
    354                 (!(validated.contains(&tan))).then_some(Ok(tan))
    355             })
    356             .collect::<Result<Vec<_>, _>>()
    357     }
    358 }
    359 
    360 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumMeta)]
    361 #[enum_meta(Str)]
    362 #[allow(non_camel_case_types)]
    363 pub enum CreditDebitInfo {
    364     credit,
    365     debit,
    366 }
    367 
    368 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    369 pub struct Balance {
    370     pub amount: Amount,
    371     pub credit_debit_indicator: CreditDebitInfo,
    372 }
    373 
    374 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumMeta, sqlx::Type)]
    375 #[enum_meta(Str)]
    376 #[sqlx(type_name = "TEXT")]
    377 #[allow(non_camel_case_types)]
    378 pub enum AccountStatus {
    379     active,
    380     locked,
    381     deleted,
    382 }
    383 
    384 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    385 pub struct AccountData {
    386     pub name: CompactString,
    387     pub payto_uri: FullBankPayto,
    388     pub balance: Balance,
    389     pub debit_threshold: Amount,
    390     pub contact_data: ChallengeContactData,
    391     pub cashout_payto_uri: Option<PaytoURI>,
    392     pub tan_channel: Option<TanChannel>,
    393     pub tan_channels: Vec<TanChannel>,
    394     pub is_public: bool,
    395     pub is_taler_exchange: bool,
    396     pub is_locked: bool,
    397     pub status: AccountStatus,
    398     pub conversion_rate_class_id: Option<u64>,
    399     pub conversion_rate: Option<ConversionRate>,
    400 }
    401 
    402 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    403 pub struct AccountMinimalData {
    404     pub username: CompactString,
    405     pub name: CompactString,
    406     pub payto_uri: FullBankPayto,
    407     pub balance: Balance,
    408     pub debit_threshold: Amount,
    409     pub is_public: bool,
    410     pub is_taler_exchange: bool,
    411     pub is_locked: bool,
    412     pub row_id: u64,
    413     pub status: AccountStatus,
    414     pub conversion_rate_class_id: Option<u64>,
    415     pub conversion_rate: Option<ConversionRate>,
    416 }
    417 
    418 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    419 pub struct ListBankAccountsResponse {
    420     pub accounts: Vec<AccountMinimalData>,
    421 }
    422 
    423 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    424 pub struct PublicAccount {
    425     pub username: CompactString,
    426     pub payto_uri: FullBankPayto,
    427     pub balance: Balance,
    428     pub is_taler_exchange: bool,
    429     pub row_id: u64,
    430 }
    431 
    432 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    433 pub struct PublicAccountsResponse {
    434     pub public_accounts: Vec<PublicAccount>,
    435 }
    436 
    437 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    438 pub struct AccountPasswordChange {
    439     pub new_password: CompactString,
    440     pub old_password: Option<CompactString>,
    441 }
    442 
    443 #[derive(Debug, Clone, Deserialize)]
    444 pub struct AccountParams {
    445     #[serde(flatten)]
    446     pub page: PageParams,
    447     pub filter_name: Option<CompactString>,
    448     pub conversion_rate_class_id: Option<u64>,
    449 }
    450 
    451 impl AccountParams {
    452     pub fn check(self) -> Result<Account, ParamsErr> {
    453         Ok(Account {
    454             page: self.page.check()?,
    455             filter_name: self.filter_name.map(|it| format_compact!("%{it}%")),
    456             conversion_rate_class_id: self.conversion_rate_class_id,
    457         })
    458     }
    459 }
    460 
    461 #[derive(Debug)]
    462 pub struct Account {
    463     pub page: Page,
    464     pub filter_name: Option<CompactString>,
    465     pub conversion_rate_class_id: Option<u64>,
    466 }
    467 
    468 pub fn account_api() -> Router<Arc<BankState>> {
    469     Router::new()
    470         .route(
    471             "/accounts",
    472             post(
    473                 async |RegistrationAuth { is_admin }: RegistrationAuth,
    474                        State(state): State<Arc<BankState>>,
    475                        Req(req): Req<RegisterAccountRequest>|
    476                        -> ApiResult<Json<RegisterAccountResponse>> {
    477                     match create_account(&state.db, &state.cfg, &req, is_admin).await? {
    478                         CreationResult::BonusBalanceInsufficient => {
    479                             Err(failure_code(ErrorCode::BANK_UNALLOWED_DEBIT))
    480                         }
    481                         CreationResult::UsernameReuse => {
    482                             Err(failure_code(ErrorCode::BANK_REGISTER_USERNAME_REUSE))
    483                         }
    484                         CreationResult::PayToReuse => {
    485                             Err(failure_code(ErrorCode::BANK_REGISTER_PAYTO_URI_REUSE))
    486                         }
    487                         CreationResult::UnknownConversionClass => {
    488                             Err(failure_code(ErrorCode::BANK_CONVERSION_RATE_CLASS_UNKNOWN))
    489                         }
    490                         CreationResult::Success(payto) => Ok(Json(RegisterAccountResponse {
    491                             internal_payto_uri: payto,
    492                         })),
    493                     }
    494                 },
    495             )
    496             .get(
    497                 async |State(state): State<Arc<BankState>>,
    498                        _: AdminRAuth,
    499                        Query(params): Query<AccountParams>| {
    500                     let params = params.check()?;
    501                     let accounts = page_admin(
    502                         &state.db,
    503                         &state.cfg.ctx,
    504                         &state.cfg.regional_currency,
    505                         state.cfg.fiat_currency(),
    506                         &params,
    507                     )
    508                     .await?;
    509                     if accounts.is_empty() {
    510                         ApiResult::Ok(NoContent.into_response())
    511                     } else {
    512                         Ok(Json(ListBankAccountsResponse { accounts }).into_response())
    513                     }
    514                 },
    515             ),
    516         )
    517         .route(
    518             "/public-accounts",
    519             get(
    520                 async |State(state): State<Arc<BankState>>, Query(params): Query<AccountParams>| {
    521                     let params = params.check()?;
    522                     let accounts = page_public(
    523                         &state.db,
    524                         &state.cfg.ctx,
    525                         &state.cfg.regional_currency,
    526                         &params,
    527                     )
    528                     .await?;
    529                     if accounts.is_empty() {
    530                         ApiResult::Ok(NoContent.into_response())
    531                     } else {
    532                         Ok(Json(PublicAccountsResponse {
    533                             public_accounts: accounts,
    534                         })
    535                         .into_response())
    536                     }
    537                 },
    538             ),
    539         )
    540         .route(
    541             "/accounts/{username}",
    542             patch(
    543                 async |State(state): State<Arc<BankState>>, req: MfaReq<AccountReconfigOp>| {
    544                     let (mut auth, req, mfa) = req.solve(&state, &[]).await?;
    545                     if let Some(tans) = mfa.pending_mfa() {
    546                         return mfa.response_validation(&auth, &state.db, tans).await;
    547                     }
    548                     match patch_account(
    549                         &state.db,
    550                         &state.cfg,
    551                         &req,
    552                         &auth.username,
    553                         auth.is_admin(),
    554                         mfa.is_2fa(),
    555                     )
    556                     .await?
    557                     {
    558                         PatchResult::Success => Ok(NoContent.into_response()),
    559                         PatchResult::MissingTanInfo(e) => Err(e),
    560                         PatchResult::Challenges(tans) => {
    561                             if tans.is_empty() {
    562                                 mfa.response_mfa(&mut auth, &state.db, &state.cfg.ctx).await
    563                             } else {
    564                                 mfa.response_validation(&auth, &state.db, &tans).await
    565                             }
    566                         }
    567                         PatchResult::UnknownAccount => {
    568                             Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT))
    569                         }
    570                         PatchResult::NonAdminName => {
    571                             Err(failure_code(ErrorCode::BANK_NON_ADMIN_PATCH_LEGAL_NAME))
    572                         }
    573                         PatchResult::NonAdminCashout => {
    574                             Err(failure_code(ErrorCode::BANK_NON_ADMIN_PATCH_CASHOUT))
    575                         }
    576                         PatchResult::NonAdminDebtLimit => {
    577                             Err(failure_code(ErrorCode::BANK_NON_ADMIN_PATCH_DEBT_LIMIT))
    578                         }
    579                         PatchResult::NonAdminConversionRateClass => Err(failure_code(
    580                             ErrorCode::BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS,
    581                         )),
    582                         PatchResult::UnknownConversionClass => {
    583                             Err(failure_code(ErrorCode::BANK_CONVERSION_RATE_CLASS_UNKNOWN))
    584                         }
    585                     }
    586                 },
    587             )
    588             .delete(
    589                 async |State(state): State<Arc<BankState>>, req: MfaReq<AccountDeletionOp>| {
    590                     let (mut auth, _, mfa) = req.solve(&state, &[]).await?;
    591                     if !state.cfg.allow_account_deletion && !auth.is_admin() {
    592                         return Err(require_admin());
    593                     }
    594                     // Not deleting reserved names
    595                     if matches!(auth.username.as_str(), "admin" | "bank") {
    596                         return Err(failure(
    597                             ErrorCode::BANK_RESERVED_USERNAME_CONFLICT,
    598                             "Cannot delete reserved account",
    599                         ));
    600                     } else if auth.username == "exchange" && state.cfg.fiat.is_some() {
    601                         return Err(failure(
    602                             ErrorCode::BANK_RESERVED_USERNAME_CONFLICT,
    603                             "Cannot delete 'exchange' accounts when conversion is enabled",
    604                         ));
    605                     }
    606                     match db::account::delete(
    607                         &state.db,
    608                         &auth.username,
    609                         auth.is_admin() || mfa.is_2fa(),
    610                     )
    611                     .await?
    612                     {
    613                         DeletionResult::Success => Ok(NoContent.into_response()),
    614                         DeletionResult::UnknownAccount => {
    615                             Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT))
    616                         }
    617                         DeletionResult::BalanceNotZero => {
    618                             Err(failure_code(ErrorCode::BANK_ACCOUNT_BALANCE_NOT_ZERO))
    619                         }
    620                         DeletionResult::TanRequired => Ok(mfa
    621                             .response_mfa(&mut auth, &state.db, &state.cfg.ctx)
    622                             .await
    623                             .into_response()),
    624                     }
    625                 },
    626             )
    627             .get(
    628                 async |State(state): State<Arc<BankState>>,
    629                        UserAuth { username, .. }: UserRAuth| {
    630                     match by_username(
    631                         &state.db,
    632                         &state.cfg.ctx,
    633                         &state.cfg.regional_currency,
    634                         state.cfg.fiat_currency(),
    635                         &username,
    636                     )
    637                     .await?
    638                     {
    639                         Some(acc) => Ok(Json(acc)),
    640                         None => Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)),
    641                     }
    642                 },
    643             ),
    644         )
    645         .route(
    646             "/accounts/{username}/auth",
    647             patch(
    648                 async |State(state): State<Arc<BankState>>, req: MfaReq<AccountPasswordOp>| {
    649                     let (mut auth, req, mfa) = req.solve(&state, &[]).await?;
    650                     if !auth.is_admin() && req.old_password.is_none() {
    651                         return Err(failure_code(
    652                             ErrorCode::BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD,
    653                         ));
    654                     }
    655                     checkpw(&req.new_password, state.cfg.pwd_check_quality)?;
    656 
    657                     match reconfig_password(
    658                         &state.db,
    659                         &state.cfg.pw_crypto,
    660                         &auth.username,
    661                         &req.new_password,
    662                         req.old_password.as_deref(),
    663                         auth.is_admin() || mfa.is_2fa(),
    664                     )
    665                     .await?
    666                     {
    667                         PatchAuthResult::UnknownAccount => {
    668                             Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT))
    669                         }
    670                         PatchAuthResult::OldPasswordMismatch => {
    671                             Err(failure_code(ErrorCode::BANK_PATCH_BAD_OLD_PASSWORD))
    672                         }
    673                         PatchAuthResult::TanRequired => Ok(mfa
    674                             .response_mfa(&mut auth, &state.db, &state.cfg.ctx)
    675                             .await?
    676                             .into_response()),
    677                         PatchAuthResult::Success => Ok(NoContent.into_response()),
    678                     }
    679                 },
    680             ),
    681         )
    682 }
    683 
    684 pub fn rand_iban_payto() -> IbanPayto {
    685     let iban = IBAN::random(Country::DE);
    686     IbanPayto::new(BankID { iban, bic: None })
    687 }
    688 
    689 pub async fn create_account(
    690     db: &PgPool,
    691     cfg: &BankCfg,
    692     req: &RegisterAccountRequest,
    693     is_admin: bool,
    694 ) -> ApiResult<CreationResult> {
    695     req.validate()?;
    696 
    697     if matches!(req.username.as_str(), "admin" | "bank") {
    698         return Err(failure_code(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT));
    699     }
    700 
    701     let channels = req.channels();
    702 
    703     if !is_admin {
    704         if req.debit_threshold.is_some() {
    705             return Err(failure_code(ErrorCode::BANK_NON_ADMIN_PATCH_DEBT_LIMIT));
    706         } else if req.conversion_rate_class_id.is_some() {
    707             return Err(failure_code(
    708                 ErrorCode::BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS,
    709             ));
    710         } else if !channels.is_empty() {
    711             return Err(failure_code(ErrorCode::BANK_NON_ADMIN_SET_TAN_CHANNEL));
    712         }
    713     } else {
    714         if let Some(amount) = req.debit_threshold {
    715             cfg.check_regio(&amount)?;
    716         }
    717     }
    718 
    719     for channel in channels {
    720         if !cfg.tan_channels.contains_key(channel) {
    721             return Err(failure(
    722                 ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED,
    723                 format_args!("unsupported tan channel {channel}"),
    724             ));
    725         }
    726 
    727         let info = match channel {
    728             TanChannel::sms => req.contact_data.phone.opt(),
    729             TanChannel::email => req.contact_data.email.opt(),
    730         };
    731 
    732         if info.is_none() {
    733             return Err(failure(
    734                 ErrorCode::BANK_MISSING_TAN_INFO,
    735                 format_args!("missing info for tan channel {channel}"),
    736             ));
    737         }
    738     }
    739 
    740     if req.username == "exchange" && !req.is_taler_exchange {
    741         return Err(failure_status(
    742             ErrorCode::END,
    743             "'exchange' account must be a taler exchange account",
    744             StatusCode::CONFLICT,
    745         ));
    746     }
    747 
    748     checkpw(&req.password, cfg.pwd_check_quality)?;
    749 
    750     let create = async |payto: LibeufinId| {
    751         crate::db::account::create(
    752             db,
    753             &cfg.ctx,
    754             &cfg.pw_crypto,
    755             &req.username,
    756             &req.password,
    757             &req.name,
    758             req.contact_data.email.opt().map(|it| it.as_str()),
    759             req.contact_data.phone.opt().map(|it| it.as_str()),
    760             req.cashout_payto_uri.as_ref(),
    761             &payto,
    762             req.is_public,
    763             req.is_taler_exchange,
    764             req.debit_threshold.unwrap_or(cfg.default_debt_limit),
    765             if req.is_taler_exchange {
    766                 Amount::zero(&cfg.regional_currency)
    767             } else {
    768                 cfg.registration_bonus
    769             },
    770             channels,
    771             req.payto_uri.is_some(),
    772             req.conversion_rate_class_id,
    773         )
    774         .await
    775     };
    776 
    777     match cfg.wire_method {
    778         WireMethod::iban => {
    779             if let Some(payto) = &req.payto_uri {
    780                 let bank_id = payto.expect_iban()?;
    781                 Ok(create(LibeufinId::IBAN(*bank_id)).await?)
    782             } else {
    783                 let mut retry = 5;
    784                 loop {
    785                     let payto = rand_iban_payto();
    786                     let res = create(LibeufinId::IBAN(payto.into_inner())).await?;
    787                     if res == CreationResult::PayToReuse && retry > 0 {
    788                         retry -= 1;
    789                         continue;
    790                     }
    791                     return Ok(res);
    792                 }
    793             }
    794         }
    795         WireMethod::x_taler_bank => {
    796             if let Some(payto) = &req.payto_uri {
    797                 let libeufin_id = payto.expect_xtaler_bank()?;
    798                 if libeufin_id.username != req.username {
    799                     return Err(bad_request(format_args!(
    800                         "Expected a payto uri for '{}' got one for '{}'",
    801                         req.username, libeufin_id.username
    802                     )));
    803                 }
    804             }
    805             Ok(create(LibeufinId::XTalerBank(XTalerBank {
    806                 hostname: cfg.ctx.hostname.clone(),
    807                 username: req.username.clone(),
    808             }))
    809             .await?)
    810         }
    811     }
    812 }
    813 
    814 /**
    815 * This function creates the admin account ONLY IF it was
    816 * NOT found in the database.  It sets it to a random password that
    817 * is only meant to be overridden by a dedicated CLI tool
    818 */
    819 pub async fn create_admin_account(
    820     db: &PgPool,
    821     cfg: &BankCfg,
    822     pw: Option<&str>,
    823 ) -> anyhow::Result<CreationResult> {
    824     let pw = pw
    825         .map(|it| it.to_owned())
    826         .unwrap_or_else(|| Base32::<32>::secure_rand().to_string());
    827 
    828     let payto = match cfg.wire_method {
    829         WireMethod::iban => LibeufinId::IBAN(rand_iban_payto().into_inner()),
    830         WireMethod::x_taler_bank => LibeufinId::XTalerBank(XTalerBank {
    831             hostname: cfg.ctx.hostname.clone(),
    832             username: CompactString::const_new("admin"),
    833         }),
    834     };
    835 
    836     Ok(crate::db::account::create(
    837         db,
    838         &cfg.ctx,
    839         &cfg.pw_crypto,
    840         "admin",
    841         &pw,
    842         "Bank administrator",
    843         None,
    844         None,
    845         None,
    846         &payto,
    847         false,
    848         false,
    849         cfg.default_debt_limit,
    850         Amount::zero(&cfg.regional_currency),
    851         &[],
    852         false,
    853         None,
    854     )
    855     .await?)
    856 }
    857 
    858 pub async fn patch_account(
    859     db: &PgPool,
    860     cfg: &BankCfg,
    861     req: &AccountReconfiguration,
    862     username: &str,
    863     is_admin: bool,
    864     is2fa: bool,
    865 ) -> ApiResult<PatchResult> {
    866     if let Some(amount) = req.debit_threshold {
    867         cfg.check_regio(&amount)?;
    868     }
    869 
    870     if username == "admin" && req.is_public == Some(true) {
    871         return Err(failure_status(
    872             ErrorCode::END,
    873             "'admin' account cannot be public",
    874             StatusCode::CONFLICT,
    875         ));
    876     }
    877 
    878     if username == "exchange" && req.is_taler_exchange == Some(false) {
    879         return Err(failure_status(
    880             ErrorCode::END,
    881             "'exchange' account must be a taler exchange account",
    882             StatusCode::CONFLICT,
    883         ));
    884     }
    885 
    886     if let Some(channels) = req.channels() {
    887         for channel in channels {
    888             if !cfg.tan_channels.contains_key(channel) {
    889                 return Err(failure(
    890                     ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED,
    891                     format_args!("unsupported tan channel {channel}"),
    892                 ));
    893             }
    894         }
    895     }
    896 
    897     Ok(reconfig(
    898         db,
    899         &cfg.regional_currency,
    900         username,
    901         req,
    902         is_admin,
    903         is2fa,
    904         cfg.allow_edit_name,
    905         cfg.allow_edit_cashout,
    906     )
    907     .await?)
    908 }
    909 
    910 #[cfg(test)]
    911 pub mod test {
    912 
    913     use axum::http::{Method, StatusCode};
    914     use jiff::Timestamp;
    915     use serde::Serialize;
    916     use sqlx::postgres::PgConnectOptions;
    917     use taler_common::{
    918         error_code::ErrorCode,
    919         types::{amount::amount, payto::PaytoImpl},
    920     };
    921     use taler_macros::db_test;
    922     use taler_test_utils::{json, server::TestServer as _};
    923 
    924     use crate::{
    925         TanChannel,
    926         api::{
    927             account::{
    928                 AccountData, AccountStatus, Balance, ChallengeContactData, CreditDebitInfo,
    929                 ListBankAccountsResponse, Maybe, PublicAccountsResponse, RegisterAccountResponse,
    930                 rand_iban_payto,
    931             },
    932             conversion::ConversionRateClass,
    933             test::{Auth, BankTestCtx, MfaRequest, bank_setup, bank_setup_conf},
    934         },
    935         db::gc::collect,
    936         payto::FullBankPayto,
    937     };
    938 
    939     #[db_test(raw)]
    940     async fn create(db: PgConnectOptions) {
    941         let mut ctx = bank_setup(db).await;
    942 
    943         let admin_only = async |body: serde_json::Value, error: ErrorCode| {
    944             // Check restricted
    945             ctx.post("/accounts").json(&body).await.assert_error(error);
    946             ctx.post_admin("/accounts").json(&body).await.assert_ok();
    947         };
    948         ctx.auth_routine(Method::GET, "/accounts/merchant", Auth::UserOrAdmin)
    949             .await;
    950 
    951         // Check generated payto
    952         {
    953             let body = json!({
    954                 "username": "john",
    955                 "password": "password",
    956                 "name": "John"
    957             });
    958             // Check ok
    959             let payto = ctx
    960                 .post("/accounts")
    961                 .json(&body)
    962                 .await
    963                 .assert_ok_json::<RegisterAccountResponse>()
    964                 .internal_payto_uri;
    965             // Check idempotency
    966             assert_eq!(
    967                 payto,
    968                 ctx.post("/accounts")
    969                     .json(&body)
    970                     .await
    971                     .assert_ok_json::<RegisterAccountResponse>()
    972                     .internal_payto_uri
    973             );
    974             // Check idempotency with payto
    975             ctx.post("/accounts")
    976                 .json(&json!(body + {
    977                     "payto_uri": payto
    978                 }))
    979                 .await
    980                 .assert_ok_json::<RegisterAccountResponse>();
    981             // Check payto conflict
    982             ctx.post("/accounts")
    983                 .json(&json!(body + {
    984                     "payto_uri": rand_iban_payto()
    985                 }))
    986                 .await
    987                 .assert_error(ErrorCode::BANK_REGISTER_USERNAME_REUSE);
    988         }
    989 
    990         let name = "Jane";
    991         let payto = rand_iban_payto().full(name);
    992         let req = json!({
    993             "username": "foo",
    994             "password": "password",
    995             "name": name,
    996             "is_public": true,
    997             "payto_uri": &payto,
    998             "is_taler_exchange": true
    999         });
   1000         // Check given payto
   1001         {
   1002             let full: FullBankPayto = payto.clone().convert();
   1003             // Check ok
   1004             assert_eq!(
   1005                 full,
   1006                 ctx.post("/accounts")
   1007                     .json(&req)
   1008                     .await
   1009                     .assert_ok_json::<RegisterAccountResponse>()
   1010                     .internal_payto_uri
   1011             );
   1012             // Check idempotency
   1013             assert_eq!(
   1014                 full,
   1015                 ctx.post("/accounts")
   1016                     .json(&req)
   1017                     .await
   1018                     .assert_ok_json::<RegisterAccountResponse>()
   1019                     .internal_payto_uri
   1020             );
   1021         }
   1022 
   1023         // Check admin only debit_threshold
   1024         admin_only(
   1025             json!({
   1026                 "username": "bat",
   1027                 "password": "password",
   1028                 "name": "Bat",
   1029                 "debit_threshold": "KUDOS:42"
   1030             }),
   1031             ErrorCode::BANK_NON_ADMIN_PATCH_DEBT_LIMIT,
   1032         )
   1033         .await;
   1034 
   1035         // Check admin only conversion_rate_class_id
   1036         let conv_class_id = ctx.create_conversion_rate_class().await;
   1037         admin_only(
   1038             json!({
   1039                 "username": "bat2",
   1040                 "password": "password",
   1041                 "name": "Bat",
   1042                 "conversion_rate_class_id": conv_class_id
   1043             }),
   1044             ErrorCode::BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS,
   1045         )
   1046         .await;
   1047 
   1048         // Check admin only tan_channel
   1049         admin_only(
   1050             json!({
   1051                 "username": "bat3",
   1052                 "password": "password",
   1053                 "name": "Bat",
   1054                 "contact_data" : {
   1055                     "phone" : "+456"
   1056                 },
   1057                 "tan_channel": "sms"
   1058             }),
   1059             ErrorCode::BANK_NON_ADMIN_SET_TAN_CHANNEL,
   1060         )
   1061         .await;
   1062 
   1063         // Check both tan channels
   1064         ctx.post("/accounts")
   1065             .json(json!({
   1066                 "username": "bat3",
   1067                 "password": "password",
   1068                 "name": "Bat",
   1069                 "tan_channel": "sms",
   1070                 "tan_channels": []
   1071             }))
   1072             .await
   1073             .assert_bad_request();
   1074 
   1075         // Check tan info
   1076         for channel in TanChannel::entries {
   1077             ctx.post_admin("/accounts")
   1078                 .json(json!({
   1079                     "username": "bat",
   1080                     "password": "password",
   1081                     "name": "Bat",
   1082                     "tan_channel": channel
   1083                 }))
   1084                 .await
   1085                 .assert_error(ErrorCode::BANK_MISSING_TAN_INFO);
   1086             ctx.post_admin("/accounts")
   1087                 .json(json!({
   1088                     "username": "bat",
   1089                     "password": "password",
   1090                     "name": "Bat",
   1091                     "tan_channels": [channel]
   1092                 }))
   1093                 .await
   1094                 .assert_error(ErrorCode::BANK_MISSING_TAN_INFO);
   1095         }
   1096         ctx.post_admin("/accounts")
   1097             .json(json!({
   1098                 "username": "bat",
   1099                 "password": "password",
   1100                 "name": "Bat",
   1101                 "tan_channels": TanChannel::entries
   1102             }))
   1103             .await
   1104             .assert_error(ErrorCode::BANK_MISSING_TAN_INFO);
   1105 
   1106         // Check unknown conversion rate class
   1107         ctx.post_admin("/accounts")
   1108             .json(json!({
   1109                 "username": "bat4",
   1110                 "password": "password",
   1111                 "name": "Bat",
   1112                 "conversion_rate_class_id": 42
   1113             }))
   1114             .await
   1115             .assert_error(ErrorCode::BANK_CONVERSION_RATE_CLASS_UNKNOWN);
   1116 
   1117         for username in ["admin", "bank"] {
   1118             ctx.post("/accounts")
   1119                 .json(json!({
   1120                     "username": username,
   1121                     "password": "password",
   1122                     "name": "John Smith"
   1123                 }))
   1124                 .await
   1125                 .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT);
   1126         }
   1127 
   1128         for username in [
   1129             "bad@username",
   1130             "bad/username",
   1131             " spaces ",
   1132             &"long".repeat(40),
   1133         ] {
   1134             ctx.post("/accounts")
   1135                 .json(json!({
   1136                     "username": username,
   1137                     "password": "password",
   1138                     "name": "John Smith"
   1139                 }))
   1140                 .await
   1141                 .assert_bad_request();
   1142         }
   1143 
   1144         // Non exchange account
   1145         ctx.post("/accounts")
   1146             .json(json!({
   1147                 "username": "exchange",
   1148                 "password": "password",
   1149                 "name": "Exchange"
   1150             }))
   1151             .await
   1152             .assert_error_status(ErrorCode::END, StatusCode::CONFLICT);
   1153 
   1154         // Username conflict
   1155         ctx.post("/accounts")
   1156             .json(json!(req + { "name" : "Foo" }))
   1157             .await
   1158             .assert_error(ErrorCode::BANK_REGISTER_USERNAME_REUSE);
   1159         // Payto conflict
   1160         ctx.post("/accounts")
   1161             .json(json!(req + { "username" : "bar" }))
   1162             .await
   1163             .assert_error(ErrorCode::BANK_REGISTER_PAYTO_URI_REUSE);
   1164         ctx.get_admin("/accounts/bar")
   1165             .await
   1166             .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT);
   1167         // Bad payto kind
   1168         ctx.post("/accounts")
   1169             .json(json!(req +{
   1170                 "payto_uri": "payto://x-taler-bank/bank.hostname.test/bar"
   1171             }))
   1172             .await
   1173             .assert_bad_request();
   1174         // Short password
   1175         ctx.post("/accounts")
   1176             .json(json!(req + { "password" : "short" }))
   1177             .await
   1178             .assert_error(ErrorCode::BANK_PASSWORD_TOO_SHORT);
   1179         // Long password
   1180         ctx.post("/accounts")
   1181         .json(json!(req +{
   1182             "password": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password"
   1183         }))
   1184         .await
   1185         .assert_error(ErrorCode::BANK_PASSWORD_TOO_LONG);
   1186         // Check currency
   1187         ctx.post_admin("/accounts")
   1188             .json(json!(req + { "debit_threshold": "EUR:100" }))
   1189             .await
   1190             .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH);
   1191 
   1192         // Check cashout payto receiver name logic
   1193         ctx.post("/accounts")
   1194             .json(json!({
   1195                 "username": "cashout_guess",
   1196                 "password": "cashout_guess-password",
   1197                 "name": "Mr Guess My Name",
   1198                 "cashout_payto_uri": &payto
   1199             }))
   1200             .await
   1201             .assert_ok();
   1202         ctx.post("/accounts")
   1203             .json(json!({
   1204                 "username": "cashout_keep",
   1205                 "password": "cashout_keep-password",
   1206                 "name": "Mr Keep My Name",
   1207                 "cashout_payto_uri": payto.as_full_uri("Santa Clauss")
   1208             }))
   1209             .await
   1210             .assert_ok();
   1211         ctx.cache_tokens(&["cashout_guess", "cashout_keep"]).await;
   1212         let acc: AccountData = ctx.geta("/accounts/cashout_guess").await.assert_ok_json();
   1213         assert_eq!(
   1214             acc.cashout_payto_uri.unwrap(),
   1215             payto.as_full_uri("Mr Guess My Name")
   1216         );
   1217         let acc: AccountData = ctx.geta("/accounts/cashout_keep").await.assert_ok_json();
   1218         assert_eq!(
   1219             acc.cashout_payto_uri.unwrap(),
   1220             payto.as_full_uri("Mr Keep My Name")
   1221         );
   1222 
   1223         // Check input restriction
   1224         {
   1225             let req = json!({
   1226                 "username": "username",
   1227                 "password": "password",
   1228                 "name": "Name"
   1229             });
   1230             ctx.post("/accounts")
   1231                 .json(json!(req + {
   1232                     "contact_data": { "phone" : " +456" }
   1233                 }))
   1234                 .await
   1235                 .assert_bad_request();
   1236             ctx.post("/accounts")
   1237                 .json(json!(req + {
   1238                     "contact_data": { "phone" : "test@gmail.com" }
   1239                 }))
   1240                 .await
   1241                 .assert_bad_request();
   1242         }
   1243 
   1244         // Create -> get
   1245         ctx.post("/accounts")
   1246             .json(json!({
   1247                 "username": "minimal",
   1248                 "password": "minimal-password",
   1249                 "name": "John Smith",
   1250             }))
   1251             .await
   1252             .assert_ok();
   1253         let new = rand_iban_payto();
   1254         ctx.post_admin("/accounts")
   1255             .json(json!({
   1256                 "username": "maximal",
   1257                 "password": "maximal-password",
   1258                 "name": "John Smith",
   1259                 "is_public": true,
   1260                 "is_taler_exchange": true,
   1261                 "contact_data": {
   1262                     "phone": "+4567",
   1263                     "email": "test@gmail.com"
   1264                 },
   1265                 "cashout_payto_uri": payto,
   1266                 "payto_uri": new,
   1267                 "debit_threshold": "KUDOS:12",
   1268                 "tan_channels": ["sms", "email"],
   1269                 "conversion_rate_class_id": conv_class_id
   1270             }))
   1271             .await
   1272             .assert_ok();
   1273         ctx.cache_tokens(&["minimal", "maximal"]).await;
   1274         let acc: AccountData = ctx.geta("/accounts/minimal").await.assert_ok_json();
   1275         pretty_assertions::assert_eq!(
   1276             AccountData {
   1277                 name: "John Smith".into(),
   1278                 balance: Balance {
   1279                     amount: amount("KUDOS:0"),
   1280                     credit_debit_indicator: CreditDebitInfo::credit,
   1281                 },
   1282                 cashout_payto_uri: None,
   1283                 contact_data: ChallengeContactData {
   1284                     phone: Maybe::Null,
   1285                     email: Maybe::Null,
   1286                 },
   1287                 conversion_rate: Some(acc.conversion_rate.clone().unwrap()),
   1288                 conversion_rate_class_id: None,
   1289                 debit_threshold: acc.debit_threshold,
   1290                 is_locked: false,
   1291                 is_public: false,
   1292                 is_taler_exchange: false,
   1293                 payto_uri: acc.payto_uri.clone(),
   1294                 status: AccountStatus::active,
   1295                 tan_channel: None,
   1296                 tan_channels: Vec::new(),
   1297             },
   1298             acc
   1299         );
   1300         let acc: AccountData = ctx.geta("/accounts/maximal").await.assert_ok_json();
   1301         pretty_assertions::assert_eq!(
   1302             AccountData {
   1303                 name: "John Smith".into(),
   1304                 balance: Balance {
   1305                     amount: amount("KUDOS:0"),
   1306                     credit_debit_indicator: CreditDebitInfo::credit,
   1307                 },
   1308                 cashout_payto_uri: Some(payto.as_full_uri("John Smith")),
   1309                 contact_data: ChallengeContactData {
   1310                     phone: Maybe::Some("+4567".into()),
   1311                     email: Maybe::Some("test@gmail.com".into())
   1312                 },
   1313                 conversion_rate: Some(acc.conversion_rate.clone().unwrap()),
   1314                 conversion_rate_class_id: Some(conv_class_id),
   1315                 debit_threshold: acc.debit_threshold,
   1316                 is_locked: false,
   1317                 is_public: true,
   1318                 is_taler_exchange: true,
   1319                 payto_uri: new.full("John Smith").convert(),
   1320                 status: AccountStatus::active,
   1321                 tan_channel: Some(TanChannel::sms),
   1322                 tan_channels: vec![TanChannel::sms, TanChannel::email]
   1323             },
   1324             acc
   1325         );
   1326 
   1327         let ctx = ctx.swap_cfg("test_bonus.conf").await;
   1328         // Create bonus
   1329         {
   1330             let req = json!({
   1331                 "password": "password",
   1332                 "name": "Mallory"
   1333             });
   1334 
   1335             ctx.set_max_debt("admin", "1000").await;
   1336 
   1337             // Check OK
   1338             for i in 0..10 {
   1339                 let username = format!("foo{i}");
   1340                 ctx.post_admin("/accounts")
   1341                     .json(json!(req + { "username": username }))
   1342                     .await
   1343                     .assert_ok();
   1344                 ctx.assert_balance(&username, "100").await;
   1345             }
   1346             ctx.assert_balance("admin", "-1000").await;
   1347 
   1348             // Check insufficient fund
   1349             ctx.post_admin("/accounts")
   1350                 .json(json!(req + { "username": "bar" }))
   1351                 .await
   1352                 .assert_error(ErrorCode::BANK_UNALLOWED_DEBIT);
   1353             ctx.get_admin("/accounts/bar")
   1354                 .await
   1355                 .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT);
   1356         }
   1357 
   1358         // Restricted account creation
   1359         let ctx = ctx.swap_cfg("test_restrict.conf").await;
   1360         ctx.auth_routine(Method::POST, "/accounts", Auth::UserOrAdmin)
   1361             .await;
   1362         ctx.post_admin("/accounts")
   1363             .json(json!({
   1364                 "username": "foobar",
   1365                 "password": "password-xyz",
   1366                 "name": "Mallory"
   1367             }))
   1368             .await
   1369             .assert_ok();
   1370 
   1371         // Unsupported tan
   1372         let ctx = ctx.swap_cfg("test_tan_err.conf").await;
   1373         ctx.post_admin("/accounts")
   1374             .json(json!({
   1375                 "username": "foo",
   1376                 "password": "password-xyz",
   1377                 "name": "Mallory",
   1378                 "tan_channel": "email"
   1379             }))
   1380             .await
   1381             .assert_error(ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED);
   1382 
   1383         // No password check
   1384         let ctx = ctx.swap_cfg("test_no_password_check.conf").await;
   1385         // Short password
   1386         ctx.post("/accounts")
   1387             .json(json!({
   1388                 "username": "short",
   1389                 "password": "short",
   1390                 "name": "John Smith",
   1391             }))
   1392             .await
   1393             .assert_ok();
   1394         // Long password
   1395         ctx.post("/accounts")
   1396             .json(json!({
   1397                 "username": "long",
   1398                 "password": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password",
   1399                 "name": "John Smith"
   1400             }))
   1401             .await
   1402             .assert_ok();
   1403     }
   1404 
   1405     #[db_test(raw)]
   1406     async fn delete(db: PgConnectOptions) {
   1407         let mut ctx = bank_setup(db).await;
   1408         ctx.auth_routine(Method::DELETE, "/accounts/merchant", Auth::UserOrAdmin)
   1409             .await;
   1410 
   1411         // Reserved accounts
   1412         for username in ["admin", "bank"] {
   1413             ctx.delete_admin(format!("/accounts/{username}"))
   1414                 .await
   1415                 .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT);
   1416         }
   1417         ctx.deletea("/accounts/exchange")
   1418             .await
   1419             .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT);
   1420 
   1421         let payto = ctx.tmp_payto().await;
   1422         ctx.post("/accounts")
   1423             .json(json!({
   1424                 "username": "john",
   1425                 "password": "john-password",
   1426                 "name": "John",
   1427                 "payto_uri": payto
   1428             }))
   1429             .await
   1430             .assert_ok();
   1431         ctx.cache_tokens(&["john"]).await;
   1432         ctx.fill_tan_info("john").await;
   1433         // Fail to delete, due to a non-zero balance.
   1434         ctx.tx("customer", "1", "john").await;
   1435         ctx.deletea("/accounts/john")
   1436             .await
   1437             .assert_error(ErrorCode::BANK_ACCOUNT_BALANCE_NOT_ZERO);
   1438         // Successful deletion
   1439         ctx.tx("john", "1", "customer").await;
   1440         ctx.deletea("/accounts/john")
   1441             .await
   1442             .assert_challenge(&ctx)
   1443             .await
   1444             .assert_no_content();
   1445         // Account no longer exists
   1446         ctx.deletea("/accounts/john")
   1447             .await
   1448             .assert_error(ErrorCode::GENERIC_TOKEN_UNKNOWN);
   1449         ctx.delete_admin("/accounts/john")
   1450             .await
   1451             .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT);
   1452 
   1453         // GC
   1454         {
   1455             let now = Timestamp::now();
   1456             collect(&ctx.state.db, &now, &now, &now).await.unwrap();
   1457             // TODO need more operations
   1458         }
   1459 
   1460         // Test admin-only account deletion
   1461         let ctx = ctx.swap_cfg("test_restrict.conf").await;
   1462         ctx.auth_routine(Method::DELETE, "/accounts/merchant", Auth::Admin)
   1463             .await;
   1464         ctx.delete_admin("/accounts/merchant")
   1465             .await
   1466             .assert_no_content();
   1467 
   1468         // Exchange is still restricted
   1469         ctx.delete_admin("/accounts/exchange")
   1470             .await
   1471             .assert_error(ErrorCode::BANK_RESERVED_USERNAME_CONFLICT);
   1472 
   1473         // Test 2FA account deletion
   1474         ctx.fill_tan_info("customer").await;
   1475         ctx.delete_admin("/accounts/customer")
   1476             .await
   1477             .assert_no_content();
   1478 
   1479         // Test delete exchange account
   1480         let ctx = ctx.swap_cfg("test_no_conversion.conf").await;
   1481         // Exchange is no longer restricted
   1482         ctx.deletea("/accounts/exchange").await.assert_no_content();
   1483     }
   1484 
   1485     async fn check_admin_only(ctx: &BankTestCtx, req: impl Serialize, error: ErrorCode) {
   1486         // Check restricted
   1487         ctx.patcha("/accounts/merchant")
   1488             .json(&req)
   1489             .await
   1490             .assert_error(error);
   1491         // Check admin always can
   1492         ctx.patch_admin("/accounts/merchant")
   1493             .json(&req)
   1494             .await
   1495             .assert_no_content();
   1496         // Check idempotent
   1497         ctx.patch_admin("/accounts/merchant")
   1498             .json(&req)
   1499             .await
   1500             .assert_no_content();
   1501     }
   1502 
   1503     #[db_test(raw)]
   1504     async fn reconfig(db: PgConnectOptions) {
   1505         let mut ctx = bank_setup(db).await;
   1506 
   1507         ctx.auth_routine(Method::PATCH, "/accounts/merchant", Auth::UserOrAdmin)
   1508             .await;
   1509 
   1510         for channel in TanChannel::entries {
   1511             ctx.patcha("/accounts/merchant")
   1512                 .json(json!({ "tan_channel": channel }))
   1513                 .await
   1514                 .assert_error(ErrorCode::BANK_MISSING_TAN_INFO);
   1515             ctx.patcha("/accounts/merchant")
   1516                 .json(json!({ "tan_channels": [channel] }))
   1517                 .await
   1518                 .assert_error(ErrorCode::BANK_MISSING_TAN_INFO);
   1519         }
   1520         ctx.patcha("/accounts/merchant")
   1521             .json(json!({ "tan_channels": TanChannel::entries }))
   1522             .await
   1523             .assert_error(ErrorCode::BANK_MISSING_TAN_INFO);
   1524 
   1525         // Successful attempt now
   1526         let cashout = rand_iban_payto();
   1527         let req = json!({
   1528             "cashout_payto_uri": cashout,
   1529             "name": "Roger",
   1530             "is_public": true,
   1531             "is_taler_exchange": true,
   1532             "contact_data": {
   1533                 "phone": "+99",
   1534                 "email": "foo@example.com"
   1535             }
   1536         });
   1537         ctx.patcha("/accounts/merchant")
   1538             .json(&req)
   1539             .await
   1540             .assert_no_content();
   1541         // Checking idempotent
   1542         ctx.patcha("/accounts/merchant")
   1543             .json(&req)
   1544             .await
   1545             .assert_no_content();
   1546 
   1547         // Check admin only
   1548         check_admin_only(
   1549             &ctx,
   1550             json!(req + { "debit_threshold": "KUDOS:100" }),
   1551             ErrorCode::BANK_NON_ADMIN_PATCH_DEBT_LIMIT,
   1552         )
   1553         .await;
   1554         check_admin_only(
   1555             &ctx,
   1556             json!(req + { "conversion_rate_class_id": 1 }),
   1557             ErrorCode::BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS,
   1558         )
   1559         .await;
   1560 
   1561         // Check unknown conversion rate class
   1562         ctx.patch_admin("/accounts/merchant")
   1563             .json(json!(req + { "conversion_rate_class_id": 42 }))
   1564             .await
   1565             .assert_error(ErrorCode::BANK_CONVERSION_RATE_CLASS_UNKNOWN);
   1566 
   1567         // Check currency
   1568         ctx.patch_admin("/accounts/merchant")
   1569             .json(json!(req + { "debit_threshold": "EUR:100" }))
   1570             .await
   1571             .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH);
   1572 
   1573         // Check patch
   1574         let acc: AccountData = ctx.geta("/accounts/merchant").await.assert_ok_json();
   1575         let expected = AccountData {
   1576             name: "Roger".into(),
   1577             balance: Balance {
   1578                 amount: amount("KUDOS:0"),
   1579                 credit_debit_indicator: CreditDebitInfo::credit,
   1580             },
   1581             cashout_payto_uri: Some(acc.cashout_payto_uri.clone().unwrap()),
   1582             contact_data: ChallengeContactData {
   1583                 phone: Maybe::Some("+99".into()),
   1584                 email: Maybe::Some("foo@example.com".into()),
   1585             },
   1586             conversion_rate: Some(acc.conversion_rate.clone().unwrap()),
   1587             conversion_rate_class_id: Some(1),
   1588             debit_threshold: acc.debit_threshold,
   1589             is_locked: false,
   1590             is_public: true,
   1591             is_taler_exchange: true,
   1592             payto_uri: ctx.merchant_payto.clone().into_inner().full("Roger"),
   1593             status: AccountStatus::active,
   1594             tan_channel: None,
   1595             tan_channels: Vec::new(),
   1596         };
   1597         pretty_assertions::assert_eq!(acc, expected);
   1598 
   1599         // Check keep values when there is no changes
   1600         ctx.patcha("/accounts/merchant")
   1601             .json(json!({}))
   1602             .await
   1603             .assert_no_content();
   1604         pretty_assertions::assert_eq!(
   1605             ctx.geta("/accounts/merchant")
   1606                 .await
   1607                 .assert_ok_json::<AccountData>(),
   1608             expected
   1609         );
   1610 
   1611         // Admin cannot be public
   1612         ctx.patcha("/accounts/admin")
   1613             .json(json!({
   1614                 "is_public": true
   1615             }))
   1616             .await
   1617             .assert_error_status(ErrorCode::END, StatusCode::CONFLICT);
   1618 
   1619         // Exchange must be exchange
   1620         ctx.patcha("/accounts/exchange")
   1621             .json(json!({
   1622                 "is_taler_exchange": false
   1623             }))
   1624             .await
   1625             .assert_error_status(ErrorCode::END, StatusCode::CONFLICT);
   1626 
   1627         // Check cashout payto receiver name logic
   1628         ctx.post("/accounts")
   1629             .json(json!({
   1630                 "username": "cashout",
   1631                 "password": "cashout-password",
   1632                 "name": "Mr Cashout Cashout"
   1633             }))
   1634             .await
   1635             .assert_ok();
   1636         ctx.cache_tokens(&["cashout"]).await;
   1637 
   1638         for (cashout, name, expect) in [
   1639             (
   1640                 cashout.as_uri(),
   1641                 None,
   1642                 cashout.as_full_uri("Mr Cashout Cashout"),
   1643             ),
   1644             (
   1645                 cashout.as_uri(),
   1646                 Some("New name"),
   1647                 cashout.as_full_uri("New name"),
   1648             ),
   1649             (
   1650                 cashout.as_full_uri("Full name"),
   1651                 None,
   1652                 cashout.as_full_uri("New name"),
   1653             ),
   1654             (
   1655                 cashout.as_full_uri("Full second name"),
   1656                 Some("Another name"),
   1657                 cashout.as_full_uri("Another name"),
   1658             ),
   1659         ] {
   1660             let mut v = json!({
   1661                 "cashout_payto_uri": cashout,
   1662             });
   1663             if let Some(name) = name {
   1664                 v["name"] = name.into();
   1665             }
   1666             ctx.patch_admin("/accounts/cashout")
   1667                 .json(v)
   1668                 .await
   1669                 .assert_no_content();
   1670             let acc: AccountData = ctx.geta("/accounts/cashout").await.assert_ok_json();
   1671             assert_eq!(expect, acc.cashout_payto_uri.unwrap());
   1672         }
   1673 
   1674         // Check 2FA
   1675         ctx.fill_tan_info("customer").await;
   1676         ctx.patcha("/accounts/customer")
   1677             .json(json!({ "is_public": true }))
   1678             .await
   1679             .assert_challenge_check(&ctx, async |_| {
   1680                 let acc: AccountData = ctx.geta("/accounts/customer").await.assert_ok_json();
   1681                 assert!(!acc.is_public);
   1682             })
   1683             .await
   1684             .assert_no_content();
   1685 
   1686         let acc: AccountData = ctx.geta("/accounts/customer").await.assert_ok_json();
   1687         assert!(acc.is_public);
   1688 
   1689         // Restriction
   1690         let ctx = ctx.swap_cfg("test_restrict.conf").await;
   1691         {
   1692             check_admin_only(
   1693                 &ctx,
   1694                 json!({ "name": "Another Foo" }),
   1695                 ErrorCode::BANK_NON_ADMIN_PATCH_LEGAL_NAME,
   1696             )
   1697             .await;
   1698             check_admin_only(
   1699                 &ctx,
   1700                 json!({ "cashout_payto_uri": rand_iban_payto() }),
   1701                 ErrorCode::BANK_NON_ADMIN_PATCH_CASHOUT,
   1702             )
   1703             .await;
   1704 
   1705             // Check idempotent
   1706             let acc: AccountData = ctx.geta("/accounts/merchant").await.assert_ok_json();
   1707             ctx.patcha("/accounts/merchant")
   1708                 .json(json!({
   1709                     "name": acc.name,
   1710                     "cashout_payto_uri": acc.cashout_payto_uri,
   1711                     "debit_threshold": acc.debit_threshold
   1712                 }))
   1713                 .await
   1714                 .assert_no_content();
   1715         }
   1716 
   1717         // Unsupported tan
   1718         let ctx = ctx.swap_cfg("test_tan_err.conf").await;
   1719         ctx.patcha("/accounts/customer")
   1720             .json(json!({
   1721                 "tan_channel": "email"
   1722             }))
   1723             .await
   1724             .assert_error(ErrorCode::BANK_TAN_CHANNEL_NOT_SUPPORTED);
   1725     }
   1726 
   1727     #[db_test(raw)]
   1728     async fn password(db: PgConnectOptions) {
   1729         let ctx = bank_setup(db).await;
   1730         ctx.auth_routine(Method::PATCH, "/accounts/merchant/auth", Auth::UserOrAdmin)
   1731             .await;
   1732 
   1733         // Changing the password
   1734         ctx.patcha("/accounts/customer/auth")
   1735             .json(json!({
   1736                 "old_password": "customer-password",
   1737                 "new_password": "new-password"
   1738             }))
   1739             .await
   1740             .assert_no_content();
   1741         // Previous password should fail.
   1742         ctx.post("/accounts/customer/token")
   1743             .basic_auth("customer", "customer-password")
   1744             .await
   1745             .assert_status(StatusCode::UNAUTHORIZED);
   1746         // New password should succeed
   1747         ctx.post("/accounts/customer/token")
   1748             .basic_auth("customer", "new-password")
   1749             .json(json!({ "scope": "readonly" }))
   1750             .await
   1751             .assert_ok();
   1752         ctx.patcha("/accounts/customer/auth")
   1753             .json(json!({
   1754                 "old_password": "new-password",
   1755                 "new_password": "customer-password"
   1756             }))
   1757             .await
   1758             .assert_no_content();
   1759 
   1760         // Check require test old password
   1761         ctx.patcha("/accounts/customer/auth")
   1762             .json(json!({
   1763                 "old_password": "bad-password",
   1764                 "new_password": "new-password"
   1765             }))
   1766             .await
   1767             .assert_error(ErrorCode::BANK_PATCH_BAD_OLD_PASSWORD);
   1768         // Check require old password for user
   1769         ctx.patcha("/accounts/customer/auth")
   1770             .json(json!({
   1771                 "new_password": "new-password"
   1772             }))
   1773             .await
   1774             .assert_error(ErrorCode::BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD);
   1775         // Testing short password
   1776         ctx.patcha("/accounts/customer/auth")
   1777             .json(json!({
   1778                 "old_password": "ignored",
   1779                 "new_password": "short"
   1780             }))
   1781             .await
   1782             .assert_error(ErrorCode::BANK_PASSWORD_TOO_SHORT);
   1783         // Testing long password
   1784         ctx.patcha("/accounts/customer/auth")
   1785             .json(json!({
   1786                 "old_password": "ignored",
   1787                 "new_password": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password"
   1788             }))
   1789             .await
   1790             .assert_error(ErrorCode::BANK_PASSWORD_TOO_LONG);
   1791 
   1792         // Check admin
   1793         ctx.patch_admin("/accounts/customer/auth")
   1794             .json(json!({
   1795                 "new_password": "customer-password"
   1796             }))
   1797             .await
   1798             .assert_no_content();
   1799 
   1800         // Check 2FA
   1801         ctx.fill_tan_info("customer").await;
   1802         ctx.patcha("/accounts/customer/auth")
   1803             .json(json!({
   1804                 "old_password": "customer-password",
   1805                 "new_password": "new-password"
   1806             }))
   1807             .await
   1808             .assert_challenge(&ctx)
   1809             .await
   1810             .assert_no_content();
   1811         ctx.patch_admin("/accounts/customer/auth")
   1812             .json(json!({
   1813                 "new_password": "customer-password"
   1814             }))
   1815             .await
   1816             .assert_no_content();
   1817 
   1818         // Check 2FA after password check
   1819         ctx.patcha("/accounts/customer/auth")
   1820             .json(json!({
   1821                 "old_password": "bad-password",
   1822                 "new_password": "new-password"
   1823             }))
   1824             .await
   1825             .assert_error(ErrorCode::BANK_PATCH_BAD_OLD_PASSWORD);
   1826 
   1827         // No password check
   1828         let ctx = ctx.swap_cfg("test_no_password_check.conf").await;
   1829         ctx.patcha("/accounts/merchant/auth")
   1830             .json(json!({
   1831                 "old_password": "merchant-password",
   1832                 "new_password": "short"
   1833             }))
   1834             .await
   1835             .assert_no_content();
   1836         // Testing long password
   1837         ctx.patcha("/accounts/merchant/auth")
   1838             .json(json!({
   1839                 "old_password": "short",
   1840                 "new_password": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-password"
   1841             }))
   1842             .await
   1843             .assert_no_content();
   1844     }
   1845 
   1846     #[db_test(raw)]
   1847     async fn list(db: PgConnectOptions) {
   1848         let ctx = bank_setup_conf(db, "test_no_conversion.conf").await;
   1849         ctx.auth_routine(Method::GET, "/accounts", Auth::Admin)
   1850             .await;
   1851 
   1852         // Remove default accounts
   1853         let accounts = ["merchant", "exchange", "customer"];
   1854         for username in accounts {
   1855             ctx.delete_admin(format!("/accounts/{username}"))
   1856                 .await
   1857                 .assert_no_content();
   1858         }
   1859         let req: ListBankAccountsResponse = ctx.get_admin("/accounts").await.assert_ok_json();
   1860         for account in req.accounts {
   1861             assert_eq!(account.conversion_rate, None);
   1862             if accounts.contains(&account.username.as_str()) {
   1863                 assert_eq!(account.status, AccountStatus::deleted);
   1864             } else {
   1865                 assert_eq!(account.status, AccountStatus::active);
   1866             }
   1867         }
   1868 
   1869         // Hard delete accounts
   1870         let now = Timestamp::now();
   1871         collect(&ctx.state.db, &now, &now, &now).await.unwrap();
   1872         ctx.get("/public-accounts").await.assert_no_content();
   1873         ctx.get_admin("/accounts").await.assert_ok();
   1874 
   1875         let ctx = ctx.swap_cfg("test.conf").await;
   1876 
   1877         for _ in 0..3 {
   1878             ctx.create_conversion_rate_class().await;
   1879         }
   1880 
   1881         // Gen some public and private accounts
   1882         for i in 0..5 {
   1883             let m = i % 3;
   1884             ctx.post_admin("/accounts")
   1885                 .json(json!({
   1886                     "username": format!("{i}"),
   1887                     "password": "password",
   1888                     "name": format!("Mr 1{i}"),
   1889                     "is_public": i % 2 == 0,
   1890                     "conversion_rate_class_id": if m > 0 && m <= 3 {
   1891                         Some(m)
   1892                     } else {
   1893                         None
   1894                     }
   1895                 }))
   1896                 .await
   1897                 .assert_ok();
   1898         }
   1899         // All public
   1900         let res: PublicAccountsResponse = ctx.get("/public-accounts").await.assert_ok_json();
   1901         assert_eq!(res.public_accounts.len(), 3);
   1902         for acc in res.public_accounts {
   1903             assert_eq!(0, acc.username.parse::<u8>().unwrap() % 2)
   1904         }
   1905         // Conversion rate
   1906         let res: ListBankAccountsResponse = ctx.get_admin("/accounts").await.assert_ok_json();
   1907         for acc in res.accounts {
   1908             assert_eq!(
   1909                 acc.conversion_rate,
   1910                 Some(
   1911                     ctx.get_admin(format!("/accounts/{}/conversion-info/rate", acc.username))
   1912                         .await
   1913                         .assert_ok_json()
   1914                 )
   1915             );
   1916         }
   1917         // Filtering
   1918         let check_ids = async |query: &str, ids: &[&str]| {
   1919             let res = ctx.get_admin(format!("/accounts?{query}")).await;
   1920             if ids.is_empty() {
   1921                 res.assert_no_content();
   1922             } else {
   1923                 let res: ListBankAccountsResponse = res.assert_ok_json();
   1924                 assert_eq!(
   1925                     ids,
   1926                     res.accounts
   1927                         .into_iter()
   1928                         .map(|it| it.username)
   1929                         .collect::<Vec<_>>()
   1930                 )
   1931             }
   1932         };
   1933         check_ids("", &["4", "3", "2", "1", "0", "admin"]).await;
   1934         check_ids("filter_name=1", &["4", "3", "2", "1", "0"]).await;
   1935         check_ids("filter_name=3", &["3"]).await;
   1936         check_ids("conversion_rate_class_id=1", &["4", "1"]).await;
   1937         check_ids("conversion_rate_class_id=2", &["2"]).await;
   1938         check_ids("conversion_rate_class_id=3", &[]).await;
   1939         check_ids("conversion_rate_class_id=4", &[]).await;
   1940         check_ids("conversion_rate_class_id=0", &["3", "0", "admin"]).await;
   1941         check_ids("conversion_rate_class_id=0&filter_name=1", &["3", "0"]).await;
   1942         for (id, num) in [(1, 2), (2, 1), (3, 0)] {
   1943             assert_eq!(
   1944                 num,
   1945                 ctx.get_admin(format!("/conversion-rate-classes/{id}"))
   1946                     .await
   1947                     .assert_ok_json::<ConversionRateClass>()
   1948                     .num_users
   1949             );
   1950         }
   1951     }
   1952 }