libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 99b1c08ebed2acd2efd0f907a5adcf57900bd075
parent 9a4be2728ea0c40451e2b0ce9c6e97d85b50a729
Author: Antoine A <>
Date:   Wed, 25 Jun 2025 13:42:52 +0200

bank: add missing token delete by id endpoint

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 3++-
Mbank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt | 17++++++++++++++++-
Mbank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt | 15+++++++++++++--
Mbank/src/test/kotlin/CoreBankApiTest.kt | 19++++++++++++++++++-
4 files changed, 49 insertions(+), 5 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -328,7 +328,8 @@ data class TokenInfo( val isRefreshable: Boolean, val description: String? = null, val last_access: TalerProtocolTimestamp, - val row_id: Long + val row_id: Long, + val token_id: Long ) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt @@ -137,15 +137,30 @@ private fun Routing.coreBankTokenApi(db: Database, cfg: BankConfig) { } } } + auth(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat, allowAdmin = true) { + delete("/accounts/{USERNAME}/tokens/{TOKEN_ID}") { + val id = call.longPath("TOKEN_ID") + if (db.token.deleteById(id)) { + call.respond(HttpStatusCode.NoContent) + } else { + throw notFound( + "Token '$id' not found", + TalerErrorCode.BANK_TRANSACTION_NOT_FOUND + ) + } + } + } auth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat) { delete("/accounts/{USERNAME}/token") { val token = call.authToken ?: throw badRequest("Basic auth not supported here.") db.token.delete(token) call.respond(HttpStatusCode.NoContent) } + } + auth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat, allowAdmin = true) { get("/accounts/{USERNAME}/tokens") { val params = PageParams.extract(call.request.queryParameters) - val tokens = db.token.page(params, call.username) + val tokens = db.token.page(params, call.username, Instant.now()) if (tokens.isEmpty()) { call.respond(HttpStatusCode.NoContent) } else { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt @@ -105,8 +105,16 @@ class TokenDAO(private val db: Database) { executeUpdate() } + /** Delete token [id] */ + suspend fun deleteById(id: Long) = db.serializable( + "DELETE FROM bearer_tokens WHERE bearer_token_id = ?" + ) { + bind(id) + executeUpdateCheck() + } + /** Get a page of all tokens of [username] accounts */ - suspend fun page(params: PageParams, username: String): List<TokenInfo> + suspend fun page(params: PageParams, username: String, timestamp: Instant): List<TokenInfo> = db.page( params, "bearer_token_id", @@ -121,10 +129,12 @@ class TokenDAO(private val db: Database) { bearer_token_id FROM bearer_tokens WHERE + expiration_time > ? AND bank_customer=(SELECT customer_id FROM customers WHERE deleted_at IS NULL AND username = ?) AND """, { + bind(timestamp.micros()) bind(username) } ) { @@ -135,7 +145,8 @@ class TokenDAO(private val db: Database) { isRefreshable = it.getBoolean("is_refreshable"), description = it.getString("description"), last_access = it.getTalerTimestamp("last_access"), - row_id = it.getLong("bearer_token_id") + row_id = it.getLong("bearer_token_id"), + token_id = it.getLong("bearer_token_id") ) } } \ No newline at end of file diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -334,7 +334,24 @@ class CoreBankTokenApiTest { }.assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN) } - // GET /accounts/USERNAME/token + // DELETE /accounts/USERNAME/tokens/TOKEN_ID + @Test + fun deleteById() = bankSetup { + authRoutine(HttpMethod.Delete, "/accounts/merchant/tokens/1t", allowAdmin = true) + + val token = client.postPw("/accounts/merchant/token") { + json { "scope" to "readonly" } + }.assertOkJson<TokenSuccessResponse>().access_token + // Check OK + client.deleteA("/accounts/merchant/tokens/2").assertNoContent() + client.deleteA("/accounts/merchant/tokens/2").assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + // Check token no longer work + client.delete("/accounts/merchant/token") { + headers[HttpHeaders.Authorization] = "Bearer $token" + }.assertUnauthorized(TalerErrorCode.GENERIC_TOKEN_UNKNOWN) + } + + // GET /accounts/USERNAME/tokens @Test fun get() = bankSetup { // Check OK