aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine A <>2024-01-17 13:03:26 +0000
committerAntoine A <>2024-01-17 13:03:26 +0000
commit5efc69c41f736fc383e78567fd2446792525420f (patch)
tree79e3a54f76e471e1ce7c5571b8f6ece6f7e7d572
parent1aa936208e54bdc8afd0a57070a864a1b044c312 (diff)
downloadlibeufin-5efc69c41f736fc383e78567fd2446792525420f.tar.gz
libeufin-5efc69c41f736fc383e78567fd2446792525420f.tar.bz2
libeufin-5efc69c41f736fc383e78567fd2446792525420f.zip
receiver-name in cashout payto
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt119
-rw-r--r--bank/src/test/kotlin/CoreBankApiTest.kt61
3 files changed, 121 insertions, 62 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
index e37a1661..5f8830cb 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
@@ -454,14 +454,17 @@ class IbanPayTo: PaytoUri {
receiverName = params["receiver-name"]
}
+ /** Canonical IBAN payto with receiver-name parameter if present */
fun maybeFull(): String {
return canonical + if (receiverName != null) ("?receiver-name=" + receiverName.encodeURLParameter()) else ""
}
+ /** Canonical IBAN payto with receiver-name parameter, fail if absent */
fun expectFull(): String {
return canonical + "?receiver-name=" + receiverName!!.encodeURLParameter()
}
+ /** Canonical IBAN payto with receiver-name parameter set to [defaultName] if absent */
fun fullOptName(defaultName: String): String {
return canonical + "?receiver-name=" + (receiverName ?: defaultName).encodeURLParameter()
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
index ac9b6a96..e9e30c52 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
@@ -240,83 +240,88 @@ class AccountDAO(private val db: Database) {
val checkCashout = !isAdmin && !allowEditCashout && cashoutPayto.isSome()
val checkDebtLimit = !isAdmin && debtLimit != null
- // Get user ID and check reconfig rights TODO checkout with name
- val (customerId, currChannel, currInfo) = conn.prepareStatement("""
- SELECT
- customer_id
- ,(${ if (checkName) "name != ?" else "false" }) as name_change
- ,(${ if (checkCashout) "cashout_payto IS DISTINCT FROM ?" else "false" }) as cashout_change
- ,(${ if (checkDebtLimit) "max_debt != (?, ?)::taler_amount" else "false" }) as debt_limit_change
- ,(${ when (tan_channel.get()) {
- null -> "false"
- TanChannel.sms -> if (phone.get() != null) "false" else "phone IS NULL"
- TanChannel.email -> if (email.get() != null) "false" else "email IS NULL"
- }}) as missing_tan_info
- ,tan_channel, phone, email
+ data class CurrentAccount(
+ val id: Long,
+ val channel: TanChannel?,
+ val email: String?,
+ val phone: String?,
+ val name: String,
+ val cashoutPayTo: String?,
+ val debtLimit: TalerAmount,
+ )
+
+ // Get user ID and current data
+ val curr = conn.prepareStatement("""
+ SELECT
+ customer_id, tan_channel, phone, email, name, cashout_payto
+ ,(max_debt).val AS max_debt_val
+ ,(max_debt).frac AS max_debt_frac
FROM customers
JOIN bank_accounts
ON customer_id=owning_customer_id
WHERE login=?
""").run {
- var idx = 1;
- if (checkName) {
- setString(idx, name); idx++
- }
- if (checkCashout) {
- setString(idx, cashoutPayto.get()?.maybeFull()); idx++ // TODO cashout with name
- }
- if (checkDebtLimit) {
- setLong(idx, debtLimit!!.value); idx++
- setInt(idx, debtLimit.frac); idx++
- }
- setString(idx, login)
- executeQuery().use {
- when {
- !it.next() -> return@transaction AccountPatchResult.UnknownAccount
- it.getBoolean("name_change") -> return@transaction AccountPatchResult.NonAdminName
- it.getBoolean("cashout_change") -> return@transaction AccountPatchResult.NonAdminCashout
- it.getBoolean("debt_limit_change") -> return@transaction AccountPatchResult.NonAdminDebtLimit
- it.getBoolean("missing_tan_info") -> return@transaction AccountPatchResult.MissingTanInfo
- else -> {
- val currChannel = it.getString("tan_channel")?.run { TanChannel.valueOf(this) }
- Triple(
- it.getLong("customer_id"),
- currChannel,
- when (tan_channel.get() ?: currChannel) {
- TanChannel.sms -> it.getString("phone")
- TanChannel.email -> it.getString("email")
- null -> null
- }
- )
- }
- }
- }
+ setString(1, login)
+ oneOrNull {
+ CurrentAccount(
+ id = it.getLong("customer_id"),
+ channel = it.getString("tan_channel")?.run { TanChannel.valueOf(this) },
+ phone = it.getString("phone"),
+ email = it.getString("email"),
+ name = it.getString("name"),
+ cashoutPayTo = it.getString("cashout_payto"),
+ debtLimit = it.getAmount("max_debt", db.bankCurrency),
+ )
+ } ?: return@transaction AccountPatchResult.UnknownAccount
}
-
- val newChannel = tan_channel.get();
- val newInfo = when (newChannel ?: currChannel) {
+
+ // Patched TAN channel
+ val patchChannel = tan_channel.get()
+ // TAN channel after the PATCH
+ val newChannel = patchChannel ?: curr.channel
+ // Patched TAN info
+ val patchInfo = when (newChannel) {
TanChannel.sms -> phone.get()
TanChannel.email -> email.get()
null -> null
}
+ // TAN info after the PATCH
+ val newInfo = patchInfo ?: when (newChannel) {
+ TanChannel.sms -> curr.phone
+ TanChannel.email -> curr.email
+ null -> null
+ }
+ // Cashout payto with a receiver-name using if receiver-name is missing the new named if present or the current one
+ val cashoutPaytoNamed = cashoutPayto.get()?.fullOptName(name ?: curr.name)
+
+ // Check reconfig rights
+ if (checkName && name != curr.name)
+ return@transaction AccountPatchResult.NonAdminName
+ if (checkCashout && cashoutPaytoNamed != curr.cashoutPayTo)
+ return@transaction AccountPatchResult.NonAdminCashout
+ if (checkDebtLimit && debtLimit != curr.debtLimit)
+ return@transaction AccountPatchResult.NonAdminDebtLimit
+ if (patchChannel != null && newInfo == null)
+ return@transaction AccountPatchResult.MissingTanInfo
+
// Tan channel verification
if (!isAdmin) {
// Check performed 2fa check
- if (currChannel != null && !is2fa) {
+ if (curr.channel != null && !is2fa) {
// Perform challenge with current settings
return@transaction AccountPatchResult.TanRequired(channel = null, info = null)
}
// If channel or info changed and the 2fa challenge is performed with old settings perform a new challenge with new settings
- if ((newChannel != null && newChannel != faChannel) || (newInfo != null && newInfo != faInfo)) {
- return@transaction AccountPatchResult.TanRequired(channel = newChannel ?: currChannel, info = newInfo ?: currInfo)
+ if ((patchChannel != null && patchChannel != faChannel) || (patchInfo != null && patchInfo != faInfo)) {
+ return@transaction AccountPatchResult.TanRequired(channel = newChannel, info = newInfo)
}
}
// Invalidate current challenges
- if (newChannel != null || newInfo != null) {
+ if (patchChannel != null || patchInfo != null) {
val stmt = conn.prepareStatement("UPDATE tan_challenges SET expiration_date=0 WHERE customer=?")
- stmt.setLong(1, customerId)
+ stmt.setLong(1, curr.id)
stmt.execute()
}
@@ -331,7 +336,7 @@ class AccountDAO(private val db: Database) {
sequence {
isPublic?.let { yield(it) }
debtLimit?.let { yield(it.value); yield(it.frac) }
- yield(customerId)
+ yield(curr.id)
}
)
@@ -347,12 +352,12 @@ class AccountDAO(private val db: Database) {
},
"WHERE customer_id = ?",
sequence {
- cashoutPayto.some { yield(it?.canonical) }
+ cashoutPayto.some { yield(cashoutPaytoNamed) }
phone.some { yield(it) }
email.some { yield(it) }
tan_channel.some { yield(it?.name) }
name?.let { yield(it) }
- yield(customerId)
+ yield(curr.id)
}
)
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
index 5a031b87..c9ecfa03 100644
--- a/bank/src/test/kotlin/CoreBankApiTest.kt
+++ b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -195,7 +195,7 @@ class CoreBankAccountsApiTest {
}
// Check given payto
- val ibanPayto = genIbanPaytoUri()
+ val ibanPayto = IbanPayTo(genIbanPaytoUri())
val req = obj {
"username" to "foo"
"password" to "password"
@@ -208,7 +208,7 @@ class CoreBankAccountsApiTest {
client.post("/accounts") {
json(req)
}.assertOkJson<RegisterAccountResponse> {
- assertEquals(ibanPayto, it.internal_payto_uri)
+ assertEquals(ibanPayto.canonical, it.internal_payto_uri)
}
// Testing idempotency
client.post("/accounts") {
@@ -298,6 +298,31 @@ class CoreBankAccountsApiTest {
client.get("/accounts/bar") {
pwAuth("admin")
}.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT)
+
+ // Check cashout payto receiver name logic
+ client.post("/accounts") {
+ json {
+ "username" to "cashout_guess"
+ "password" to "cashout_guess-password"
+ "name" to "Mr Guess My Name"
+ "cashout_payto_uri" to ibanPayto
+ }
+ }.assertOk()
+ client.getA("/accounts/cashout_guess").assertOkJson<AccountData> {
+ assertEquals(ibanPayto.fullOptName("Mr Guess My Name"), it.cashout_payto_uri)
+ }
+ val full = ibanPayto.fullOptName("Santa Claus")
+ client.post("/accounts") {
+ json {
+ "username" to "cashout_keep"
+ "password" to "cashout_keep-password"
+ "name" to "Mr Keep My Name"
+ "cashout_payto_uri" to full
+ }
+ }.assertOk()
+ client.getA("/accounts/cashout_keep").assertOkJson<AccountData> {
+ assertEquals(full, it.cashout_payto_uri)
+ }
}
// Test account created with bonus
@@ -465,7 +490,7 @@ class CoreBankAccountsApiTest {
// Successful attempt now
val cashout = IbanPayTo(genIbanPaytoUri())
val req = obj {
- "cashout_payto_uri" to cashout.canonical
+ "cashout_payto_uri" to cashout
"name" to "Roger"
"is_public" to true
"contact_data" to obj {
@@ -496,7 +521,7 @@ class CoreBankAccountsApiTest {
// Check patch
client.getA("/accounts/merchant").assertOkJson<AccountData> { obj ->
assertEquals("Roger", obj.name)
- assertEquals(cashout.canonical, obj.cashout_payto_uri)
+ assertEquals(cashout.fullOptName(obj.name), obj.cashout_payto_uri)
assertEquals("+99", obj.contact_data?.phone?.get())
assertEquals("foo@example.com", obj.contact_data?.email?.get())
assertEquals(TalerAmount("KUDOS:100"), obj.debit_threshold)
@@ -510,7 +535,7 @@ class CoreBankAccountsApiTest {
}.assertNoContent()
client.getA("/accounts/merchant").assertOkJson<AccountData> { obj ->
assertEquals("Roger", obj.name)
- assertEquals(cashout.canonical, obj.cashout_payto_uri)
+ assertEquals(cashout.fullOptName(obj.name), obj.cashout_payto_uri)
assertEquals("+99", obj.contact_data?.phone?.get())
assertEquals("foo@example.com", obj.contact_data?.email?.get())
assertEquals(TalerAmount("KUDOS:100"), obj.debit_threshold)
@@ -525,6 +550,32 @@ class CoreBankAccountsApiTest {
}
}.assertConflict(TalerErrorCode.END)
+ // Check cashout payto receiver name logic
+ client.post("/accounts") {
+ json {
+ "username" to "cashout"
+ "password" to "cashout-password"
+ "name" to "Mr Cashout Cashout"
+ }
+ }.assertOk()
+ for ((cashout, name, expect) in listOf(
+ Triple(cashout.canonical, null, cashout.fullOptName("Mr Cashout Cashout")),
+ Triple(cashout.canonical, "New name", cashout.fullOptName("New name")),
+ Triple(cashout.fullOptName("Full name"), null, cashout.fullOptName("Full name")),
+ Triple(cashout.fullOptName("Full second name"), "Another name", cashout.fullOptName("Full second name"))
+ )) {
+ client.patch("/accounts/cashout") {
+ pwAuth("admin")
+ json {
+ "cashout_payto_uri" to cashout
+ if (name != null) "name" to name
+ }
+ }.assertNoContent()
+ client.getA("/accounts/cashout").assertOkJson<AccountData> { obj ->
+ assertEquals(expect, obj.cashout_payto_uri)
+ }
+ }
+
// Check 2FA
fillTanInfo("merchant")
client.patchA("/accounts/merchant") {