summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-09-24 13:19:20 +0200
committerFlorian Dold <florian@dold.me>2023-09-24 13:19:20 +0200
commit1ce3c4d4251015283d61654be5196c1a9734ca40 (patch)
treecc3ff99dd6c5b943c772fe5190bf02bd82114dc3
parent236b29f957795aad279bc62081de06861bfe0821 (diff)
downloadlibeufin-1ce3c4d4251015283d61654be5196c1a9734ca40.tar.gz
libeufin-1ce3c4d4251015283d61654be5196c1a9734ca40.tar.bz2
libeufin-1ce3c4d4251015283d61654be5196c1a9734ca40.zip
refactor file structure
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt494
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt (renamed from bank/src/main/kotlin/tech/libeufin/bank/talerIntegrationHandlers.kt)0
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Main.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt (renamed from bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt)0
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt168
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt177
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt105
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt136
m---------contrib/wallet-core0
9 files changed, 494 insertions, 589 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
new file mode 100644
index 00000000..49421402
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
@@ -0,0 +1,494 @@
+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 net.taler.wallet.crypto.Base32Crockford
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import tech.libeufin.util.*
+import java.util.*
+
+private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.accountsMgmtHandlers")
+
+/**
+ * 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(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 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 = 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
+ 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
+ )
+ 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 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."
+ )
+ )
+ }
+ // 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 idempotency.
+ 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 = ctx.cashoutCurrency,
+ 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 = ctx.defaultCustomerDebtLimit
+ val newBankAccount = BankAccount(
+ hasDebt = false,
+ internalPaytoUri = req.internal_payto_uri ?: genIbanPaytoUri(),
+ owningCustomerId = newCustomerRowId,
+ isPublic = req.is_public,
+ isTalerExchange = req.is_taler_exchange,
+ maxDebt = maxDebt
+ )
+ val newBankAccountId = db.bankAccountCreate(newBankAccount)
+ ?: throw internalServerError("Could not INSERT bank account despite all the checks.")
+
+ /**
+ * The new account got created, now optionally award the registration
+ * bonus to it. The configuration gets either a Taler amount (of the
+ * bonus), or null if no bonus is meant to be awarded.
+ */
+ val bonusAmount = if (ctx.registrationBonusEnabled) ctx.registrationBonus else null
+ if (bonusAmount != null) {
+ 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(
+ creditorAccountId = newBankAccountId,
+ debtorAccountId = adminBankAccount.expectRowId(),
+ amount = bonusAmount,
+ 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 */}
+ }
+ }
+ call.respond(HttpStatusCode.Created)
+ return@post
+ }
+ get("/accounts/{USERNAME}") {
+ val c = call.myAuth(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
+ )
+ // 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.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()
+ // 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}'")
+ 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.
+ )
+ // Auth and funds passed, create the operation now!
+ val opId = UUID.randomUUID()
+ if(
+ !db.talerWithdrawalCreate(
+ opId,
+ b.expectRowId(),
+ withdrawalAmount
+ )
+ )
+ 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())
+ ))
+ 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.
+ 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
+ ))
+ return@get
+ }
+ post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/abort") {
+ val c = call.myAuth(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"))
+ // Idempotency:
+ if (op.aborted) {
+ call.respondText("{}", ContentType.Application.Json)
+ return@post
+ }
+ // 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
+ )
+ call.respondText("{}", ContentType.Application.Json)
+ return@post
+ }
+ post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/confirm") {
+ val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
+ // No admin allowed.
+ 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
+ )
+ // 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
+ * 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.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
+ * exists when POST /withdrawals happened, or because the exchange
+ * 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
+ )
+ }
+ return@post
+ }
+
+ get("/accounts/{USERNAME}/transactions") {
+ val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
+ val resourceName = call.expectUriComponent("USERNAME")
+ if (c.login != resourceName && c.login != "admin") throw forbidden()
+ // Collecting params.
+ val historyParams = getHistoryParams(call.request)
+ // Making the query.
+ val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
+ ?: 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
+ )
+ 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"
+ )
+ ))
+ }
+ call.respond(res)
+ return@get
+ }
+ // Creates a bank transaction.
+ post("/accounts/{USERNAME}/transactions") {
+ val c = call.myAuth(db, 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>()
+ // FIXME: make payto parser IBAN-agnostic?
+ val payto = parsePayto(txData.payto_uri) ?: throw badRequest("Invalid creditor Payto")
+ val paytoWithoutParams = "payto://iban/${payto.bic}/${payto.iban}"
+ 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(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
+ )
+ 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(db, 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/talerIntegrationHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt
index 502d8746..502d8746 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/talerIntegrationHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 61ea1227..c01fd67f 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -311,9 +311,6 @@ fun Application.corebankWebApp(db: Database, ctx: BankApplicationContext) {
return@get
}
this.accountsMgmtHandlers(db, ctx)
- this.tokenHandlers(db, ctx)
- this.transactionsHandlers(db, ctx)
- this.talerWebHandlers(db)
this.talerIntegrationHandlers(db, ctx)
this.talerWireGatewayHandlers(db, ctx)
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
index 619e5fc8..619e5fc8 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
deleted file mode 100644
index ef6d66e3..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
+++ /dev/null
@@ -1,168 +0,0 @@
-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 org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.getNowUs
-import tech.libeufin.util.maybeUriComponent
-
-private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.accountsMgmtHandlers")
-
-/**
- * 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(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."
- )
- )
- }
- // 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 idempotency.
- 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 = ctx.cashoutCurrency,
- 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 = ctx.defaultCustomerDebtLimit
- val newBankAccount = BankAccount(
- hasDebt = false,
- internalPaytoUri = req.internal_payto_uri ?: genIbanPaytoUri(),
- owningCustomerId = newCustomerRowId,
- isPublic = req.is_public,
- isTalerExchange = req.is_taler_exchange,
- maxDebt = maxDebt
- )
- val newBankAccountId = db.bankAccountCreate(newBankAccount)
- ?: throw internalServerError("Could not INSERT bank account despite all the checks.")
-
- /**
- * The new account got created, now optionally award the registration
- * bonus to it. The configuration gets either a Taler amount (of the
- * bonus), or null if no bonus is meant to be awarded.
- */
- val bonusAmount = if (ctx.registrationBonusEnabled) ctx.registrationBonus else null
- if (bonusAmount != null) {
- 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(
- creditorAccountId = newBankAccountId,
- debtorAccountId = adminBankAccount.expectRowId(),
- amount = bonusAmount,
- 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 */}
- }
- }
- call.respond(HttpStatusCode.Created)
- return@post
- }
- get("/accounts/{USERNAME}") {
- val c = call.myAuth(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
- )
- // 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.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
- }
-} \ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
deleted file mode 100644
index 258bc005..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
+++ /dev/null
@@ -1,177 +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/>
- */
-
-/* This file contains all the Taler handlers that do NOT
- * communicate with wallets, therefore any handler that serves
- * to SPAs or CLI HTTP clients.
- */
-
-package tech.libeufin.bank
-
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.plugins.*
-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.getBaseUrl
-import tech.libeufin.util.getNowUs
-import java.util.*
-
-fun Routing.talerWebHandlers(db: Database) {
- post("/accounts/{USERNAME}/withdrawals") {
- val c = call.myAuth(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}'")
- 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.
- )
- // Auth and funds passed, create the operation now!
- val opId = UUID.randomUUID()
- if(
- !db.talerWithdrawalCreate(
- opId,
- b.expectRowId(),
- withdrawalAmount
- )
- )
- 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())
- ))
- 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.
- 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
- ))
- return@get
- }
- post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/abort") {
- val c = call.myAuth(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"))
- // Idempotency:
- if (op.aborted) {
- call.respondText("{}", ContentType.Application.Json)
- return@post
- }
- // 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
- )
- call.respondText("{}", ContentType.Application.Json)
- return@post
- }
- post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/confirm") {
- val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
- // No admin allowed.
- 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
- )
- // 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
- * 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.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
- * exists when POST /withdrawals happened, or because the exchange
- * 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
- )
- }
- return@post
- }
-}
-
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
deleted file mode 100644
index 218651d3..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
+++ /dev/null
@@ -1,105 +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.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 org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import tech.libeufin.util.maybeUriComponent
-import tech.libeufin.util.getNowUs
-
-private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.accountsMgmtHandlers")
-
-fun Routing.tokenHandlers(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 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 = 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
- 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
- )
- 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
deleted file mode 100644
index 64266110..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package tech.libeufin.bank
-
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.plugins.*
-import io.ktor.server.request.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
-import net.taler.common.errorcodes.TalerErrorCode
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import tech.libeufin.util.getNowUs
-import tech.libeufin.util.parsePayto
-import kotlin.math.abs
-
-
-private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.transactionHandlers")
-
-fun Routing.transactionsHandlers(db: Database, ctx: BankApplicationContext) {
- get("/accounts/{USERNAME}/transactions") {
- val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
- val resourceName = call.expectUriComponent("USERNAME")
- if (c.login != resourceName && c.login != "admin") throw forbidden()
- // Collecting params.
- val historyParams = getHistoryParams(call.request)
- // Making the query.
- val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
- ?: 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
- )
- 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"
- )
- ))
- }
- call.respond(res)
- return@get
- }
- // Creates a bank transaction.
- post("/accounts/{USERNAME}/transactions") {
- val c = call.myAuth(db, 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>()
- // FIXME: make payto parser IBAN-agnostic?
- val payto = parsePayto(txData.payto_uri) ?: throw badRequest("Invalid creditor Payto")
- val paytoWithoutParams = "payto://iban/${payto.bic}/${payto.iban}"
- 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(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
- )
- 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(db, 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/contrib/wallet-core b/contrib/wallet-core
-Subproject 7079bce1ad2640e44561f56b46d5f00758df8e5
+Subproject 9e2d95b39723a038eb714d723ac0910a5bf596e