gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]