commit fb40741be92b179dba8fc416f993c59f1a6850aa
parent 1ce3c4d4251015283d61654be5196c1a9734ca40
Author: Florian Dold <florian@dold.me>
Date: Sun, 24 Sep 2023 14:18:56 +0200
refactoring, adapt to core bank API withdrawal change
Diffstat:
9 files changed, 952 insertions(+), 1013 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt b/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
@@ -0,0 +1,39 @@
+package tech.libeufin.bank
+
+import io.ktor.http.*
+import io.ktor.server.application.*
+import net.taler.common.errorcodes.TalerErrorCode
+import tech.libeufin.util.getAuthorizationDetails
+import tech.libeufin.util.getAuthorizationRawHeader
+
+/**
+ * This function tries to authenticate the call according
+ * to the scheme that is mentioned in the Authorization header.
+ * The allowed schemes are either 'HTTP basic auth' or 'bearer token'.
+ *
+ * requiredScope can be either "readonly" or "readwrite".
+ *
+ * Returns the authenticated customer, or null if they failed.
+ */
+fun ApplicationCall.authenticateBankRequest(db: Database, requiredScope: TokenScope): Customer? {
+ // Extracting the Authorization header.
+ val header = getAuthorizationRawHeader(this.request) ?: throw badRequest(
+ "Authorization header not found.",
+ TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+ )
+ val authDetails = getAuthorizationDetails(header) ?: throw badRequest(
+ "Authorization is invalid.",
+ TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+ )
+ return when (authDetails.scheme) {
+ "Basic" -> doBasicAuth(db, authDetails.content)
+ "Bearer" -> doTokenAuth(db, authDetails.content, requiredScope)
+ else -> throw LibeufinBankException(
+ httpStatus = HttpStatusCode.Unauthorized,
+ talerError = TalerError(
+ code = TalerErrorCode.TALER_EC_GENERIC_UNAUTHORIZED.code,
+ hint = "Authorization method wrong or not supported."
+ )
+ )
+ }
+}
+\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
@@ -0,0 +1,601 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2019 Stanisci and Dold.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.bank
+
+import io.ktor.http.*
+import io.ktor.server.application.*
+import kotlinx.serialization.Contextual
+import kotlinx.serialization.Serializable
+import java.util.*
+
+/**
+ * Allowed lengths for fractional digits in amounts.
+ */
+enum class FracDigits(howMany: Int) {
+ TWO(2),
+ EIGHT(8)
+}
+
+
+/**
+ * Timestamp containing the number of seconds since epoch.
+ */
+@Serializable
+data class Timestamp(
+ val t_s: Long // FIXME (?): not supporting "never" at the moment.
+)
+
+/**
+ * HTTP response type of successful token refresh.
+ * access_token is the Crockford encoding of the 32 byte
+ * access token, whereas 'expiration' is the point in time
+ * when this token expires.
+ */
+@Serializable
+data class TokenSuccessResponse(
+ val access_token: String,
+ val expiration: Timestamp
+)
+
+/**
+ * Error object to respond to the client. The
+ * 'code' field takes values from the GANA gnu-taler-error-code
+ * specification. 'hint' is a human-readable description
+ * of the error.
+ */
+@Serializable
+data class TalerError(
+ val code: Int,
+ val hint: String? = null
+)
+
+/* Contains contact data to send TAN challges to the
+* users, to let them complete cashout operations. */
+@Serializable
+data class ChallengeContactData(
+ val email: String? = null,
+ val phone: String? = null
+)
+
+// Type expected at POST /accounts
+@Serializable
+data class RegisterAccountRequest(
+ val username: String,
+ val password: String,
+ val name: String,
+ val is_public: Boolean = false,
+ val is_taler_exchange: Boolean = false,
+ val challenge_contact_data: ChallengeContactData? = null,
+ // External bank account where to send cashout amounts.
+ val cashout_payto_uri: String? = null,
+ // Bank account internal to Libeufin-Bank.
+ val internal_payto_uri: String? = null
+)
+
+/**
+ * Internal representation of relative times. The
+ * "forever" case is represented with Long.MAX_VALUE.
+ */
+data class RelativeTime(
+ val d_us: Long
+)
+
+/**
+ * Type expected at POST /accounts/{USERNAME}/token
+ * It complies with Taler's design document #49
+ */
+@Serializable
+data class TokenRequest(
+ val scope: TokenScope,
+ @Contextual
+ val duration: RelativeTime? = null,
+ val refreshable: Boolean = false
+)
+
+/**
+ * Convenience type to throw errors along the bank activity
+ * and that is meant to be caught by Ktor and responded to the
+ * client.
+ */
+class LibeufinBankException(
+ // Status code that Ktor will set for the response.
+ val httpStatus: HttpStatusCode,
+ // Error detail object, after Taler API.
+ val talerError: TalerError
+) : Exception(talerError.hint)
+
+/**
+ * Convenience type to hold customer data, typically after such
+ * data gets fetched from the database. It is also used to _insert_
+ * customer data to the database.
+ */
+data class Customer(
+ val login: String,
+ val passwordHash: String,
+ val name: String,
+ /**
+ * Only non-null when this object is defined _by_ the
+ * database.
+ */
+ val dbRowId: Long? = null,
+ val email: String? = null,
+ val phone: String? = null,
+ /**
+ * External bank account where customers send
+ * their cashout amounts.
+ */
+ val cashoutPayto: String? = null,
+ /**
+ * Currency of the external bank account where
+ * customers send their cashout amounts.
+ */
+ val cashoutCurrency: String? = null
+)
+
+/**
+ * Represents a Taler amount. This type can be used both
+ * to hold database records and amounts coming from the parser.
+ * If maybeCurrency is null, then the constructor defaults it
+ * to be the "internal currency". Internal currency is the one
+ * with which Libeufin-Bank moves funds within itself, therefore
+ * not to be mistaken with the cashout currency, which is the one
+ * that gets credited to Libeufin-Bank users to their cashout_payto_uri.
+ *
+ * maybeCurrency is typically null when the TalerAmount object gets
+ * defined by the Database class.
+ */
+class TalerAmount(
+ val value: Long,
+ val frac: Int,
+ val currency: String
+) {
+ override fun equals(other: Any?): Boolean {
+ return other is TalerAmount &&
+ other.value == this.value &&
+ other.frac == this.frac &&
+ other.currency == this.currency
+ }
+
+ override fun toString(): String {
+ return "$currency:$value.$frac"
+ }
+}
+
+/**
+ * Convenience type to get and set bank account information
+ * from/to the database.
+ */
+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,
+ /**
+ * Because bank accounts MAY be funded by an external currency,
+ * local bank accounts need to query Nexus, in order to find this
+ * out. This field is a pointer to the latest incoming payment that
+ * was contained in a Nexus history response.
+ *
+ * Typically, the 'admin' bank account uses this field, in order
+ * to initiate Taler withdrawals that depend on an external currency
+ * being wired by wallet owners.
+ */
+ val lastNexusFetchRowId: Long = 0L,
+ val balance: TalerAmount? = null, // null when a new bank account gets created.
+ val hasDebt: Boolean,
+ val maxDebt: TalerAmount
+)
+
+// Allowed values for bank transactions directions.
+enum class TransactionDirection {
+ credit,
+ debit
+}
+
+// Allowed values for cashout TAN channels.
+enum class TanChannel {
+ sms,
+ email,
+ file // Writes cashout TANs to /tmp, for testing.
+}
+
+// Scopes for authentication tokens.
+enum class TokenScope {
+ readonly,
+ readwrite,
+ refreshable // Not spec'd as a scope!
+}
+
+/**
+ * Convenience type to set/get authentication tokens to/from
+ * the database.
+ */
+data class BearerToken(
+ val content: ByteArray,
+ val scope: TokenScope,
+ val isRefreshable: Boolean = false,
+ val creationTime: Long,
+ val expirationTime: Long,
+ /**
+ * Serial ID of the database row that hosts the bank customer
+ * that is associated with this token. NOTE: if the token is
+ * refreshed by a client that doesn't have a user+password login
+ * in the system, the creator remains always the original bank
+ * customer that created the very first token.
+ */
+ val bankCustomer: Long
+)
+
+/**
+ * Convenience type to _communicate_ a bank transfer to the
+ * database procedure, NOT representing therefore any particular
+ * table. The procedure will then retrieve all the tables data
+ * from this type.
+ */
+data class BankInternalTransaction(
+ // Database row ID of the internal bank account sending the payment.
+ val creditorAccountId: Long,
+ // Database row ID of the internal bank account receiving the payment.
+ val debtorAccountId: Long,
+ val subject: String,
+ val amount: TalerAmount,
+ val transactionDate: Long,
+ val accountServicerReference: String = "not used", // ISO20022
+ val endToEndId: String = "not used", // ISO20022
+ val paymentInformationId: String = "not used" // ISO20022
+)
+
+/**
+ * Convenience type representing bank transactions as they
+ * are in the respective database table. Only used to _get_
+ * the information from the database.
+ */
+data class BankAccountTransaction(
+ val creditorPaytoUri: String,
+ val creditorName: String,
+ val debtorPaytoUri: String,
+ val debtorName: String,
+ val subject: String,
+ val amount: TalerAmount,
+ val transactionDate: Long, // microseconds
+ /**
+ * Is the transaction debit, or credit for the
+ * bank account pointed by this object?
+ */
+ val direction: TransactionDirection,
+ /**
+ * database row ID of the bank account that is
+ * impacted by the direction. For example, if the
+ * direction is debit, then this value points to the
+ * bank account of the payer.
+ */
+ val bankAccountId: Long,
+ // Null if this type is used to _create_ one transaction.
+ val dbRowId: Long? = null,
+ // Following are ISO20022 specific.
+ val accountServicerReference: String,
+ val paymentInformationId: String,
+ val endToEndId: String,
+)
+
+/**
+ * Represents a Taler withdrawal operation, as it is
+ * stored in the respective database table.
+ */
+data class TalerWithdrawalOperation(
+ val withdrawalUuid: UUID,
+ val amount: TalerAmount,
+ val selectionDone: Boolean = false,
+ val aborted: Boolean = false,
+ val confirmationDone: Boolean = false,
+ val reservePub: String?,
+ val selectedExchangePayto: String?,
+ val walletBankAccount: Long
+)
+
+/**
+ * Represents a cashout operation, as it is stored
+ * in the respective database table.
+ */
+data class Cashout(
+ val cashoutUuid: UUID,
+ val localTransaction: Long? = null,
+ val amountDebit: TalerAmount,
+ val amountCredit: TalerAmount,
+ val buyAtRatio: Int,
+ val buyInFee: TalerAmount,
+ val sellAtRatio: Int,
+ val sellOutFee: TalerAmount,
+ val subject: String,
+ val creationTime: Long,
+ val tanConfirmationTime: Long? = null,
+ val tanChannel: TanChannel,
+ val tanCode: String,
+ 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
+)
+
+@Serializable
+data class Balance(
+ // FIXME: Should not be a string
+ val amount: String,
+ // FIXME: Should not be a string
+ val credit_debit_indicator: String,
+)
+
+/**
+ * GET /accounts/$USERNAME response.
+ */
+@Serializable
+data class AccountData(
+ val name: String,
+ val balance: Balance,
+ val payto_uri: String,
+ val debit_threshold: String,
+ val contact_data: ChallengeContactData? = null,
+ val cashout_payto_uri: String? = null,
+)
+
+/**
+ * Response type of corebank API transaction initiation.
+ */
+@Serializable
+data class BankAccountTransactionCreate(
+ val payto_uri: String,
+ val amount: String
+)
+
+/* History element, either from GET /transactions/T_ID
+ or from GET /transactions */
+@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
+)
+
+// Response type for histories, namely GET /transactions
+@Serializable
+data class BankAccountTransactionsResponse(
+ val transactions: MutableList<BankAccountTransactionInfo>
+)
+
+// Taler withdrawal request.
+@Serializable
+data class BankAccountCreateWithdrawalRequest(
+ val amount: String
+)
+
+// Taler withdrawal response.
+@Serializable
+data class BankAccountCreateWithdrawalResponse(
+ val withdrawal_id: String,
+ val taler_withdraw_uri: String
+)
+
+// Taler withdrawal details response
+@Serializable
+data class BankAccountGetWithdrawalResponse(
+ val amount: String,
+ val aborted: Boolean,
+ val confirmation_done: Boolean,
+ val selection_done: Boolean,
+ val selected_reserve_pub: String? = null,
+ val selected_exchange_account: String? = null
+)
+
+typealias ResourceName = String
+
+/**
+ * Checks if the input Customer has the rights over ResourceName.
+ * FIXME: myAuth() gives null on failures, but this gives false.
+ * Should they return the same, for consistency?
+ */
+fun ResourceName.canI(c: Customer, withAdmin: Boolean = true): Boolean {
+ if (c.login == this) return true
+ if (c.login == "admin" && withAdmin) return true
+ return false
+}
+
+/**
+ * Factors out the retrieval of the resource name from
+ * the URI. The resource looked for defaults to "USERNAME"
+ * as this is frequently mentioned resource along the endpoints.
+ *
+ * This helper is recommended because it returns a ResourceName
+ * type that then offers the ".canI()" helper to check if the user
+ * has the rights on the resource.
+ */
+fun ApplicationCall.getResourceName(param: String): ResourceName =
+ this.expectUriComponent(param)
+
+/**
+ * This type communicates the result of a database operation
+ * to confirm one withdrawal operation.
+ */
+enum class WithdrawalConfirmationResult {
+ SUCCESS,
+ OP_NOT_FOUND,
+ EXCHANGE_NOT_FOUND,
+ BALANCE_INSUFFICIENT,
+
+ /**
+ * This state indicates that the withdrawal was already
+ * confirmed BUT Kotlin did not detect it and still invoked
+ * the SQL procedure to confirm the withdrawal. This is
+ * conflictual because only Kotlin is responsible to check
+ * for idempotency, and this state witnesses a failure in
+ * this regard.
+ */
+ CONFLICT
+}
+
+// GET /config response from the Taler Integration API.
+@Serializable
+data class TalerIntegrationConfigResponse(
+ val name: String = "taler-bank-integration",
+ val version: String = "0:0:0:",
+ val currency: String
+)
+
+/**
+ * Withdrawal status as specified in the Taler Integration API.
+ */
+@Serializable
+data class BankWithdrawalOperationStatus(
+ // Indicates whether the withdrawal was aborted.
+ val aborted: Boolean,
+
+ /* Has the wallet selected parameters for the withdrawal operation
+ (exchange and reserve public key) and successfully sent it
+ to the bank? */
+ val selection_done: Boolean,
+
+ /* The transfer has been confirmed and registered by the bank.
+ Does not guarantee that the funds have arrived at the exchange
+ already. */
+ val transfer_done: Boolean,
+
+ /* Amount that will be withdrawn with this operation
+ (raw amount without fee considerations). */
+ val amount: String,
+
+ /* Bank account of the customer that is withdrawing, as a
+ ``payto`` URI. */
+ val sender_wire: String? = null,
+
+ // Suggestion for an exchange given by the bank.
+ val suggested_exchange: String? = null,
+
+ /* URL that the user needs to navigate to in order to
+ complete some final confirmation (e.g. 2FA).
+ It may contain withdrawal operation id */
+ val confirm_transfer_url: String? = null,
+
+ // Wire transfer types supported by the bank.
+ val wire_types: MutableList<String> = mutableListOf("iban")
+)
+
+/**
+ * Selection request on a Taler withdrawal.
+ */
+@Serializable
+data class BankWithdrawalOperationPostRequest(
+ val reserve_pub: String,
+ val selected_exchange: String,
+)
+
+/**
+ * Response to the wallet after it selects the exchange
+ * and the reserve pub.
+ */
+@Serializable
+data class BankWithdrawalOperationPostResponse(
+ val transfer_done: Boolean,
+ val confirm_transfer_url: String? = null
+)
+
+/**
+ * Request to an /admin/add-incoming request from
+ * the Taler Wire Gateway API.
+ */
+@Serializable
+data class AddIncomingRequest(
+ val amount: String,
+ val reserve_pub: String,
+ val debit_account: String
+)
+
+/**
+ * Response to /admin/add-incoming
+ */
+@Serializable
+data class AddIncomingResponse(
+ val timestamp: Long,
+ val row_id: Long
+)
+
+@Serializable
+data class TWGConfigResponse(
+ val name: String = "taler-wire-gateway",
+ val version: String = "0:0:0:",
+ val currency: String
+)
+
+/**
+ * Response of a TWG /history/incoming call.
+ */
+@Serializable
+data class IncomingHistory(
+ val incoming_transactions: MutableList<IncomingReserveTransaction> = mutableListOf(),
+ val credit_account: String // Receiver's Payto URI.
+)
+
+/**
+ * TWG's incoming payment record.
+ */
+@Serializable
+data class IncomingReserveTransaction(
+ val type: String = "RESERVE",
+ val row_id: Long, // DB row ID of the payment.
+ val date: Long, // microseconds timestamp.
+ val amount: String,
+ val debit_account: String, // Payto of the sender.
+ val reserve_pub: String
+)
+
+/**
+ * TWG's request to pay a merchant.
+ */
+@Serializable
+data class TransferRequest(
+ val request_uid: String,
+ @Contextual
+ val amount: TalerAmount,
+ val exchange_base_url: String,
+ val wtid: String,
+ val credit_account: String
+)
+
+/**
+ * TWG's response to merchant payouts
+ */
+@Serializable
+data class TransferResponse(
+ val timestamp: Long,
+ val row_id: Long
+)
+\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
@@ -24,15 +24,13 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
delete("/accounts/{USERNAME}/token") {
throw internalServerError("Token deletion not implemented.")
}
-
+
post("/accounts/{USERNAME}/token") {
- val customer = call.myAuth(db, TokenScope.refreshable) ?: throw unauthorized("Authentication failed")
+ val customer = call.authenticateBankRequest(db, 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
- )
+ 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>()
/**
@@ -44,33 +42,29 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
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
- )
+ 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)
+ Random().nextBytes(this)
}
val maxDurationTime: Long = ctx.maxAuthTokenDurationUs
- if (req.duration != null && req.duration.d_us > maxDurationTime)
- 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
+ if (req.duration != null && req.duration.d_us > maxDurationTime) 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(
"Could not get 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
- )
+ 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,
@@ -79,12 +73,10 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
scope = req.scope,
isRefreshable = req.refreshable
)
- if (!db.bearerTokenCreate(token))
- throw internalServerError("Failed at inserting new token in the database")
+ if (!db.bearerTokenCreate(token)) throw internalServerError("Failed at inserting new token in the database")
call.respond(
TokenSuccessResponse(
- access_token = Base32Crockford.encode(tokenBytes),
- expiration = Timestamp(
+ access_token = Base32Crockford.encode(tokenBytes), expiration = Timestamp(
t_s = expirationTimestampUs / 1000000L
)
)
@@ -95,27 +87,23 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
post("/accounts") {
// check if only admin is allowed to create new accounts
if (ctx.restrictRegistration) {
- val customer: Customer? = call.myAuth(db, 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."
- )
+ val customer: Customer? = call.authenticateBankRequest(db, 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."
- )
+ 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 idempotency.
val maybeCustomerExists = db.customerGetFromLogin(req.username)
// Can be null if previous call crashed before completion.
@@ -124,24 +112,19 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
db.bankAccountGetFromOwnerId(this.expectRowId())
}
if (maybeCustomerExists != null && maybeHasBankAccount != null) {
- tech.libeufin.bank.logger.debug("Registering username was found: ${maybeCustomerExists.login}")
+ 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
+ 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(
+ httpStatus = HttpStatusCode.Conflict, talerError = TalerError(
code = GENERIC_UNDEFINED, // GANA needs this.
hint = "Idempotency check failed."
)
@@ -159,8 +142,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
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
+ ?: 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 = ctx.defaultCustomerDebtLimit
@@ -182,8 +164,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
*/
val bonusAmount = if (ctx.registrationBonusEnabled) ctx.registrationBonus else null
if (bonusAmount != null) {
- val adminCustomer = db.customerGetFromLogin("admin")
- ?: throw internalServerError("Admin customer not found")
+ val adminCustomer =
+ db.customerGetFromLogin("admin") ?: throw internalServerError("Admin customer not found")
val adminBankAccount = db.bankAccountGetFromOwnerId(adminCustomer.expectRowId())
?: throw internalServerError("Admin bank account not found")
val adminPaysBonus = BankInternalTransaction(
@@ -193,111 +175,104 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
subject = "Registration bonus.",
transactionDate = getNowUs()
)
- when(db.bankTransactionCreate(adminPaysBonus)) {
- Database.BankTransactionResult.NO_CREDITOR ->
- throw internalServerError("Bonus impossible: creditor not found, despite its recent creation.")
- Database.BankTransactionResult.NO_DEBTOR ->
- throw internalServerError("Bonus impossible: admin not found.")
- Database.BankTransactionResult.CONFLICT ->
- throw internalServerError("Bonus impossible: admin has insufficient balance.")
- Database.BankTransactionResult.SUCCESS -> {/* continue the execution */}
+ when (db.bankTransactionCreate(adminPaysBonus)) {
+ Database.BankTransactionResult.NO_CREDITOR -> throw internalServerError("Bonus impossible: creditor not found, despite its recent creation.")
+
+ Database.BankTransactionResult.NO_DEBTOR -> throw internalServerError("Bonus impossible: admin not found.")
+
+ Database.BankTransactionResult.CONFLICT -> throw internalServerError("Bonus impossible: admin has insufficient balance.")
+
+ Database.BankTransactionResult.SUCCESS -> {/* continue the execution */
+ }
}
}
call.respond(HttpStatusCode.Created)
return@post
}
+
get("/accounts/{USERNAME}") {
- val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized("Login failed")
+ val c = call.authenticateBankRequest(db, 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
+ 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)
+ 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.'")
+ val balance = Balance(
+ amount = bankAccountData.balance.toString(),
+ credit_debit_indicator = if (bankAccountData.hasDebt) { "debit" } else { "credit" }
+ )
+ call.respond(
+ AccountData(
+ name = customerData.name,
+ balance = balance,
+ debit_threshold = bankAccountData.maxDebt.toString(),
+ payto_uri = bankAccountData.internalPaytoUri,
+ contact_data = ChallengeContactData(
+ email = customerData.email, phone = customerData.phone
+ ),
+ cashout_payto_uri = customerData.cashoutPayto,
)
- 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.toString(),
- debit_threshold = bankAccountData.maxDebt.toString(),
- payto_uri = bankAccountData.internalPaytoUri,
- contact_data = ChallengeContactData(
- email = customerData.email,
- phone = customerData.phone
- ),
- cashout_payto_uri = customerData.cashoutPayto,
- has_debit = bankAccountData.hasDebt
- ))
+ )
return@get
}
post("/accounts/{USERNAME}/withdrawals") {
- val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
+ val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized()
// Admin not allowed to withdraw in the name of customers:
val accountName = call.expectUriComponent("USERNAME")
- if (c.login != accountName)
- throw unauthorized("User ${c.login} not allowed to withdraw for account '${accountName}'")
+ if (c.login != accountName) throw unauthorized("User ${c.login} not allowed to withdraw for account '${accountName}'")
val req = call.receive<BankAccountCreateWithdrawalRequest>()
// Checking that the user has enough funds.
val b = db.bankAccountGetFromOwnerId(c.expectRowId())
?: throw internalServerError("Customer '${c.login}' lacks bank account.")
val withdrawalAmount = parseTalerAmount(req.amount)
- if (
- !isBalanceEnough(
- balance = b.expectBalance(),
- due = withdrawalAmount,
- maxDebt = b.maxDebt,
- hasBalanceDebt = b.hasDebt
- ))
- throw forbidden(
- hint = "Insufficient funds to withdraw with Taler",
- talerErrorCode = TalerErrorCode.TALER_EC_NONE // FIXME: need EC.
+ if (!isBalanceEnough(
+ balance = b.expectBalance(), due = withdrawalAmount, maxDebt = b.maxDebt, hasBalanceDebt = b.hasDebt
)
+ ) throw forbidden(
+ hint = "Insufficient funds to withdraw with Taler",
+ talerErrorCode = TalerErrorCode.TALER_EC_NONE // FIXME: need EC.
+ )
// Auth and funds passed, create the operation now!
val opId = UUID.randomUUID()
- if(
- !db.talerWithdrawalCreate(
- opId,
- b.expectRowId(),
- withdrawalAmount
+ if (!db.talerWithdrawalCreate(
+ opId, b.expectRowId(), withdrawalAmount
)
- )
- throw internalServerError("Bank failed at creating the withdraw operation.")
+ ) throw internalServerError("Bank failed at creating the withdraw operation.")
- val bankBaseUrl = call.request.getBaseUrl()
- ?: throw internalServerError("Bank could not find its own base URL")
- call.respond(BankAccountCreateWithdrawalResponse(
- withdrawal_id = opId.toString(),
- taler_withdraw_uri = getTalerWithdrawUri(bankBaseUrl, opId.toString())
- ))
+ val bankBaseUrl = call.request.getBaseUrl() ?: throw internalServerError("Bank could not find its own base URL")
+ call.respond(
+ BankAccountCreateWithdrawalResponse(
+ withdrawal_id = opId.toString(), taler_withdraw_uri = getTalerWithdrawUri(bankBaseUrl, opId.toString())
+ )
+ )
return@post
}
- get("/accounts/{USERNAME}/withdrawals/{withdrawal_id}") {
- val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
- val accountName = call.expectUriComponent("USERNAME")
- // Admin allowed to see the details
- if (c.login != accountName && c.login != "admin") throw forbidden()
- // Permissions passed, get the information.
+
+ get("/withdrawals/{withdrawal_id}") {
val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
- call.respond(BankAccountGetWithdrawalResponse(
- amount = op.amount.toString(),
- aborted = op.aborted,
- confirmation_done = op.confirmationDone,
- selection_done = op.selectionDone,
- selected_exchange_account = op.selectedExchangePayto,
- selected_reserve_pub = op.reservePub
- ))
+ call.respond(
+ BankAccountGetWithdrawalResponse(
+ amount = op.amount.toString(),
+ aborted = op.aborted,
+ confirmation_done = op.confirmationDone,
+ selection_done = op.selectionDone,
+ selected_exchange_account = op.selectedExchangePayto,
+ selected_reserve_pub = op.reservePub
+ )
+ )
return@get
}
- post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/abort") {
- val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
+
+ post("/withdrawals/{withdrawal_id}/abort") {
+ val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw unauthorized()
// Admin allowed to abort.
if (!call.getResourceName("USERNAME").canI(c)) throw forbidden()
val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
@@ -308,52 +283,47 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
}
// Op is found, it'll now fail only if previously confirmed (DB checks).
if (!db.talerWithdrawalAbort(op.withdrawalUuid)) throw conflict(
- hint = "Cannot abort confirmed withdrawal",
- talerEc = TalerErrorCode.TALER_EC_END
+ hint = "Cannot abort confirmed withdrawal", talerEc = TalerErrorCode.TALER_EC_END
)
call.respondText("{}", ContentType.Application.Json)
return@post
}
- post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/confirm") {
- val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
+
+ post("/withdrawals/{withdrawal_id}/confirm") {
+ val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized()
// No admin allowed.
- if(!call.getResourceName("USERNAME").canI(c, withAdmin = false)) throw forbidden()
+ if (!call.getResourceName("USERNAME").canI(c, withAdmin = false)) throw forbidden()
val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
// Checking idempotency:
if (op.confirmationDone) {
call.respondText("{}", ContentType.Application.Json)
return@post
}
- if (op.aborted)
- throw conflict(
- hint = "Cannot confirm an aborted withdrawal",
- talerEc = TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
- )
+ if (op.aborted) throw conflict(
+ hint = "Cannot confirm an aborted withdrawal", talerEc = TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
+ )
// Checking that reserve GOT indeed selected.
- if (!op.selectionDone)
- throw LibeufinBankException(
- httpStatus = HttpStatusCode.UnprocessableEntity,
- talerError = TalerError(
- hint = "Cannot confirm an unselected withdrawal",
- code = TalerErrorCode.TALER_EC_END.code
- ))
- /* Confirmation conditions are all met, now put the operation
+ if (!op.selectionDone) throw LibeufinBankException(
+ httpStatus = HttpStatusCode.UnprocessableEntity, talerError = TalerError(
+ hint = "Cannot confirm an unselected withdrawal", code = TalerErrorCode.TALER_EC_END.code
+ )
+ )/* Confirmation conditions are all met, now put the operation
* to the selected state _and_ wire the funds to the exchange.
* Note: 'when' helps not to omit more result codes, should more
* be added.
*/
when (db.talerWithdrawalConfirm(op.withdrawalUuid, getNowUs())) {
- WithdrawalConfirmationResult.BALANCE_INSUFFICIENT ->
- throw conflict(
- "Insufficient funds",
- TalerErrorCode.TALER_EC_END // FIXME: define EC for this.
- )
+ WithdrawalConfirmationResult.BALANCE_INSUFFICIENT -> throw conflict(
+ "Insufficient funds", TalerErrorCode.TALER_EC_END // FIXME: define EC for this.
+ )
+
WithdrawalConfirmationResult.OP_NOT_FOUND ->
/**
* Despite previous checks, the database _still_ did not
* find the withdrawal operation, that's on the bank.
*/
throw internalServerError("Withdrawal operation (${op.withdrawalUuid}) not found")
+
WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND ->
/**
* That can happen because the bank did not check the exchange
@@ -361,22 +331,20 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
* bank account got removed before this confirmation.
*/
throw conflict(
- hint = "Exchange to withdraw from not found",
- talerEc = TalerErrorCode.TALER_EC_END // FIXME
- )
- WithdrawalConfirmationResult.CONFLICT ->
- throw internalServerError("Bank didn't check for idempotency")
- WithdrawalConfirmationResult.SUCCESS ->
- call.respondText(
- "{}",
- ContentType.Application.Json
+ hint = "Exchange to withdraw from not found", talerEc = TalerErrorCode.TALER_EC_END // FIXME
)
+
+ WithdrawalConfirmationResult.CONFLICT -> throw internalServerError("Bank didn't check for idempotency")
+
+ WithdrawalConfirmationResult.SUCCESS -> call.respondText(
+ "{}", ContentType.Application.Json
+ )
}
return@post
}
get("/accounts/{USERNAME}/transactions") {
- val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
+ val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw unauthorized()
val resourceName = call.expectUriComponent("USERNAME")
if (c.login != resourceName && c.login != "admin") throw forbidden()
// Collecting params.
@@ -386,34 +354,34 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
?: throw internalServerError("Customer '${c.login}' lacks bank account.")
val bankAccountId = bankAccount.expectRowId()
val history: List<BankAccountTransaction> = db.bankTransactionGetHistory(
- start = historyParams.start,
- delta = historyParams.delta,
- bankAccountId = bankAccountId
+ start = historyParams.start, delta = historyParams.delta, bankAccountId = bankAccountId
)
val res = BankAccountTransactionsResponse(transactions = mutableListOf())
history.forEach {
- res.transactions.add(BankAccountTransactionInfo(
- debtor_payto_uri = it.debtorPaytoUri,
- creditor_payto_uri = it.creditorPaytoUri,
- subject = it.subject,
- amount = it.amount.toString(),
- direction = it.direction,
- date = it.transactionDate,
- row_id = it.dbRowId ?: throw internalServerError(
- "Transaction timestamped with '${it.transactionDate}' did not have row ID"
+ res.transactions.add(
+ BankAccountTransactionInfo(
+ debtor_payto_uri = it.debtorPaytoUri,
+ creditor_payto_uri = it.creditorPaytoUri,
+ subject = it.subject,
+ amount = it.amount.toString(),
+ direction = it.direction,
+ date = it.transactionDate,
+ row_id = it.dbRowId ?: throw internalServerError(
+ "Transaction timestamped with '${it.transactionDate}' did not have row ID"
+ )
)
- ))
+ )
}
call.respond(res)
return@get
}
+
// Creates a bank transaction.
post("/accounts/{USERNAME}/transactions") {
- val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
+ val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized()
val resourceName = call.expectUriComponent("USERNAME")
// admin has no rights here.
- if ((c.login != resourceName) && (call.getAuthToken() == null))
- throw forbidden()
+ if ((c.login != resourceName) && (call.getAuthToken() == null)) throw forbidden()
val txData = call.receive<BankAccountTransactionCreate>()
// FIXME: make payto parser IBAN-agnostic?
val payto = parsePayto(txData.payto_uri) ?: throw badRequest("Invalid creditor Payto")
@@ -422,17 +390,13 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
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(paytoWithoutParams)
- ?: throw notFound(
- "Creditor account not found",
- TalerErrorCode.TALER_EC_END // FIXME: define this EC.
- )
+ val creditorCustomerData = db.bankAccountGetFromInternalPayto(paytoWithoutParams) ?: throw notFound(
+ "Creditor account not found", TalerErrorCode.TALER_EC_END // FIXME: define this EC.
+ )
val amount = parseTalerAmount(txData.amount)
- if (amount.currency != ctx.currency)
- throw badRequest(
- "Wrong currency: ${amount.currency}",
- talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
- )
+ if (amount.currency != ctx.currency) throw badRequest(
+ "Wrong currency: ${amount.currency}", talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
+ )
val dbInstructions = BankInternalTransaction(
debtorAccountId = debtorId,
creditorAccountId = creditorCustomerData.owningCustomerId,
@@ -441,26 +405,25 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
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.")
+ 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(db, TokenScope.readonly) ?: throw unauthorized()
+ val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw unauthorized()
val accountOwner = call.expectUriComponent("USERNAME")
// auth ok, check rights.
- if (c.login != "admin" && c.login != accountOwner)
- throw forbidden()
+ if (c.login != "admin" && c.login != accountOwner) throw forbidden()
// rights ok, check tx exists.
val tId = call.expectUriComponent("T_ID")
val txRowId = try {
@@ -470,25 +433,24 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
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 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")
+ 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
- ))
+ 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/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -165,38 +165,6 @@ object TalerAmountSerializer : KSerializer<TalerAmount> {
}
}
-/**
- * This function tries to authenticate the call according
- * to the scheme that is mentioned in the Authorization header.
- * The allowed schemes are either 'HTTP basic auth' or 'bearer token'.
- *
- * requiredScope can be either "readonly" or "readwrite".
- *
- * Returns the authenticated customer, or null if they failed.
- */
-fun ApplicationCall.myAuth(db: Database, requiredScope: TokenScope): Customer? {
- // Extracting the Authorization header.
- val header = getAuthorizationRawHeader(this.request) ?: throw badRequest(
- "Authorization header not found.",
- TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
- )
- val authDetails = getAuthorizationDetails(header) ?: throw badRequest(
- "Authorization is invalid.",
- TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
- )
- return when (authDetails.scheme) {
- "Basic" -> doBasicAuth(db, authDetails.content)
- "Bearer" -> doTokenAuth(db, authDetails.content, requiredScope)
- else -> throw LibeufinBankException(
- httpStatus = HttpStatusCode.Unauthorized,
- talerError = TalerError(
- code = TalerErrorCode.TALER_EC_GENERIC_UNAUTHORIZED.code,
- hint = "Authorization method wrong or not supported."
- )
- )
- }
-}
-
/**
* Set up web server handlers for the Taler corebank API.
@@ -408,7 +376,7 @@ fun readBankApplicationContextFromConfig(cfg: TalerConfig): BankApplicationConte
class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") {
private val configFile by option(
- "--config",
+ "--config", "-c",
help = "set the configuration file"
)
init {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
@@ -34,8 +34,9 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext)
call.respond(TWGConfigResponse(currency = ctx.currency))
return@get
}
+
get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") {
- val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
+ val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw unauthorized()
if (!call.getResourceName("USERNAME").canI(c, withAdmin = true)) throw forbidden()
val params = getHistoryParams(call.request)
val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
@@ -66,8 +67,9 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext)
call.respond(resp)
return@get
}
+
post("/accounts/{USERNAME}/taler-wire-gateway/transfer") {
- val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
+ val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized()
if (!call.getResourceName("USERNAME").canI(c, withAdmin = false)) throw forbidden()
val req = call.receive<TransferRequest>()
// Checking for idempotency.
@@ -120,8 +122,9 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext)
))
return@post
}
+
post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
- val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
+ val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized()
if (!call.getResourceName("USERNAME").canI(c, withAdmin = false)) throw forbidden()
val req = call.receive<AddIncomingRequest>()
val amount = parseTalerAmount(req.amount)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -20,7 +20,6 @@
package tech.libeufin.bank
import io.ktor.http.*
-import io.ktor.http.cio.*
import io.ktor.server.application.*
import io.ktor.server.plugins.*
import io.ktor.server.request.*
@@ -30,10 +29,8 @@ import net.taler.wallet.crypto.Base32Crockford
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.util.*
-import java.lang.NumberFormatException
import java.net.URL
import java.util.*
-import kotlin.system.exitProcess
const val FRACTION_BASE = 100000000
@@ -41,23 +38,20 @@ private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.helpers
fun ApplicationCall.expectUriComponent(componentName: String) =
this.maybeUriComponent(componentName) ?: throw badRequest(
- hint = "No username found in the URI",
- talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MISSING
-)
+ 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? {
val h = getAuthorizationRawHeader(this.request) ?: return null
val authDetails = getAuthorizationDetails(h) ?: throw badRequest(
- "Authorization header is malformed.",
+ "Authorization header is malformed.", TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+ )
+ if (authDetails.scheme == "Bearer") return splitBearerToken(authDetails.content) ?: throw throw badRequest(
+ "Authorization header is malformed (could not strip the prefix from Bearer token).",
TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
)
- if (authDetails.scheme == "Bearer")
- return splitBearerToken(authDetails.content) ?: throw
- throw badRequest(
- "Authorization header is malformed (could not strip the prefix from Bearer token).",
- TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
- )
return null // Not a Bearer token case.
}
@@ -76,14 +70,12 @@ fun doBasicAuth(db: Database, encodedCredentials: String): Customer? {
*/
limit = 2
)
- if (userAndPassSplit.size != 2)
- throw LibeufinBankException(
- httpStatus = HttpStatusCode.BadRequest,
- talerError = TalerError(
- code = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED.code,
- "Malformed Basic auth credentials found in the Authorization header."
- )
+ if (userAndPassSplit.size != 2) throw LibeufinBankException(
+ httpStatus = HttpStatusCode.BadRequest, talerError = TalerError(
+ code = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED.code,
+ "Malformed Basic auth credentials found in the Authorization header."
)
+ )
val login = userAndPassSplit[0]
val plainPassword = userAndPassSplit[1]
val maybeCustomer = db.customerGetFromLogin(login) ?: throw unauthorized()
@@ -111,15 +103,13 @@ fun doTokenAuth(
requiredScope: TokenScope,
): Customer? {
val bareToken = splitBearerToken(token) ?: throw badRequest(
- "Bearer token malformed",
- talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+ "Bearer token malformed", talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
)
val tokenBytes = try {
Base32Crockford.decode(bareToken)
} catch (e: Exception) {
throw badRequest(
- e.message,
- TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+ e.message, TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
)
}
val maybeToken: BearerToken? = db.bearerTokenGet(tokenBytes)
@@ -140,87 +130,67 @@ fun doTokenAuth(
return null
}
// Getting the related username.
- return db.customerGetFromRowId(maybeToken.bankCustomer)
- ?: throw LibeufinBankException(
- httpStatus = HttpStatusCode.InternalServerError,
- talerError = TalerError(
- code = TalerErrorCode.TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE.code,
- hint = "Customer not found, despite token mentions it.",
- ))
+ return db.customerGetFromRowId(maybeToken.bankCustomer) ?: throw LibeufinBankException(
+ httpStatus = HttpStatusCode.InternalServerError, talerError = TalerError(
+ code = TalerErrorCode.TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE.code,
+ hint = "Customer not found, despite token mentions it.",
+ )
+ )
}
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(
- code = talerErrorCode.code,
- hint = hint
- )
+): LibeufinBankException = LibeufinBankException(
+ httpStatus = HttpStatusCode.Forbidden, talerError = TalerError(
+ code = talerErrorCode.code, hint = hint
)
+)
-fun unauthorized(hint: String = "Login failed"): LibeufinBankException =
- LibeufinBankException(
- httpStatus = HttpStatusCode.Unauthorized,
- talerError = TalerError(
- code = TalerErrorCode.TALER_EC_BANK_LOGIN_FAILED.code,
- hint = hint
- )
+fun unauthorized(hint: String = "Login failed"): LibeufinBankException = LibeufinBankException(
+ httpStatus = HttpStatusCode.Unauthorized, talerError = TalerError(
+ code = TalerErrorCode.TALER_EC_BANK_LOGIN_FAILED.code, hint = hint
)
-fun internalServerError(hint: String?): LibeufinBankException =
- LibeufinBankException(
- httpStatus = HttpStatusCode.InternalServerError,
- talerError = TalerError(
- code = TalerErrorCode.TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE.code,
- hint = hint
- )
+)
+
+fun internalServerError(hint: String?): LibeufinBankException = LibeufinBankException(
+ httpStatus = HttpStatusCode.InternalServerError, talerError = TalerError(
+ code = TalerErrorCode.TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE.code, hint = hint
)
+)
fun notFound(
- hint: String?,
- talerEc: TalerErrorCode
-): LibeufinBankException =
- LibeufinBankException(
- httpStatus = HttpStatusCode.NotFound,
- talerError = TalerError(
- code = talerEc.code,
- hint = hint
- )
+ 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
- )
+ 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
-): LibeufinBankException =
- LibeufinBankException(
- httpStatus = HttpStatusCode.BadRequest,
- talerError = TalerError(
- code = talerErrorCode.code,
- hint = hint
- )
+ hint: String? = null, talerErrorCode: TalerErrorCode = TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID
+): LibeufinBankException = LibeufinBankException(
+ httpStatus = HttpStatusCode.BadRequest, talerError = TalerError(
+ code = talerErrorCode.code, hint = hint
)
+)
+
// Generates a new Payto-URI with IBAN scheme.
fun genIbanPaytoUri(): String = "payto://iban/SANDBOXX/${getIban()}"
// Parses Taler amount, returning null if the input is invalid.
fun parseTalerAmount2(
- amount: String,
- fracDigits: FracDigits
+ amount: String, fracDigits: FracDigits
): TalerAmount? {
val format = when (fracDigits) {
FracDigits.TWO -> "^([A-Z]+):([0-9]+)(\\.[0-9][0-9]?)?$"
@@ -246,11 +216,10 @@ fun parseTalerAmount2(
return null
}
return TalerAmount(
- value = value,
- frac = fraction,
- currency = match.destructured.component1()
+ value = value, frac = fraction, currency = match.destructured.component1()
)
}
+
/**
* This helper takes the serialized version of a Taler Amount
* type and parses it into Libeufin's internal representation.
@@ -260,16 +229,13 @@ fun parseTalerAmount2(
* responded to the client.
*/
fun parseTalerAmount(
- amount: String,
- fracDigits: FracDigits = FracDigits.EIGHT
+ amount: String, fracDigits: FracDigits = FracDigits.EIGHT
): TalerAmount {
- val maybeAmount = parseTalerAmount2(amount, fracDigits)
- ?: throw LibeufinBankException(
- httpStatus = HttpStatusCode.BadRequest,
- talerError = TalerError(
- code = TalerErrorCode.TALER_EC_BANK_BAD_FORMAT_AMOUNT.code,
- hint = "Invalid amount: $amount"
- ))
+ val maybeAmount = parseTalerAmount2(amount, fracDigits) ?: throw LibeufinBankException(
+ httpStatus = HttpStatusCode.BadRequest, talerError = TalerError(
+ code = TalerErrorCode.TALER_EC_BANK_BAD_FORMAT_AMOUNT.code, hint = "Invalid amount: $amount"
+ )
+ )
return maybeAmount
}
@@ -278,9 +244,7 @@ private fun normalizeAmount(amt: TalerAmount): TalerAmount {
val normalValue = amt.value + (amt.frac / FRACTION_BASE)
val normalFrac = amt.frac % FRACTION_BASE
return TalerAmount(
- value = normalValue,
- frac = normalFrac,
- currency = amt.currency
+ value = normalValue, frac = normalFrac, currency = amt.currency
)
}
return amt
@@ -289,22 +253,19 @@ private fun normalizeAmount(amt: TalerAmount): TalerAmount {
// Adds two amounts and returns the normalized version.
private fun amountAdd(first: TalerAmount, second: TalerAmount): TalerAmount {
- if (first.currency != second.currency)
- throw badRequest(
- "Currency mismatch, balance '${first.currency}', price '${second.currency}'",
- TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
- )
+ if (first.currency != second.currency) throw badRequest(
+ "Currency mismatch, balance '${first.currency}', price '${second.currency}'",
+ TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
+ )
val valueAdd = first.value + second.value
- if (valueAdd < first.value)
- throw badRequest("Amount value overflowed")
+ if (valueAdd < first.value) throw badRequest("Amount value overflowed")
val fracAdd = first.frac + second.frac
- if (fracAdd < first.frac)
- throw badRequest("Amount fraction overflowed")
- return normalizeAmount(TalerAmount(
- value = valueAdd,
- frac = fracAdd,
- currency = first.currency
- ))
+ if (fracAdd < first.frac) throw badRequest("Amount fraction overflowed")
+ return normalizeAmount(
+ TalerAmount(
+ value = valueAdd, frac = fracAdd, currency = first.currency
+ )
+ )
}
/**
@@ -315,20 +276,13 @@ private fun amountAdd(first: TalerAmount, second: TalerAmount): TalerAmount {
* the database.
*/
fun isBalanceEnough(
- balance: TalerAmount,
- due: TalerAmount,
- maxDebt: TalerAmount,
- hasBalanceDebt: Boolean
+ balance: TalerAmount, due: TalerAmount, maxDebt: TalerAmount, hasBalanceDebt: Boolean
): Boolean {
val normalMaxDebt = normalizeAmount(maxDebt) // Very unlikely to be needed.
if (hasBalanceDebt) {
val chargedBalance = amountAdd(balance, due)
if (chargedBalance.value > normalMaxDebt.value) return false // max debt surpassed
- if (
- (chargedBalance.value == normalMaxDebt.value) &&
- (chargedBalance.frac > maxDebt.frac)
- )
- return false
+ if ((chargedBalance.value == normalMaxDebt.value) && (chargedBalance.frac > maxDebt.frac)) return false
return true
}
/**
@@ -336,19 +290,17 @@ fun isBalanceEnough(
* block calculates how much debt the balance would get, should a
* subtraction of 'due' occur.
*/
- if (balance.currency != due.currency)
- throw badRequest(
- "Currency mismatch, balance '${balance.currency}', due '${due.currency}'",
- TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
- )
+ if (balance.currency != due.currency) throw badRequest(
+ "Currency mismatch, balance '${balance.currency}', due '${due.currency}'",
+ TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
+ )
val valueDiff = if (balance.value < due.value) due.value - balance.value else 0L
val fracDiff = if (balance.frac < due.frac) due.frac - balance.frac else 0
// Getting the normalized version of such diff.
val normalDiff = normalizeAmount(TalerAmount(valueDiff, fracDiff, balance.currency))
// Failing if the normalized diff surpasses the max debt.
if (normalDiff.value > normalMaxDebt.value) return false
- if ((normalDiff.value == normalMaxDebt.value) &&
- (normalDiff.frac > normalMaxDebt.frac)) return false
+ if ((normalDiff.value == normalMaxDebt.value) && (normalDiff.frac > normalMaxDebt.frac)) return false
return true
}
@@ -360,47 +312,41 @@ fun isBalanceEnough(
*
* https://$BANK_URL/taler-integration
*/
-fun getTalerWithdrawUri(baseUrl: String, woId: String) =
- url {
- val baseUrlObj = URL(baseUrl)
- protocol = URLProtocol(
- name = "taler".plus(if (baseUrlObj.protocol.lowercase() == "http") "+http" else ""),
- defaultPort = -1
- )
- host = "withdraw"
- val pathSegments = mutableListOf(
- // adds the hostname(+port) of the actual bank that will serve the withdrawal request.
- baseUrlObj.host.plus(
- if (baseUrlObj.port != -1)
- ":${baseUrlObj.port}"
- else ""
- )
+fun getTalerWithdrawUri(baseUrl: String, woId: String) = url {
+ val baseUrlObj = URL(baseUrl)
+ protocol = URLProtocol(
+ name = "taler".plus(if (baseUrlObj.protocol.lowercase() == "http") "+http" else ""), defaultPort = -1
+ )
+ host = "withdraw"
+ val pathSegments = mutableListOf(
+ // adds the hostname(+port) of the actual bank that will serve the withdrawal request.
+ baseUrlObj.host.plus(
+ if (baseUrlObj.port != -1) ":${baseUrlObj.port}"
+ else ""
)
- // Removing potential double slashes.
- baseUrlObj.path.split("/").forEach {
- if (it.isNotEmpty()) pathSegments.add(it)
- }
- pathSegments.add("taler-integration/${woId}")
- this.appendPathSegments(pathSegments)
+ )
+ // Removing potential double slashes.
+ baseUrlObj.path.split("/").forEach {
+ if (it.isNotEmpty()) pathSegments.add(it)
}
+ pathSegments.add("taler-integration/${woId}")
+ this.appendPathSegments(pathSegments)
+}
// Builds a withdrawal confirm URL.
fun getWithdrawalConfirmUrl(
- baseUrl: String,
- wopId: String,
- username: String
- ) =
- url {
- val baseUrlObj = URL(baseUrl)
- protocol = URLProtocol(name = baseUrlObj.protocol, defaultPort = -1)
- host = baseUrlObj.host
- // Removing potential double slashes:
- baseUrlObj.path.split("/").forEach {
- this.appendPathSegments(it)
- }
- // Completing the endpoint:
- this.appendPathSegments("accounts/${username}/withdrawals/${wopId}/confirm")
+ baseUrl: String, wopId: String, username: String
+) = url {
+ val baseUrlObj = URL(baseUrl)
+ protocol = URLProtocol(name = baseUrlObj.protocol, defaultPort = -1)
+ host = baseUrlObj.host
+ // Removing potential double slashes:
+ baseUrlObj.path.split("/").forEach {
+ this.appendPathSegments(it)
}
+ // Completing the endpoint:
+ this.appendPathSegments("accounts/${username}/withdrawals/${wopId}/confirm")
+}
/**
@@ -417,24 +363,23 @@ fun getWithdrawal(db: Database, opIdParam: String): TalerWithdrawalOperation {
logger.error(e.message)
throw badRequest("withdrawal_id query parameter was malformed")
}
- val op = db.talerWithdrawalGet(opId)
- ?: throw notFound(
- hint = "Withdrawal operation $opIdParam not found",
- talerEc = TalerErrorCode.TALER_EC_END
- )
+ val op = db.talerWithdrawalGet(opId) ?: throw notFound(
+ hint = "Withdrawal operation $opIdParam not found", talerEc = TalerErrorCode.TALER_EC_END
+ )
return op
}
data class HistoryParams(
- val delta: Long,
- val start: Long
+ val delta: Long, val start: Long
)
+
/**
* Extracts the query parameters from "history-like" endpoints,
* providing the defaults according to the API specification.
*/
fun getHistoryParams(req: ApplicationRequest): HistoryParams {
- val deltaParam: String = req.queryParameters["delta"] ?: throw MissingRequestParameterException(parameterName = "delta")
+ val deltaParam: String =
+ req.queryParameters["delta"] ?: throw MissingRequestParameterException(parameterName = "delta")
val delta: Long = try {
deltaParam.toLong()
} catch (e: Exception) {
@@ -473,8 +418,7 @@ fun maybeCreateAdminAccount(db: Database, ctx: BankApplicationContext): Boolean
* Hashing the password helps to avoid the "password not hashed"
* error, in case the admin tries to authenticate.
*/
- passwordHash = CryptoUtil.hashpw(String(pwBuf, Charsets.UTF_8)),
- name = "Bank administrator"
+ passwordHash = CryptoUtil.hashpw(String(pwBuf, Charsets.UTF_8)), name = "Bank administrator"
)
val rowId = db.customerCreate(adminCustomer)
if (rowId == null) {
@@ -482,9 +426,7 @@ fun maybeCreateAdminAccount(db: Database, ctx: BankApplicationContext): Boolean
return false
}
rowId
- }
- else
- maybeAdminCustomer.expectRowId()
+ } else maybeAdminCustomer.expectRowId()
val maybeAdminBankAccount = db.bankAccountGetFromOwnerId(adminCustomerId)
if (maybeAdminBankAccount == null) {
logger.info("Creating admin bank account")
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/types.kt b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
@@ -1,572 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2019 Stanisci and Dold.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING. If not, see
- * <http://www.gnu.org/licenses/>
- */
-
-package tech.libeufin.bank
-
-import io.ktor.http.*
-import io.ktor.server.application.*
-import kotlinx.serialization.Contextual
-import kotlinx.serialization.Serializable
-import java.io.Serial
-import java.util.*
-
-// Allowed lengths for fractional digits in amounts.
-enum class FracDigits(howMany: Int) {
- TWO(2),
- EIGHT(8)
-}
-
-
-// It contains the number of microseconds since the Epoch.
-@Serializable
-data class Timestamp(
- val t_s: Long // FIXME (?): not supporting "never" at the moment.
-)
-
-/**
- * HTTP response type of successful token refresh.
- * access_token is the Crockford encoding of the 32 byte
- * access token, whereas 'expiration' is the point in time
- * when this token expires.
- */
-@Serializable
-data class TokenSuccessResponse(
- val access_token: String,
- val expiration: Timestamp
-)
-
-/**
- * Error object to respond to the client. The
- * 'code' field takes values from the GANA gnu-taler-error-code
- * specification. 'hint' is a human-readable description
- * of the error.
- */
-@Serializable
-data class TalerError(
- val code: Int,
- val hint: String? = null
-)
-
-/* Contains contact data to send TAN challges to the
-* users, to let them complete cashout operations. */
-@Serializable
-data class ChallengeContactData(
- val email: String? = null,
- val phone: String? = null
-)
-
-// Type expected at POST /accounts
-@Serializable
-data class RegisterAccountRequest(
- val username: String,
- val password: String,
- val name: String,
- val is_public: Boolean = false,
- val is_taler_exchange: Boolean = false,
- val challenge_contact_data: ChallengeContactData? = null,
- // External bank account where to send cashout amounts.
- val cashout_payto_uri: String? = null,
- // Bank account internal to Libeufin-Bank.
- val internal_payto_uri: String? = null
-)
-
-/* Internal representation of relative times. The
-* "forever" case is represented with Long.MAX_VALUE.
-*/
-data class RelativeTime(
- val d_us: Long
-)
-
-/**
- * Type expected at POST /accounts/{USERNAME}/token
- * It complies with Taler's design document #49
- */
-@Serializable
-data class TokenRequest(
- val scope: TokenScope,
- @Contextual
- val duration: RelativeTime? = null,
- val refreshable: Boolean = false
-)
-
-/**
- * Convenience type to throw errors along the bank activity
- * and that is meant to be caught by Ktor and responded to the
- * client.
- */
-class LibeufinBankException(
- // Status code that Ktor will set for the response.
- val httpStatus: HttpStatusCode,
- // Error detail object, after Taler API.
- val talerError: TalerError
-) : Exception(talerError.hint)
-
-/**
- * Convenience type to hold customer data, typically after such
- * data gets fetched from the database. It is also used to _insert_
- * customer data to the database.
- */
-data class Customer(
- val login: String,
- val passwordHash: String,
- val name: String,
- /**
- * Only non-null when this object is defined _by_ the
- * database.
- */
- val dbRowId: Long? = null,
- val email: String? = null,
- val phone: String? = null,
- /**
- * External bank account where customers send
- * their cashout amounts.
- */
- val cashoutPayto: String? = null,
- /**
- * Currency of the external bank account where
- * customers send their cashout amounts.
- */
- val cashoutCurrency: String? = null
-)
-
-/**
- * Represents a Taler amount. This type can be used both
- * to hold database records and amounts coming from the parser.
- * If maybeCurrency is null, then the constructor defaults it
- * to be the "internal currency". Internal currency is the one
- * with which Libeufin-Bank moves funds within itself, therefore
- * not to be mistaken with the cashout currency, which is the one
- * that gets credited to Libeufin-Bank users to their cashout_payto_uri.
- *
- * maybeCurrency is typically null when the TalerAmount object gets
- * defined by the Database class.
- */
-class TalerAmount(
- val value: Long,
- val frac: Int,
- val currency: String
-) {
- override fun equals(other: Any?): Boolean {
- return other is TalerAmount &&
- other.value == this.value &&
- other.frac == this.frac &&
- other.currency == this.currency
- }
-
- override fun toString(): String {
- return "$currency:$value.$frac"
- }
-}
-
-/**
- * Convenience type to get and set bank account information
- * from/to the database.
- */
-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,
- /**
- * Because bank accounts MAY be funded by an external currency,
- * local bank accounts need to query Nexus, in order to find this
- * out. This field is a pointer to the latest incoming payment that
- * was contained in a Nexus history response.
- *
- * Typically, the 'admin' bank account uses this field, in order
- * to initiate Taler withdrawals that depend on an external currency
- * being wired by wallet owners.
- */
- val lastNexusFetchRowId: Long = 0L,
- val balance: TalerAmount? = null, // null when a new bank account gets created.
- val hasDebt: Boolean,
- val maxDebt: TalerAmount
-)
-
-// Allowed values for bank transactions directions.
-enum class TransactionDirection {
- credit,
- debit
-}
-
-// Allowed values for cashout TAN channels.
-enum class TanChannel {
- sms,
- email,
- file // Writes cashout TANs to /tmp, for testing.
-}
-
-// Scopes for authentication tokens.
-enum class TokenScope {
- readonly,
- readwrite,
- refreshable // Not spec'd as a scope!
-}
-
-/**
- * Convenience type to set/get authentication tokens to/from
- * the database.
- */
-data class BearerToken(
- val content: ByteArray,
- val scope: TokenScope,
- val isRefreshable: Boolean = false,
- val creationTime: Long,
- val expirationTime: Long,
- /**
- * Serial ID of the database row that hosts the bank customer
- * that is associated with this token. NOTE: if the token is
- * refreshed by a client that doesn't have a user+password login
- * in the system, the creator remains always the original bank
- * customer that created the very first token.
- */
- val bankCustomer: Long
-)
-
-/**
- * Convenience type to _communicate_ a bank transfer to the
- * database procedure, NOT representing therefore any particular
- * table. The procedure will then retrieve all the tables data
- * from this type.
- */
-data class BankInternalTransaction(
- // Database row ID of the internal bank account sending the payment.
- val creditorAccountId: Long,
- // Database row ID of the internal bank account receiving the payment.
- val debtorAccountId: Long,
- val subject: String,
- val amount: TalerAmount,
- val transactionDate: Long,
- val accountServicerReference: String = "not used", // ISO20022
- val endToEndId: String = "not used", // ISO20022
- val paymentInformationId: String = "not used" // ISO20022
-)
-
-/**
- * Convenience type representing bank transactions as they
- * are in the respective database table. Only used to _get_
- * the information from the database.
- */
-data class BankAccountTransaction(
- val creditorPaytoUri: String,
- val creditorName: String,
- val debtorPaytoUri: String,
- val debtorName: String,
- val subject: String,
- val amount: TalerAmount,
- val transactionDate: Long, // microseconds
- /**
- * Is the transaction debit, or credit for the
- * bank account pointed by this object?
- */
- val direction: TransactionDirection,
- /**
- * database row ID of the bank account that is
- * impacted by the direction. For example, if the
- * direction is debit, then this value points to the
- * bank account of the payer.
- */
- val bankAccountId: Long,
- // Null if this type is used to _create_ one transaction.
- val dbRowId: Long? = null,
- // Following are ISO20022 specific.
- val accountServicerReference: String,
- val paymentInformationId: String,
- val endToEndId: String,
-)
-
-/**
- * Represents a Taler withdrawal operation, as it is
- * stored in the respective database table.
- */
-data class TalerWithdrawalOperation(
- val withdrawalUuid: UUID,
- val amount: TalerAmount,
- val selectionDone: Boolean = false,
- val aborted: Boolean = false,
- val confirmationDone: Boolean = false,
- val reservePub: String?,
- val selectedExchangePayto: String?,
- val walletBankAccount: Long
-)
-
-/**
- * Represents a cashout operation, as it is stored
- * in the respective database table.
- */
-data class Cashout(
- val cashoutUuid: UUID,
- val localTransaction: Long? = null,
- val amountDebit: TalerAmount,
- val amountCredit: TalerAmount,
- val buyAtRatio: Int,
- val buyInFee: TalerAmount,
- val sellAtRatio: Int,
- val sellOutFee: TalerAmount,
- val subject: String,
- val creationTime: Long,
- val tanConfirmationTime: Long? = null,
- val tanChannel: TanChannel,
- val tanCode: String,
- 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.
-@Serializable
-data class AccountData(
- val name: String,
- val balance: String,
- val payto_uri: String,
- val debit_threshold: String,
- 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
-)
-
-/* History element, either from GET /transactions/T_ID
- or from GET /transactions */
-@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
-)
-
-// Response type for histories, namely GET /transactions
-@Serializable
-data class BankAccountTransactionsResponse(
- val transactions: MutableList<BankAccountTransactionInfo>
-)
-
-// Taler withdrawal request.
-@Serializable
-data class BankAccountCreateWithdrawalRequest(
- val amount: String
-)
-
-// Taler withdrawal response.
-@Serializable
-data class BankAccountCreateWithdrawalResponse(
- val withdrawal_id: String,
- val taler_withdraw_uri: String
-)
-
-// Taler withdrawal details response
-@Serializable
-data class BankAccountGetWithdrawalResponse(
- val amount: String,
- val aborted: Boolean,
- val confirmation_done: Boolean,
- val selection_done: Boolean,
- val selected_reserve_pub: String? = null,
- val selected_exchange_account: String? = null
-)
-
-typealias ResourceName = String
-
-/**
- * Checks if the input Customer has the rights over ResourceName.
- * FIXME: myAuth() gives null on failures, but this gives false.
- * Should they return the same, for consistency?
- */
-fun ResourceName.canI(c: Customer, withAdmin: Boolean = true): Boolean {
- if (c.login == this) return true
- if (c.login == "admin" && withAdmin) return true
- return false
-}
-
-/**
- * Factors out the retrieval of the resource name from
- * the URI. The resource looked for defaults to "USERNAME"
- * as this is frequently mentioned resource along the endpoints.
- *
- * This helper is recommended because it returns a ResourceName
- * type that then offers the ".canI()" helper to check if the user
- * has the rights on the resource.
- */
-fun ApplicationCall.getResourceName(param: String): ResourceName =
- this.expectUriComponent(param)
-
-/**
- * This type communicates the result of a database operation
- * to confirm one withdrawal operation.
- */
-enum class WithdrawalConfirmationResult {
- SUCCESS,
- OP_NOT_FOUND,
- EXCHANGE_NOT_FOUND,
- BALANCE_INSUFFICIENT,
-
- /**
- * This state indicates that the withdrawal was already
- * confirmed BUT Kotlin did not detect it and still invoked
- * the SQL procedure to confirm the withdrawal. This is
- * conflictual because only Kotlin is responsible to check
- * for idempotency, and this state witnesses a failure in
- * this regard.
- */
- CONFLICT
-}
-
-// GET /config response from the Taler Integration API.
-@Serializable
-data class TalerIntegrationConfigResponse(
- val name: String = "taler-bank-integration",
- val version: String = "0:0:0:",
- val currency: String
-)
-
-// Withdrawal status as spec'd in the Taler Integration API.
-@Serializable
-data class BankWithdrawalOperationStatus(
- // Indicates whether the withdrawal was aborted.
- val aborted: Boolean,
-
- /* Has the wallet selected parameters for the withdrawal operation
- (exchange and reserve public key) and successfully sent it
- to the bank? */
- val selection_done: Boolean,
-
- /* The transfer has been confirmed and registered by the bank.
- Does not guarantee that the funds have arrived at the exchange
- already. */
- val transfer_done: Boolean,
-
- /* Amount that will be withdrawn with this operation
- (raw amount without fee considerations). */
- val amount: String,
-
- /* Bank account of the customer that is withdrawing, as a
- ``payto`` URI. */
- val sender_wire: String? = null,
-
- // Suggestion for an exchange given by the bank.
- val suggested_exchange: String? = null,
-
- /* URL that the user needs to navigate to in order to
- complete some final confirmation (e.g. 2FA).
- It may contain withdrawal operation id */
- val confirm_transfer_url: String? = null,
-
- // Wire transfer types supported by the bank.
- val wire_types: MutableList<String> = mutableListOf("iban")
-)
-
-// Selection request on a Taler withdrawal.
-@Serializable
-data class BankWithdrawalOperationPostRequest(
- val reserve_pub: String,
- val selected_exchange: String,
-)
-
-/**
- * Response to the wallet after it selects the exchange
- * and the reserve pub.
- */
-@Serializable
-data class BankWithdrawalOperationPostResponse(
- val transfer_done: Boolean,
- val confirm_transfer_url: String? = null
-)
-
-/**
- * Request to an /admin/add-incoming request from
- * the Taler Wire Gateway API.
- */
-@Serializable
-data class AddIncomingRequest(
- val amount: String,
- val reserve_pub: String,
- val debit_account: String
-)
-
-// Response to /admin/add-incoming
-@Serializable
-data class AddIncomingResponse(
- val timestamp: Long,
- val row_id: Long
-)
-
-@Serializable
-data class TWGConfigResponse(
- val name: String = "taler-wire-gateway",
- val version: String = "0:0:0:",
- val currency: String
-)
-
-// Response of a TWG /history/incoming call.
-@Serializable
-data class IncomingHistory(
- val incoming_transactions: MutableList<IncomingReserveTransaction> = mutableListOf(),
- val credit_account: String // Receiver's Payto URI.
-)
-
-// TWG's incoming payment record.
-@Serializable
-data class IncomingReserveTransaction(
- val type: String = "RESERVE",
- val row_id: Long, // DB row ID of the payment.
- val date: Long, // microseconds timestamp.
- val amount: String,
- val debit_account: String, // Payto of the sender.
- val reserve_pub: String
-)
-
-// TWG's request to pay a merchant.
-@Serializable
-data class TransferRequest(
- val request_uid: String,
- @Contextual
- val amount: TalerAmount,
- val exchange_base_url: String,
- val wtid: String,
- val credit_account: String
-)
-
-// TWG's response to merchant payouts
-@Serializable
-data class TransferResponse(
- val timestamp: Long,
- val row_id: Long
-)
-\ No newline at end of file
diff --git a/contrib/libeufin-bank.sample.conf b/contrib/libeufin-bank.sample.conf
@@ -1,5 +1,5 @@
[libeufin-bank]
-currency = KUDOS
+CURRENCY = KUDOS
DEFAULT_CUSTOMER_DEBT_LIMIT = KUDOS:200
DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:2000
REGISTRATION_BONUS = KUDOS:100
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
@@ -3,9 +3,7 @@ package tech.libeufin.util
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
-import io.ktor.server.response.*
import io.ktor.server.util.*
-import io.ktor.util.*
import logger
// Get the base URL of a request, returns null if any problem occurs.
@@ -22,13 +20,13 @@ fun ApplicationRequest.getBaseUrl(): String? {
prefix += "/"
URLBuilder(
protocol = URLProtocol(
- name = this.headers.get("X-Forwarded-Proto") ?: run {
+ name = this.headers["X-Forwarded-Proto"] ?: run {
logger.error("Reverse proxy did not define X-Forwarded-Proto")
return null
},
defaultPort = -1 // Port must be specified with X-Forwarded-Host.
),
- host = this.headers.get("X-Forwarded-Host") ?: run {
+ host = this.headers["X-Forwarded-Host"] ?: run {
logger.error("Reverse proxy did not define X-Forwarded-Host")
return null
}
@@ -46,10 +44,6 @@ 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.maybeUriComponent(name: String): String? {
val ret: String? = this.parameters[name]
if (ret == null) {
@@ -77,6 +71,7 @@ data class AuthorizationDetails(
val scheme: String,
val content: String
)
+
// Returns the authorization scheme mentioned in the Auth header,
// or null if that could not be found.
fun getAuthorizationDetails(authorizationHeader: String): AuthorizationDetails? {