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 ¶ms, 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 ¶ms, 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 }