[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] 02/05: Bank API.
From: |
gnunet |
Subject: |
[libeufin] 02/05: Bank API. |
Date: |
Mon, 18 Sep 2023 14:27:15 +0200 |
This is an automated email from the git hooks/post-receive script.
ms pushed a commit to branch master
in repository libeufin.
commit 0a3956ee07af2d66b1f726198b2dd8def187b372
Author: MS <ms@taler.net>
AuthorDate: Mon Sep 18 13:23:52 2023 +0200
Bank API.
Implementing:
GET /accounts/{USERNAME}
GET /transactions/{T_ID}
POST /transactions
---
.../src/main/kotlin/tech/libeufin/bank/Database.kt | 99 +++++++++++-
bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 171 +--------------------
.../tech/libeufin/bank/accountsMgmtHandlers.kt | 141 +++++++++++++++++
bank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 42 ++++-
.../kotlin/tech/libeufin/bank/tokenHandlers.kt | 91 +++++++++++
.../tech/libeufin/bank/transactionsHandlers.kt | 95 ++++++++++++
bank/src/main/kotlin/tech/libeufin/bank/types.kt | 49 +++++-
util/src/main/kotlin/HTTP.kt | 2 +-
8 files changed, 517 insertions(+), 173 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index 73194c36..ebe4491a 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -177,6 +177,7 @@ class Database(private val dbConfig: String) {
)
}
}
+
fun customerGetFromLogin(login: String): Customer? {
reconnect()
val stmt = prepare("""
@@ -187,7 +188,8 @@ class Database(private val dbConfig: String) {
email,
phone,
cashout_payto,
- cashout_currency
+ cashout_currency,
+ has_debit
FROM customers
WHERE login=?
""")
@@ -317,6 +319,7 @@ class Database(private val dbConfig: String) {
,has_debt
,(max_debt).val AS max_debt_val
,(max_debt).frac AS max_debt_frac
+ ,bank_account_id
FROM bank_accounts
WHERE owning_customer_id=?
""")
@@ -338,7 +341,48 @@ class Database(private val dbConfig: String) {
maxDebt = TalerAmount(
value = it.getLong("max_debt_val"),
frac = it.getInt("max_debt_frac")
- )
+ ),
+ bankAccountId = it.getLong("bank_account_id")
+ )
+ }
+ }
+ fun bankAccountGetFromInternalPayto(internalPayto: String): BankAccount? {
+ reconnect()
+ val stmt = prepare("""
+ SELECT
+ ,bank_account_id
+ ,owning_customer_id
+ ,is_public
+ ,is_taler_exchange
+ ,last_nexus_fetch_row_id
+ ,(balance).val AS balance_val
+ ,(balance).frac AS balance_frac
+ ,has_debt
+ ,(max_debt).val AS max_debt_val
+ ,(max_debt).frac AS max_debt_frac
+ FROM bank_accounts
+ WHERE internal_payto_uri=?
+ """)
+ stmt.setString(1, internalPayto)
+
+ val rs = stmt.executeQuery()
+ rs.use {
+ if (!it.next()) return null
+ return BankAccount(
+ internalPaytoUri = internalPayto,
+ balance = TalerAmount(
+ it.getLong("balance_val"),
+ it.getInt("balance_frac")
+ ),
+ lastNexusFetchRowId = it.getLong("last_nexus_fetch_row_id"),
+ owningCustomerId = it.getLong("owning_customer_id"),
+ hasDebt = it.getBoolean("has_debt"),
+ isTalerExchange = it.getBoolean("is_taler_exchange"),
+ maxDebt = TalerAmount(
+ value = it.getLong("max_debt_val"),
+ frac = it.getInt("max_debt_frac")
+ ),
+ bankAccountId = it.getLong("bank_account_id")
)
}
}
@@ -388,6 +432,57 @@ class Database(private val dbConfig: String) {
}
}
+ // Get the bank transaction whose row ID is rowId
+ fun bankTransactionGetFromInternalId(rowId: Long): BankAccountTransaction?
{
+ reconnect()
+ val stmt = prepare("""
+ SELECT
+ creditor_payto_uri
+ ,creditor_name
+ ,debtor_payto_uri
+ ,debtor_name
+ ,subject
+ ,(amount).val AS amount_val
+ ,(amount).frac AS amount_frac
+ ,transaction_date
+ ,account_servicer_reference
+ ,payment_information_id
+ ,end_to_end_id
+ ,direction
+ ,owning_customer_id
+ FROM bank_account_transactions
+ WHERE bank_transaction_id=?
+ """)
+ stmt.setLong(1, rowId)
+ val rs = stmt.executeQuery()
+ rs.use {
+ if (!it.next()) return null
+ return BankAccountTransaction(
+ creditorPaytoUri = it.getString("creditor_payto_uri"),
+ creditorName = it.getString("creditor_name"),
+ debtorPaytoUri = it.getString("debtor_payto_uri"),
+ debtorName = it.getString("debtor_name"),
+ amount = TalerAmount(
+ it.getLong("amount_val"),
+ it.getInt("amount_frac")
+ ),
+ accountServicerReference =
it.getString("account_servicer_reference"),
+ endToEndId = it.getString("end_to_end_id"),
+ direction = it.getString("direction").run {
+ when(this) {
+ "credit" -> TransactionDirection.credit
+ "debit" -> TransactionDirection.debit
+ else -> throw internalServerError("Wrong direction in
transaction: $this")
+ }
+ },
+ bankAccountId = it.getLong("owning_customer_id"),
+ paymentInformationId = it.getString("payment_information_id"),
+ subject = it.getString("subject"),
+ transactionDate = it.getLong("transaction_date")
+ )
+ }
+ }
+
fun bankTransactionGetForHistoryPage(
upperBound: Long,
bankAccountId: Long,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 2e30038d..fd55befd 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -39,13 +39,11 @@ import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.SerializersModule
import net.taler.common.errorcodes.TalerErrorCode
-import net.taler.wallet.crypto.Base32Crockford
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import tech.libeufin.util.*
import java.time.Duration
-import kotlin.random.Random
// GLOBALS
val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank")
@@ -207,167 +205,14 @@ val webApp: Application.() -> Unit = {
}
}
routing {
- post("/accounts/{USERNAME}/token") {
- val customer = call.myAuth(TokenScope.refreshable) ?: throw
unauthorized("Authentication failed")
- val endpointOwner = call.expectUriComponent("USERNAME")
- if (customer.login != endpointOwner)
- throw forbidden(
- "User has no rights on this enpoint",
- TalerErrorCode.TALER_EC_END // FIXME: need generic
forbidden
- )
- val maybeAuthToken = call.getAuthToken()
- val req = call.receive<TokenRequest>()
- /**
- * This block checks permissions ONLY IF the call was authenticated
- * with a token. Basic auth gets always granted.
- */
- if (maybeAuthToken != null) {
- val tokenBytes = Base32Crockford.decode(maybeAuthToken)
- val refreshingToken = db.bearerTokenGet(tokenBytes) ?: throw
internalServerError(
- "Token used to auth not found in the database!"
- )
- if (refreshingToken.scope == TokenScope.readonly && req.scope
== TokenScope.readwrite)
- throw forbidden(
- "Cannot generate RW token from RO",
-
TalerErrorCode.TALER_EC_GENERIC_TOKEN_PERMISSION_INSUFFICIENT
- )
- }
- val tokenBytes = ByteArray(32).apply {
- java.util.Random().nextBytes(this)
- }
- val maxDurationTime: Long = db.configGet("token_max_duration").run
{
- if (this == null)
- return@run Long.MAX_VALUE
- return@run try {
- this.toLong()
- } catch (e: Exception) {
- logger.error("Could not convert config's
token_max_duration to Long")
- throw internalServerError(e.message)
- }
- }
- if (req.duration != null &&
req.duration.d_us.compareTo(maxDurationTime) == 1)
- throw forbidden(
- "Token duration bigger than bank's limit",
- // FIXME: define new EC for this case.
- TalerErrorCode.TALER_EC_END
- )
- val tokenDurationUs = req.duration?.d_us ?:
TOKEN_DEFAULT_DURATION_US
- val customerDbRow = customer.dbRowId ?: throw internalServerError(
- "Coud not resort customer '${customer.login}' database row ID"
- )
- val expirationTimestampUs: Long = getNowUs() + tokenDurationUs
- if (expirationTimestampUs < tokenDurationUs)
- throw badRequest(
- "Token duration caused arithmetic overflow",
- // FIXME: need dedicate EC (?)
- talerErrorCode = TalerErrorCode.TALER_EC_END
- )
- val token = BearerToken(
- bankCustomer = customerDbRow,
- content = tokenBytes,
- creationTime = expirationTimestampUs,
- expirationTime = expirationTimestampUs,
- scope = req.scope,
- isRefreshable = req.refreshable
- )
- if (!db.bearerTokenCreate(token))
- throw internalServerError("Failed at inserting new token in
the database")
- call.respond(TokenSuccessResponse(
- access_token = Base32Crockford.encode(tokenBytes),
- expiration = Timestamp(
- t_s = expirationTimestampUs / 1000000L
- )
- ))
- return@post
- }
- post("/accounts") {
- // check if only admin.
- val maybeOnlyAdmin = db.configGet("only_admin_registrations")
- if (maybeOnlyAdmin?.lowercase() == "yes") {
- val customer: Customer? = call.myAuth(TokenScope.readwrite)
- if (customer == null || customer.login != "admin")
- throw LibeufinBankException(
- httpStatus = HttpStatusCode.Unauthorized,
- talerError = TalerError(
- code =
TalerErrorCode.TALER_EC_BANK_LOGIN_FAILED.code,
- hint = "Either 'admin' not authenticated or an
ordinary user tried this operation."
- )
- )
- }
- // auth passed, proceed with activity.
- val req = call.receive<RegisterAccountRequest>()
- // Prohibit reserved usernames:
- if (req.username == "admin" || req.username == "bank")
- throw LibeufinBankException(
- httpStatus = HttpStatusCode.Conflict,
- talerError = TalerError(
- code = GENERIC_UNDEFINED, // FIXME: this waits GANA.
- hint = "Username '${req.username}' is reserved."
- )
- )
- // Checking imdepotency.
- val maybeCustomerExists = db.customerGetFromLogin(req.username)
- // Can be null if previous call crashed before completion.
- val maybeHasBankAccount = maybeCustomerExists.run {
- if (this == null) return@run null
- db.bankAccountGetFromOwnerId(this.expectRowId())
- }
- if (maybeCustomerExists != null && maybeHasBankAccount != null) {
- logger.debug("Registering username was found:
${maybeCustomerExists.login}")
- // Checking _all_ the details are the same.
- val isIdentic =
- maybeCustomerExists.name == req.name &&
- maybeCustomerExists.email ==
req.challenge_contact_data?.email &&
- maybeCustomerExists.phone ==
req.challenge_contact_data?.phone &&
- maybeCustomerExists.cashoutPayto == req.cashout_payto_uri
&&
- CryptoUtil.checkpw(req.password,
maybeCustomerExists.passwordHash) &&
- maybeHasBankAccount.isPublic == req.is_public &&
- maybeHasBankAccount.isTalerExchange ==
req.is_taler_exchange &&
- maybeHasBankAccount.internalPaytoUri ==
req.internal_payto_uri
- if (isIdentic) {
- call.respond(HttpStatusCode.Created)
- return@post
- }
- throw LibeufinBankException(
- httpStatus = HttpStatusCode.Conflict,
- talerError = TalerError(
- code = GENERIC_UNDEFINED, // GANA needs this.
- hint = "Idempotency check failed."
- )
- )
- }
- // From here: fresh user being added.
- val newCustomer = Customer(
- login = req.username,
- name = req.name,
- email = req.challenge_contact_data?.email,
- phone = req.challenge_contact_data?.phone,
- cashoutPayto = req.cashout_payto_uri,
- // Following could be gone, if included in cashout_payto_uri
- cashoutCurrency = db.configGet("cashout_currency"),
- passwordHash = CryptoUtil.hashpw(req.password)
- )
- val newCustomerRowId = db.customerCreate(newCustomer)
- ?: throw internalServerError("New customer INSERT failed
despite the previous checks")
- /* Crashing here won't break data consistency between customers
- * and bank accounts, because of the idempotency. Client will
- * just have to retry. */
- val maxDebt = db.configGet("max_debt_ordinary_customers").run {
- if (this == null) throw internalServerError("Max debt not
configured")
- parseTalerAmount(this)
- }
- val newBankAccount = BankAccount(
- hasDebt = false,
- internalPaytoUri = req.internal_payto_uri ?: genIbanPaytoUri(),
- owningCustomerId = newCustomerRowId,
- isPublic = req.is_public,
- isTalerExchange = req.is_taler_exchange,
- maxDebt = maxDebt
- )
- if (!db.bankAccountCreate(newBankAccount))
- throw internalServerError("Could not INSERT bank account
despite all the checks.")
- call.respond(HttpStatusCode.Created)
- return@post
+ get("/config") {
+ call.respond(Config())
+ return@get
}
+ this.accountsMgmtHandlers()
+ this.tokenHandlers()
+ this.transactionsHandlers()
+ // this.talerHandlers()
+ // this.walletIntegrationHandlers()
}
}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
new file mode 100644
index 00000000..0bbe0065
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
@@ -0,0 +1,141 @@
+package tech.libeufin.bank
+
+import io.ktor.http.*
+import io.ktor.server.application.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
+import net.taler.common.errorcodes.TalerErrorCode
+import tech.libeufin.util.CryptoUtil
+import tech.libeufin.util.maybeUriComponent
+
+/**
+ * This function collects all the /accounts handlers that
+ * create, update, delete, show bank accounts. No histories
+ * and wire transfers should belong here.
+ */
+fun Routing.accountsMgmtHandlers() {
+ post("/accounts") {
+ // check if only admin.
+ val maybeOnlyAdmin = db.configGet("only_admin_registrations")
+ if (maybeOnlyAdmin?.lowercase() == "yes") {
+ val customer: Customer? = call.myAuth(TokenScope.readwrite)
+ if (customer == null || customer.login != "admin")
+ throw LibeufinBankException(
+ httpStatus = HttpStatusCode.Unauthorized,
+ talerError = TalerError(
+ code = TalerErrorCode.TALER_EC_BANK_LOGIN_FAILED.code,
+ hint = "Either 'admin' not authenticated or an
ordinary user tried this operation."
+ )
+ )
+ }
+ // auth passed, proceed with activity.
+ val req = call.receive<RegisterAccountRequest>()
+ // Prohibit reserved usernames:
+ if (req.username == "admin" || req.username == "bank")
+ throw LibeufinBankException(
+ httpStatus = HttpStatusCode.Conflict,
+ talerError = TalerError(
+ code = GENERIC_UNDEFINED, // FIXME: this waits GANA.
+ hint = "Username '${req.username}' is reserved."
+ )
+ )
+ // Checking imdepotency.
+ val maybeCustomerExists = db.customerGetFromLogin(req.username)
+ // Can be null if previous call crashed before completion.
+ val maybeHasBankAccount = maybeCustomerExists.run {
+ if (this == null) return@run null
+ db.bankAccountGetFromOwnerId(this.expectRowId())
+ }
+ if (maybeCustomerExists != null && maybeHasBankAccount != null) {
+ tech.libeufin.bank.logger.debug("Registering username was found:
${maybeCustomerExists.login}")
+ // Checking _all_ the details are the same.
+ val isIdentic =
+ maybeCustomerExists.name == req.name &&
+ maybeCustomerExists.email ==
req.challenge_contact_data?.email &&
+ maybeCustomerExists.phone ==
req.challenge_contact_data?.phone &&
+ maybeCustomerExists.cashoutPayto ==
req.cashout_payto_uri &&
+ CryptoUtil.checkpw(req.password,
maybeCustomerExists.passwordHash) &&
+ maybeHasBankAccount.isPublic == req.is_public &&
+ maybeHasBankAccount.isTalerExchange ==
req.is_taler_exchange &&
+ maybeHasBankAccount.internalPaytoUri ==
req.internal_payto_uri
+ if (isIdentic) {
+ call.respond(HttpStatusCode.Created)
+ return@post
+ }
+ throw LibeufinBankException(
+ httpStatus = HttpStatusCode.Conflict,
+ talerError = TalerError(
+ code = GENERIC_UNDEFINED, // GANA needs this.
+ hint = "Idempotency check failed."
+ )
+ )
+ }
+ // From here: fresh user being added.
+ val newCustomer = Customer(
+ login = req.username,
+ name = req.name,
+ email = req.challenge_contact_data?.email,
+ phone = req.challenge_contact_data?.phone,
+ cashoutPayto = req.cashout_payto_uri,
+ // Following could be gone, if included in cashout_payto_uri
+ cashoutCurrency = db.configGet("cashout_currency"),
+ passwordHash = CryptoUtil.hashpw(req.password),
+ )
+ val newCustomerRowId = db.customerCreate(newCustomer)
+ ?: throw internalServerError("New customer INSERT failed despite
the previous checks")
+ /* Crashing here won't break data consistency between customers
+ * and bank accounts, because of the idempotency. Client will
+ * just have to retry. */
+ val maxDebt = db.configGet("max_debt_ordinary_customers").run {
+ if (this == null) throw internalServerError("Max debt not
configured")
+ parseTalerAmount(this)
+ }
+ val bonus = db.configGet("registration_bonus")
+ val initialBalance = if (bonus != null) parseTalerAmount(bonus) else
TalerAmount(0, 0)
+ val newBankAccount = BankAccount(
+ hasDebt = false,
+ internalPaytoUri = req.internal_payto_uri ?: genIbanPaytoUri(),
+ owningCustomerId = newCustomerRowId,
+ isPublic = req.is_public,
+ isTalerExchange = req.is_taler_exchange,
+ maxDebt = maxDebt,
+ balance = initialBalance
+ )
+ if (!db.bankAccountCreate(newBankAccount))
+ throw internalServerError("Could not INSERT bank account despite
all the checks.")
+ call.respond(HttpStatusCode.Created)
+ return@post
+ }
+ get("/accounts/{USERNAME}") {
+ val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized("Login
failed")
+ val resourceName = call.maybeUriComponent("USERNAME") ?: throw
badRequest(
+ hint = "No username found in the URI",
+ talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MISSING
+ )
+ // Checking resource name only if Basic auth was used.
+ // Successful tokens do not need this check, they just pass.
+ if (
+ ((c.login != resourceName)
+ && (c.login != "admin"))
+ && (call.getAuthToken() == null)
+ )
+ throw forbidden("No rights on the resource.")
+ val customerData = db.customerGetFromLogin(c.login) ?: throw
internalServerError("Customer '${c.login} despite being authenticated.'")
+ val customerInternalId = customerData.dbRowId ?: throw
internalServerError("Customer '${c.login} had no row ID despite it was found in
the database.'")
+ val bankAccountData = db.bankAccountGetFromOwnerId(customerInternalId)
?: throw internalServerError("Customer '${c.login} had no bank account despite
they are customer.'")
+ call.respond(AccountData(
+ name = customerData.name,
+ balance = bankAccountData.balance,
+ debit_threshold = bankAccountData.maxDebt,
+ payto_uri = bankAccountData.internalPaytoUri,
+ contact_data = ChallengeContactData(
+ email = customerData.email,
+ phone = customerData.phone
+ ),
+ cashout_payto_uri = customerData.cashoutPayto,
+ has_debit = bankAccountData.hasDebt
+ ))
+ return@get
+ }
+}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index 28c58fe5..258c1f96 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -26,6 +26,11 @@ import net.taler.wallet.crypto.Base32Crockford
import tech.libeufin.util.*
import java.lang.NumberFormatException
+fun ApplicationCall.expectUriComponent(componentName: String) =
+ this.maybeUriComponent(componentName) ?: throw badRequest(
+ hint = "No username found in the URI",
+ talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MISSING
+)
// Get the auth token (stripped of the bearer-token:-prefix)
// IF the call was authenticated with it.
fun ApplicationCall.getAuthToken(): String? {
@@ -131,7 +136,11 @@ fun doTokenAuth(
))
}
-fun forbidden(hint: String? = null, talerErrorCode: TalerErrorCode):
LibeufinBankException =
+fun forbidden(
+ hint: String = "No rights on the resource",
+ // FIXME: create a 'generic forbidden' Taler EC.
+ talerErrorCode: TalerErrorCode = TalerErrorCode.TALER_EC_END
+): LibeufinBankException =
LibeufinBankException(
httpStatus = HttpStatusCode.Forbidden,
talerError = TalerError(
@@ -140,7 +149,7 @@ fun forbidden(hint: String? = null, talerErrorCode:
TalerErrorCode): LibeufinBan
)
)
-fun unauthorized(hint: String? = null): LibeufinBankException =
+fun unauthorized(hint: String = "Login failed"): LibeufinBankException =
LibeufinBankException(
httpStatus = HttpStatusCode.Unauthorized,
talerError = TalerError(
@@ -156,6 +165,31 @@ fun internalServerError(hint: String?):
LibeufinBankException =
hint = hint
)
)
+
+
+fun notFound(
+ hint: String?,
+ talerEc: TalerErrorCode
+): LibeufinBankException =
+ LibeufinBankException(
+ httpStatus = HttpStatusCode.NotFound,
+ talerError = TalerError(
+ code = talerEc.code,
+ hint = hint
+ )
+ )
+
+fun conflict(
+ hint: String?,
+ talerEc: TalerErrorCode
+): LibeufinBankException =
+ LibeufinBankException(
+ httpStatus = HttpStatusCode.Conflict,
+ talerError = TalerError(
+ code = talerEc.code,
+ hint = hint
+ )
+ )
fun badRequest(
hint: String? = null,
talerErrorCode: TalerErrorCode =
TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID
@@ -221,4 +255,6 @@ fun parseTalerAmount(
frac = fraction,
maybeCurrency = match.destructured.component1()
)
-}
\ No newline at end of file
+}
+
+fun getBankCurrency(): String = db.configGet("internal_currency") ?: throw
internalServerError("Bank lacks currency")
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
new file mode 100644
index 00000000..0b665069
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
@@ -0,0 +1,91 @@
+package tech.libeufin.bank
+
+import io.ktor.server.application.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
+import net.taler.common.errorcodes.TalerErrorCode
+import net.taler.wallet.crypto.Base32Crockford
+import tech.libeufin.util.maybeUriComponent
+import tech.libeufin.util.getNowUs
+
+fun Routing.tokenHandlers() {
+ delete("/accounts/{USERNAME}/token") {
+ throw internalServerError("Token deletion not implemented.")
+ }
+ post("/accounts/{USERNAME}/token") {
+ val customer = call.myAuth(TokenScope.refreshable) ?: throw
unauthorized("Authentication failed")
+ val endpointOwner = call.maybeUriComponent("USERNAME")
+ if (customer.login != endpointOwner)
+ throw forbidden(
+ "User has no rights on this enpoint",
+ TalerErrorCode.TALER_EC_END // FIXME: need generic forbidden
+ )
+ val maybeAuthToken = call.getAuthToken()
+ val req = call.receive<TokenRequest>()
+ /**
+ * This block checks permissions ONLY IF the call was authenticated
+ * with a token. Basic auth gets always granted.
+ */
+ if (maybeAuthToken != null) {
+ val tokenBytes = Base32Crockford.decode(maybeAuthToken)
+ val refreshingToken = db.bearerTokenGet(tokenBytes) ?: throw
internalServerError(
+ "Token used to auth not found in the database!"
+ )
+ if (refreshingToken.scope == TokenScope.readonly && req.scope ==
TokenScope.readwrite)
+ throw forbidden(
+ "Cannot generate RW token from RO",
+
TalerErrorCode.TALER_EC_GENERIC_TOKEN_PERMISSION_INSUFFICIENT
+ )
+ }
+ val tokenBytes = ByteArray(32).apply {
+ java.util.Random().nextBytes(this)
+ }
+ val maxDurationTime: Long = db.configGet("token_max_duration").run {
+ if (this == null)
+ return@run Long.MAX_VALUE
+ return@run try {
+ this.toLong()
+ } catch (e: Exception) {
+ tech.libeufin.bank.logger.error("Could not convert config's
token_max_duration to Long")
+ throw internalServerError(e.message)
+ }
+ }
+ if (req.duration != null &&
req.duration.d_us.compareTo(maxDurationTime) == 1)
+ throw forbidden(
+ "Token duration bigger than bank's limit",
+ // FIXME: define new EC for this case.
+ TalerErrorCode.TALER_EC_END
+ )
+ val tokenDurationUs = req.duration?.d_us ?: TOKEN_DEFAULT_DURATION_US
+ val customerDbRow = customer.dbRowId ?: throw internalServerError(
+ "Coud not resort customer '${customer.login}' database row ID"
+ )
+ val expirationTimestampUs: Long = getNowUs() + tokenDurationUs
+ if (expirationTimestampUs < tokenDurationUs)
+ throw badRequest(
+ "Token duration caused arithmetic overflow",
+ // FIXME: need dedicate EC (?)
+ talerErrorCode = TalerErrorCode.TALER_EC_END
+ )
+ val token = BearerToken(
+ bankCustomer = customerDbRow,
+ content = tokenBytes,
+ creationTime = expirationTimestampUs,
+ expirationTime = expirationTimestampUs,
+ scope = req.scope,
+ isRefreshable = req.refreshable
+ )
+ if (!db.bearerTokenCreate(token))
+ throw internalServerError("Failed at inserting new token in the
database")
+ call.respond(
+ TokenSuccessResponse(
+ access_token = Base32Crockford.encode(tokenBytes),
+ expiration = Timestamp(
+ t_s = expirationTimestampUs / 1000000L
+ )
+ )
+ )
+ return@post
+ }
+}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
new file mode 100644
index 00000000..509df766
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
@@ -0,0 +1,95 @@
+package tech.libeufin.bank
+
+import io.ktor.http.*
+import io.ktor.server.application.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
+import net.taler.common.errorcodes.TalerErrorCode
+import tech.libeufin.util.getNowUs
+import tech.libeufin.util.parsePayto
+
+fun Routing.transactionsHandlers() {
+ // Creates a bank transaction.
+ post("/accounts/{USERNAME}/transactions") {
+ val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
+ val resourceName = call.expectUriComponent("USERNAME")
+ // admin has no rights here.
+ if ((c.login != resourceName) && (call.getAuthToken() == null))
+ throw forbidden()
+ val txData = call.receive<BankAccountTransactionCreate>()
+ val payto = parsePayto(txData.payto_uri)
+ val subject = payto?.message ?: throw badRequest("Wire transfer lacks
subject")
+ val debtorId = c.dbRowId ?: throw internalServerError("Debtor database
ID not found")
+ // This performs already a SELECT on the bank account,
+ // like the wire transfer will do as well later!
+ val creditorCustomerData =
db.bankAccountGetFromInternalPayto(txData.payto_uri)
+ ?: throw notFound(
+ "Creditor account not found",
+ TalerErrorCode.TALER_EC_END // FIXME: define this EC.
+ )
+ val amount = parseTalerAmount(txData.amount)
+ if (amount.currency != getBankCurrency())
+ throw badRequest(
+ "Wrong currency: ${amount.currency}",
+ talerErrorCode =
TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
+ )
+ val dbInstructions = BankInternalTransaction(
+ debtorAccountId = debtorId,
+ creditorAccountId = creditorCustomerData.owningCustomerId,
+ subject = subject,
+ amount = amount,
+ transactionDate = getNowUs()
+ )
+ val res = db.bankTransactionCreate(dbInstructions)
+ when(res) {
+ Database.BankTransactionResult.CONFLICT ->
+ throw conflict(
+ "Insufficient funds",
+ TalerErrorCode.TALER_EC_END // FIXME: need bank
'insufficient funds' EC.
+ )
+ Database.BankTransactionResult.NO_CREDITOR ->
+ throw internalServerError("Creditor not found despite previous
checks.")
+ Database.BankTransactionResult.NO_DEBTOR ->
+ throw internalServerError("Debtor not found despite the
request was authenticated.")
+ Database.BankTransactionResult.SUCCESS ->
call.respond(HttpStatusCode.OK)
+ }
+ return@post
+ }
+ get("/accounts/{USERNAME}/transactions/{T_ID}") {
+ val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
+ val accountOwner = call.expectUriComponent("USERNAME")
+ // auth ok, check rights.
+ if (c.login != "admin" && c.login != accountOwner)
+ throw forbidden()
+ // rights ok, check tx exists.
+ val tId = call.expectUriComponent("T_ID")
+ val txRowId = try {
+ tId.toLong()
+ } catch (e: Exception) {
+ logger.error(e.message)
+ throw badRequest("TRANSACTION_ID is not a number: ${tId}")
+ }
+ val customerRowId = c.dbRowId ?: throw
internalServerError("Authenticated client lacks database entry")
+ val tx = db.bankTransactionGetFromInternalId(txRowId)
+ ?: throw notFound(
+ "Bank transaction '$tId' not found",
+ TalerErrorCode.TALER_EC_NONE // FIXME: need def.
+ )
+ val customerBankAccount = db.bankAccountGetFromOwnerId(customerRowId)
+ ?: throw internalServerError("Customer '${c.login}' lacks bank
account.")
+ if (tx.bankAccountId != customerBankAccount.bankAccountId)
+ throw forbidden("Client has no rights over the bank transaction:
$tId")
+ // auth and rights, respond.
+ call.respond(BankAccountTransactionInfo(
+ amount =
"${tx.amount.currency}:${tx.amount.value}.${tx.amount.frac}",
+ creditor_payto_uri = tx.creditorPaytoUri,
+ debtor_payto_uri = tx.debtorPaytoUri,
+ date = tx.transactionDate,
+ direction = tx.direction,
+ subject = tx.subject,
+ row_id = txRowId
+ ))
+ return@get
+ }
+}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/types.kt
b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
index 25e7edca..edf3422b 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/types.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
@@ -182,6 +182,7 @@ data class BankAccount(
val internalPaytoUri: String,
// Database row ID of the customer that owns this bank account.
val owningCustomerId: Long,
+ val bankAccountId: Long? = null, // null at INSERT.
val isPublic: Boolean = false,
val isTalerExchange: Boolean = false,
/**
@@ -195,7 +196,7 @@ data class BankAccount(
* being wired by wallet owners.
*/
val lastNexusFetchRowId: Long = 0L,
- val balance: TalerAmount? = null,
+ val balance: TalerAmount,
val hasDebt: Boolean,
val maxDebt: TalerAmount
)
@@ -254,9 +255,9 @@ data class BankInternalTransaction(
val subject: String,
val amount: TalerAmount,
val transactionDate: Long,
- val accountServicerReference: String, // ISO20022
- val endToEndId: String, // ISO20022
- val paymentInformationId: String // ISO20022
+ val accountServicerReference: String = "not used", // ISO20022
+ val endToEndId: String = "not used", // ISO20022
+ val paymentInformationId: String = "not used" // ISO20022
)
/**
@@ -326,4 +327,44 @@ data class Cashout(
val bankAccount: Long,
val credit_payto_uri: String,
val cashoutCurrency: String
+)
+
+// Type to return as GET /config response
+@Serializable // Never used to parse JSON.
+data class Config(
+ val name: String = "libeufin-bank",
+ val version: String = "0:0:0",
+ val have_cashout: Boolean = false,
+ // Following might probably get renamed:
+ val fiat_currency: String? = null
+)
+
+// GET /accounts/$USERNAME response.
+data class AccountData(
+ val name: String,
+ val balance: TalerAmount,
+ val payto_uri: String,
+ val debit_threshold: TalerAmount,
+ val contact_data: ChallengeContactData? = null,
+ val cashout_payto_uri: String? = null,
+ val has_debit: Boolean
+)
+
+// Type of POST /transactions
+@Serializable
+data class BankAccountTransactionCreate(
+ val payto_uri: String,
+ val amount: String
+)
+
+// GET /transactions/T_ID
+@Serializable
+data class BankAccountTransactionInfo(
+ val creditor_payto_uri: String,
+ val debtor_payto_uri: String,
+ val amount: String,
+ val direction: TransactionDirection,
+ val subject: String,
+ val row_id: Long, // is T_ID
+ val date: Long
)
\ No newline at end of file
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
index 0f49daa9..9bb769ea 100644
--- a/util/src/main/kotlin/HTTP.kt
+++ b/util/src/main/kotlin/HTTP.kt
@@ -50,7 +50,7 @@ fun ApplicationRequest.getBaseUrl(): String? {
* Get the URI (path's) component or throw Internal server error.
* @param component the name of the URI component to return.
*/
-fun ApplicationCall.expectUriComponent(name: String): String? {
+fun ApplicationCall.maybeUriComponent(name: String): String? {
val ret: String? = this.parameters[name]
if (ret == null) {
logger.error("Component $name not found in URI")
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.