PreparedTransferApi.kt (5569B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2026 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 package tech.libeufin.nexus.api 21 22 import io.ktor.http.* 23 import io.ktor.server.request.* 24 import io.ktor.server.response.* 25 import io.ktor.server.routing.* 26 import io.github.smiley4.ktoropenapi.* 27 import io.ktor.util.pipeline.* 28 import tech.libeufin.common.* 29 import tech.libeufin.common.crypto.CryptoUtil 30 import tech.libeufin.nexus.NexusConfig 31 import tech.libeufin.nexus.db.Database 32 import tech.libeufin.nexus.db.TransferDAO.RegistrationResult 33 import java.time.Instant 34 import java.time.Duration 35 36 fun Routing.preparedTransferAPI(db: Database, cfg: NexusConfig) = conditional(cfg.wireGatewayApiCfg) { 37 get("/taler-prepared-transfer/config", { 38 operationId = "getPreparedTransferConfig" 39 description = "Get the configuration of the prepared transfer API" 40 tags = listOf("Prepared Transfer") 41 response { 42 code(HttpStatusCode.OK) { description = "Configuration of the prepared transfer API"; body<PreparedTransferConfig>() } 43 } 44 }) { 45 call.respond( 46 PreparedTransferConfig( 47 currency = cfg.currency, 48 supported_formats = listOf(SubjectFormat.SIMPLE, SubjectFormat.CH_QR_BILL) 49 ) 50 ) 51 } 52 post("/taler-prepared-transfer/registration", { 53 operationId = "registerTransfer" 54 description = "Register a prepared transfer" 55 tags = listOf("Prepared Transfer") 56 request { 57 body<SubjectRequest>() 58 } 59 response { 60 code(HttpStatusCode.OK) { description = "Registration successful"; body<SubjectResult>() } 61 code(HttpStatusCode.Conflict) { description = "Reserve pub or subject derivation already used" } 62 } 63 }) { 64 val req = call.receive<SubjectRequest>(); 65 66 if (!req.verify()) 67 throw forbidden( 68 "invalid signature", 69 TalerErrorCode.BANK_BAD_SIGNATURE 70 ) 71 72 val iban = req.credit_account.expectIban().iban 73 val reference = if (iban == cfg.ebics.account.iban) { 74 null 75 } else if (iban == cfg.ebics.qrIban) { 76 subjectFmtQrBill(req.authorization_pub) 77 } else { 78 throw conflict( 79 "Creditor account '${req.credit_account}' is not supported", 80 TalerErrorCode.BANK_UNKNOWN_CREDITOR 81 ) 82 } 83 84 when (val result = db.transfer.register( 85 req.type, 86 req.account_pub, 87 req.authorization_pub, 88 req.authorization_sig, 89 req.recurrent, 90 reference, 91 Instant.now() 92 )) { 93 RegistrationResult.ReservePubReuse -> throw conflict( 94 "reserve_pub used already", 95 TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT 96 ) 97 RegistrationResult.SubjectReuse -> throw conflict( 98 "subject derivation used already", 99 TalerErrorCode.BANK_DERIVATION_REUSE 100 ) 101 RegistrationResult.Success -> { 102 call.respond( 103 SubjectResult( 104 if (reference != null) { 105 listOf(TransferSubject.QrBill(reference, req.credit_amount)) 106 } else { 107 listOf(TransferSubject.Simple(fmtIncomingSubject(IncomingType.map, req.authorization_pub), req.credit_amount)) 108 }, 109 TalerTimestamp.never() 110 ) 111 ) 112 } 113 } 114 } 115 post("/taler-prepared-transfer/unregistration", { 116 operationId = "unregisterTransfer" 117 description = "Unregister a prepared subject" 118 tags = listOf("Prepared Transfer") 119 response { 120 code(HttpStatusCode.NoContent) { description = "Successfully unregistered" } 121 code(HttpStatusCode.NotFound) { description = "Prepared transfer not found" } 122 code(HttpStatusCode.Conflict) { description = "Invalid signature or timestamp too old" } 123 } 124 }) { 125 val req = call.receive<Unregistration>(); 126 127 if (req.timestamp.instant.isBefore(Instant.now().minus(Duration.ofMinutes(15)))) 128 throw conflict( 129 "timestamp too old", 130 TalerErrorCode.BANK_OLD_TIMESTAMP 131 ) 132 133 if (!req.verify()) 134 throw forbidden( 135 "invalid signature", 136 TalerErrorCode.BANK_BAD_SIGNATURE 137 ) 138 139 if (db.transfer.unregister(req.authorization_pub, Instant.now())) { 140 call.respond(HttpStatusCode.NoContent) 141 } else { 142 throw notFound( 143 "Prepared transfer '${req.authorization_pub}' not found", 144 TalerErrorCode.BANK_TRANSACTION_NOT_FOUND 145 ) 146 } 147 } 148 }