From 85914c4e6d047f25c3d704163912fb16e27c3ee8 Mon Sep 17 00:00:00 2001 From: vladae36 <80091744+vladae36@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:06:10 +0100 Subject: [PATCH] faster docker build, added ebicsResponse trace --- .github/workflows/dockerhub.yml | 5 +- .gitignore | 4 + Dockerfile | 42 +- .../tech/libeufin/nexus/ebics/EbicsClient.kt | 81 +- .../tech/libeufin/nexus/ebics/EbicsNexus.kt | 40 +- .../tech/libeufin/nexus/server/NexusServer.kt | 2121 +++++++++-------- 6 files changed, 1207 insertions(+), 1086 deletions(-) diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index a4ecfe2c..83fd92f4 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -7,6 +7,9 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Bootstrap - neeed by LibeuFin to inititalize the sub-repositories + run: ./bootstrap + - name: Log in to Docker Hub uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 with: @@ -27,5 +30,3 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - - diff --git a/.gitignore b/.gitignore index 6c643471..a0ffdb99 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ __pycache__ *.mk util/src/main/resources/version.txt .gitconfig +*/bin +.gitconfig +*/spx/* +spx \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a22715df..1ee56a2e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,14 @@ FROM zenika/kotlin:1.4.20-M2-jdk11-slim as build +# +# IMPORTANT - you need to call ./bootstrap in order to inititalize the sub-repo of Libeufin +# Ohterwise the build will fail with +# failed to solve: failed to compute cache key: failed to calculate checksum ..."/build-system/taler-build-scripts/configure": not found +# RUN apt update +# needed for nexus/sandbox RUN apt install -y python3 python3-pip git RUN pip3 install click requests -RUN mkdir /app -COPY ./ /app/ -WORKDIR /app/ -RUN chmod a+x bootstrap; ./bootstrap -RUN chmod a+x configure; ./configure -RUN make install -# needed for stating the UI to use the sandbox - LibFinEu/frontend +# needed for starting the UI to use the sandbox - LibFinEu/frontend RUN apt-get install -y wget curl nodejs yarnpkg npm RUN apt-get install postgresql-client -y # install versions according to LibFinEu/frontend/README.md @@ -16,6 +16,30 @@ RUN npm install -g n RUN n 10.16.0 RUN npm install -g npm@6.9.0 RUN npm install --global yarn@1.22.4 +RUN yarnpkg global add serve@13.0.4 +# moved ahead so that debugging kotlin is faster +RUN mkdir /app; ls -la /app/ +COPY gradlew build.gradle gradle.properties Makefile settings.gradle /app/ +COPY ./presentation /app/presentation +COPY ./frontend /app/frontend +WORKDIR /app/ RUN yarn --cwd /app/frontend/ install -# RUN yarnpkg --cwd /app/frontend/ build -RUN yarnpkg global add serve \ No newline at end of file + +# setup system +ARG CACHEBUST=10 +COPY ./build-system /app/build-system +COPY ./cli /app/cli +COPY ./contrib /app/contrib +COPY ./debian /app/debian +COPY ./nexus /app/nexus +COPY ./parsing-tests /app/parsing-tests +COPY ./sandbox /app/sandbox +COPY ./util /app/util +COPY ./gradle /app/gradle +COPY build-system/taler-build-scripts/configure /app/configure + +RUN chmod a+x configure; ./configure +RUN make all +RUN make install + +# RUN yarnpkg --cwd /app/frontend/ build \ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt index 65caf516..751d19b1 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt @@ -23,31 +23,48 @@ package tech.libeufin.nexus.ebics import io.ktor.client.HttpClient -import io.ktor.client.request.post -import io.ktor.http.HttpStatusCode +import io.ktor.client.features.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.util.* import org.slf4j.Logger import org.slf4j.LoggerFactory import tech.libeufin.nexus.NexusError import tech.libeufin.util.* import java.util.* +import java.io.File +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import tech.libeufin.nexus.server.setTransactionId private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util") private suspend inline fun HttpClient.postToBank(url: String, body: String): String { logger.debug("Posting: $body") if (!XMLUtil.validateFromString(body)) throw NexusError( - HttpStatusCode.InternalServerError, "EBICS (outgoing) document is invalid" + HttpStatusCode.InternalServerError, + "EBICS (outgoing) document is invalid" ) val response: String = try { - this.post( + this.post( urlString = url, block = { this.body = body } ) - } catch (e: Exception) { - logger.warn("Exception during request", e) - throw NexusError(HttpStatusCode.InternalServerError, "Cannot reach the bank") + } catch (e: ClientRequestException) { + logger.error(e.message) + throw NexusError( + HttpStatusCode.BadGateway, + e.message + ) + } + catch (e: Exception) { + logger.error("Exception during request", e) + throw NexusError( + HttpStatusCode.BadGateway, + e.message ?: "Could not reach the bank" + ) } logger.debug("Receiving: $response") return response @@ -56,16 +73,44 @@ private suspend inline fun HttpClient.postToBank(url: String, body: String): Str sealed class EbicsDownloadResult class EbicsDownloadSuccessResult( - val orderData: ByteArray + val orderData: ByteArray, + val transactionId: String ) : EbicsDownloadResult() /** - * Some bank-technical error occured. + * Some bank-technical error occured.w */ class EbicsDownloadBankErrorResult( val returnCode: EbicsReturnCode ) : EbicsDownloadResult() + +fun writeResponseToFile( transactionId: String, initResponseStr: String) { + // Get base directory from environment variable TRACE_DIR + setTransactionId(transactionId) + val baseDirectory = System.getenv("TRACE_DIR") ?: System.getProperty("user.dir") + + // Check if directory "trace" exists, create it if not + val traceDirectory = File("$baseDirectory/trace") + if (!traceDirectory.exists()) { + traceDirectory.mkdirs() + } + + // Get current date and time + val currentDateTime = LocalDateTime.now() + val formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss") + val formattedDateTime = currentDateTime.format(formatter) + + // Construct file name with transactionId and current date/time + val fileName = "${traceDirectory.absolutePath}/${transactionId}_$formattedDateTime.xml" + + // Write response to file + val file = File(fileName) + file.writeText(initResponseStr) + println("Response written to file: $fileName") +} + + /** * Do an EBICS download transaction. This includes the initialization phase, transaction phase * and receipt phase. @@ -90,7 +135,8 @@ suspend fun doEbicsDownloadTransaction( else -> { throw EbicsProtocolError( HttpStatusCode.InternalServerError, - "unexpected return code ${initResponse.technicalReturnCode}" + "unexpected return code ${initResponse.technicalReturnCode}", + initResponse.technicalReturnCode ) } } @@ -110,7 +156,9 @@ suspend fun doEbicsDownloadTransaction( HttpStatusCode.InternalServerError, "initial response must contain transaction ID" ) - + // Write response to file wasa + writeResponseToFile(transactionID,initResponseStr) + val encryptionInfo = initResponse.dataEncryptionInfo ?: throw NexusError(HttpStatusCode.InternalServerError, "initial response did not contain encryption info") @@ -177,7 +225,7 @@ suspend fun doEbicsDownloadTransaction( throw NexusError(HttpStatusCode.InternalServerError, "unexpected return code") } } - return EbicsDownloadSuccessResult(respPayload) + return EbicsDownloadSuccessResult(respPayload, transactionID) } @@ -237,8 +285,7 @@ suspend fun doEbicsUploadTransaction( suspend fun doEbicsHostVersionQuery(client: HttpClient, ebicsBaseUrl: String, ebicsHostId: String): EbicsHevDetails { val ebicsHevRequest = makeEbicsHEVRequestRaw(ebicsHostId) val resp = client.postToBank(ebicsBaseUrl, ebicsHevRequest) - val versionDetails = parseEbicsHEVResponse(resp) - return versionDetails + return parseEbicsHEVResponse(resp) } suspend fun doEbicsIniRequest( @@ -250,8 +297,7 @@ suspend fun doEbicsIniRequest( subscriberDetails.ebicsUrl, request ) - val resp = parseAndDecryptEbicsKeyManagementResponse(subscriberDetails, respStr) - return resp + return parseAndDecryptEbicsKeyManagementResponse(subscriberDetails, respStr) } suspend fun doEbicsHiaRequest( @@ -263,8 +309,7 @@ suspend fun doEbicsHiaRequest( subscriberDetails.ebicsUrl, request ) - val resp = parseAndDecryptEbicsKeyManagementResponse(subscriberDetails, respStr) - return resp + return parseAndDecryptEbicsKeyManagementResponse(subscriberDetails, respStr) } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt index f0782124..50806317 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -70,7 +70,6 @@ private data class EbicsFetchSpec( val orderParams: EbicsOrderParams ) -// Moved eventually in a tucked "camt" file. fun storeCamt(bankConnectionId: String, camt: String, historyType: String) { val camt53doc = XMLUtil.parseStringIntoDom(camt) val msgId = camt53doc.pickStringWithRootNs("/*[1]/*[1]/root:GrpHdr/root:MsgId") @@ -104,12 +103,24 @@ private suspend fun fetchEbicsC5x( subscriberDetails: EbicsClientSubscriberDetails ) { logger.debug("Requesting $historyType") - val response = doEbicsDownloadTransaction( - client, - subscriberDetails, - historyType, - orderParams - ) + val response = try { + doEbicsDownloadTransaction( + client, + subscriberDetails, + historyType, + orderParams + ) + } catch (e: EbicsProtocolError) { + /** + * This error type is not an actual error in this handler. + */ + if (e.ebicsTechnicalCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) { + logger.info("Could not find new transactions to download") + return + } + // re-throw in any other error case. + throw e + } when (historyType) { "C52" -> { } @@ -122,7 +133,7 @@ private suspend fun fetchEbicsC5x( when (response) { is EbicsDownloadSuccessResult -> { response.orderData.unzipWithLambda { - logger.debug("Camt entry: ${it.second}") + logger.debug("Camt entry (filename (in the Zip archive): ${it.first}): ${it.second}") storeCamt(bankConnectionId, it.second, historyType) } } @@ -302,7 +313,7 @@ fun Route.ebicsBankConnectionRoutes(client: HttpClient) { } post("/download/{msgtype}") { - val orderType = requireNotNull(call.parameters["msgtype"]).toUpperCase(Locale.ROOT) + val orderType = requireNotNull(call.parameters["msgtype"]).uppercase(Locale.ROOT) if (orderType.length != 3) { throw NexusError(HttpStatusCode.BadRequest, "ebics order type must be three characters") } @@ -596,7 +607,7 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { it.add(Paragraph("Verschlüsselungsschlüssel").setFontSize(24f)) writeCommon(it) it.add(Paragraph("Öffentlicher Schlüssel (Public encryption key)")) - writeKey(it, ebicsSubscriber.customerSignPriv) + writeKey(it, ebicsSubscriber.customerEncPriv) it.add(Paragraph("\n")) writeSigLine(it) } @@ -795,6 +806,13 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { } if (subscriber.ebicsIniState == EbicsInitState.UNKNOWN || subscriber.ebicsHiaState == EbicsInitState.UNKNOWN) { if (tentativeHpb(client, connId)) { + /** + * NOTE/FIXME: in case the HIA/INI did succeed (state is UNKNOWN but Sandbox + * has somehow the keys), here the state should be set to SENT, because later - + * when the Sandbox will respond to the INI/HIA requests - we'll get a + * EBICS_INVALID_USER_OR_USER_STATE. Hence, the state will never switch to + * SENT again. + */ return } } @@ -815,7 +833,7 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { val hpbData = try { doEbicsHpbRequest(client, subscriber) } catch (e: EbicsProtocolError) { - logger.warn("failed hpb request", e) + logger.warn("failed HPB request", e) null } transaction { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt index 4a0b7a3a..ae07f44c 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt @@ -17,1050 +17,1079 @@ * */ -package tech.libeufin.nexus.server + package tech.libeufin.nexus.server -import UtilError -import com.fasterxml.jackson.core.util.DefaultIndenter -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.core.JsonParseException -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.JsonMappingException -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.databind.exc.MismatchedInputException -import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import io.ktor.application.* -import io.ktor.client.* -import io.ktor.features.* -import io.ktor.http.* -import io.ktor.jackson.* -import io.ktor.request.* -import io.ktor.response.* -import io.ktor.routing.* -import io.ktor.server.engine.* -import io.ktor.server.netty.* -import io.ktor.util.* -import org.jetbrains.exposed.exceptions.ExposedSQLException -import org.jetbrains.exposed.sql.and -import org.jetbrains.exposed.sql.transactions.transaction -import org.slf4j.event.Level -import tech.libeufin.nexus.* -import tech.libeufin.nexus.bankaccount.* -import tech.libeufin.nexus.ebics.* -import tech.libeufin.nexus.iso20022.CamtBankAccountEntry -import tech.libeufin.util.* -import java.net.BindException -import java.net.URLEncoder -import kotlin.system.exitProcess - -/** - * Return facade state depending on the type. - */ -fun getFacadeState(type: String, facade: FacadeEntity): JsonNode { - return transaction { - when (type) { - "taler-wire-gateway" -> { - val state = TalerFacadeStateEntity.find { - TalerFacadeStateTable.facade eq facade.id - }.firstOrNull() - if (state == null) throw NexusError(HttpStatusCode.NotFound, "State of facade ${facade.id} not found") - val node = jacksonObjectMapper().createObjectNode() - node.put("bankConnection", state.bankConnection) - node.put("bankAccount", state.bankAccount) - node - } - else -> throw NexusError(HttpStatusCode.NotFound, "Facade type $type not supported") - } - } -} - - -fun ensureNonNull(param: String?): String { - return param ?: throw NexusError( - HttpStatusCode.BadRequest, "Bad ID given: $param" - ) -} - -fun ensureLong(param: String?): Long { - val asString = ensureNonNull(param) - return asString.toLongOrNull() ?: throw NexusError( - HttpStatusCode.BadRequest, "Parameter is not Long: $param" - ) -} - -fun expectNonNull(param: T?): T { - return param ?: throw NexusError( - HttpStatusCode.BadRequest, - "Non-null value expected." - ) -} - - -fun ApplicationRequest.hasBody(): Boolean { - if (this.isChunked()) { - return true - } - val contentLengthHeaderStr = this.headers["content-length"] - if (contentLengthHeaderStr != null) { - return try { - val cl = contentLengthHeaderStr.toInt() - cl != 0 - } catch (e: NumberFormatException) { - false - } - } - return false -} - -fun ApplicationCall.expectUrlParameter(name: String): String { - return this.request.queryParameters[name] - ?: throw NexusError(HttpStatusCode.BadRequest, "Parameter '$name' not provided in URI") -} - -fun isValidResourceName(name: String): Boolean { - return name.matches(Regex("[a-z]([-a-z0-9]*[a-z0-9])?")) -} - -fun requireValidResourceName(name: String): String { - if (!isValidResourceName(name)) { - throw NexusError( - HttpStatusCode.BadRequest, - "Invalid resource name. The first character must be a lowercase letter, " + - "and all following characters (except for the last character) must be a dash, " + - "lowercase letter, or digit. The last character must be a lowercase letter or digit." - ) - } - return name -} - -suspend inline fun ApplicationCall.receiveJson(): T { - try { - return this.receive() - } catch (e: MissingKotlinParameterException) { - throw NexusError(HttpStatusCode.BadRequest, "Missing value for ${e.pathReference}") - } catch (e: MismatchedInputException) { - throw NexusError(HttpStatusCode.BadRequest, "Invalid value for '${e.pathReference}'") - } catch (e: JsonParseException) { - throw NexusError(HttpStatusCode.BadRequest, "Invalid JSON") - } -} - -fun requireBankConnectionInternal(connId: String): NexusBankConnectionEntity { - return transaction { - NexusBankConnectionEntity.find { NexusBankConnectionsTable.connectionId eq connId }.firstOrNull() - } - ?: throw NexusError(HttpStatusCode.NotFound, "bank connection '$connId' not found") -} - -fun requireBankConnection(call: ApplicationCall, parameterKey: String): NexusBankConnectionEntity { - val name = call.parameters[parameterKey] - if (name == null) { - throw NexusError( - HttpStatusCode.NotFound, - "Parameter '${parameterKey}' wasn't found in URI" - ) - } - return requireBankConnectionInternal(name) -} - -val client = HttpClient { - expectSuccess = false // this way, it does not throw exceptions on != 200 responses. -} - -fun serverMain(host: String, port: Int) { - val server = embeddedServer(Netty, port = port, host = host) { - install(CallLogging) { - this.level = Level.DEBUG - this.logger = tech.libeufin.nexus.logger - } - install(ContentNegotiation) { - jackson { - enable(SerializationFeature.INDENT_OUTPUT) - setDefaultPrettyPrinter(DefaultPrettyPrinter().apply { - indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance) - indentObjectsWith(DefaultIndenter(" ", "\n")) - }) - registerModule(KotlinModule(nullisSameAsDefault = true)) - configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - } - } - install(StatusPages) { - exception { cause -> - logger.error("Caught exception while handling '${call.request.uri} (${cause.reason})") - call.respond( - status = cause.statusCode, - message = ErrorResponse( - code = TalerErrorCode.TALER_EC_LIBEUFIN_NEXUS_GENERIC_ERROR.code, - hint = "nexus error, see detail", - detail = cause.reason, - ) - ) - } - exception { cause -> - logger.error("Exception while handling '${call.request.uri}'", cause) - call.respond( - HttpStatusCode.BadRequest, - message = ErrorResponse( - code = TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID.code, - hint = "POSTed data was not valid", - detail = cause.message ?: "not given", - ) - ) - } - exception { cause -> - logger.error("Exception while handling '${call.request.uri}'", cause) - call.respond( - cause.statusCode, - message = ErrorResponse( - code = cause.ec?.code ?: TalerErrorCode.TALER_EC_NONE.code, - hint = "see detail", - detail = cause.reason, - ) - ) - } - exception { cause -> - logger.error("Caught exception while handling '${call.request.uri}' (${cause.reason})") - call.respond( - cause.httpStatusCode, - message = ErrorResponse( - code = TalerErrorCode.TALER_EC_LIBEUFIN_NEXUS_GENERIC_ERROR.code, - hint = "EBICS protocol error", - detail = cause.reason, - ) - ) - } - exception { cause -> - logger.error("Uncaught exception while handling '${call.request.uri}'") - cause.printStackTrace() - call.respond( - HttpStatusCode.InternalServerError, - ErrorResponse( - code = TalerErrorCode.TALER_EC_LIBEUFIN_NEXUS_UNCAUGHT_EXCEPTION.code, - hint = "unexpected exception", - detail = "exception message: ${cause.message}", - ) - ) - } - } - install(RequestBodyDecompression) - intercept(ApplicationCallPipeline.Fallback) { - if (this.call.response.status() == null) { - call.respondText("Not found (no route matched).\n", ContentType.Text.Plain, HttpStatusCode.NotFound) - return@intercept finish() - } - } - routing { - get("/config") { - call.respond( - makeJsonObject { - prop("version", getVersion()) - } - ) - return@get - } - // Shows information about the requesting user. - get("/user") { - val ret = transaction { - val currentUser = authenticateRequest(call.request) - UserResponse( - username = currentUser.username, - superuser = currentUser.superuser - ) - } - call.respond(ret) - return@get - } - - get("/permissions") { - val resp = object { - val permissions = mutableListOf() - } - transaction { - requireSuperuser(call.request) - NexusPermissionEntity.all().map { - resp.permissions.add( - Permission( - subjectType = it.subjectType, - subjectId = it.subjectId, - resourceType = it.resourceType, - resourceId = it.resourceId, - permissionName = it.permissionName, - ) - ) - } - } - call.respond(resp) - } - - post("/permissions") { - val req = call.receive() - val knownPermissions = listOf("facade.talerwiregateway.history", "facade.talerwiregateway.transfer") - val permName = req.permission.permissionName.lowercase() - if (!knownPermissions.contains(permName)) { - throw NexusError( - HttpStatusCode.BadRequest, - "Permission $permName not known" - ) - } - transaction { - requireSuperuser(call.request) - val existingPerm = findPermission(req.permission) - when (req.action) { - PermissionChangeAction.GRANT -> { - if (existingPerm == null) { - NexusPermissionEntity.new { - subjectType = req.permission.subjectType - subjectId = req.permission.subjectId - resourceType = req.permission.resourceType - resourceId = req.permission.resourceId - permissionName = permName - - } - } - } - PermissionChangeAction.REVOKE -> { - existingPerm?.delete() - } - } - null - } - call.respond(object {}) - } - - get("/users") { - transaction { - requireSuperuser(call.request) - } - val users = transaction { - transaction { - NexusUserEntity.all().map { - UserInfo(it.username, it.superuser) - } - } - } - val usersResp = UsersResponse(users) - call.respond(usersResp) - return@get - } - - // change a user's password - post("/users/{username}/password") { - val body = call.receiveJson() - val targetUsername = ensureNonNull(call.parameters["username"]) - transaction { - requireSuperuser(call.request) - val targetUser = NexusUserEntity.find { - NexusUsersTable.username eq targetUsername - }.firstOrNull() - if (targetUser == null) throw NexusError( - HttpStatusCode.NotFound, - "Username $targetUsername not found" - ) - targetUser.passwordHash = CryptoUtil.hashpw(body.newPassword) - } - call.respond(NexusMessage(message = "Password successfully changed")) - return@post - } - - // Add a new ordinary user in the system (requires superuser privileges) - post("/users") { - val body = call.receiveJson() - val requestedUsername = requireValidResourceName(body.username) - transaction { - requireSuperuser(call.request) - // check if username is available - val checkUsername = NexusUserEntity.find { - NexusUsersTable.username eq requestedUsername - }.firstOrNull() - if (checkUsername != null) throw NexusError( - HttpStatusCode.Conflict, - "Username $requestedUsername unavailable" - ) - NexusUserEntity.new { - username = requestedUsername - passwordHash = CryptoUtil.hashpw(body.password) - superuser = false - } - } - call.respond( - NexusMessage( - message = "New user '${body.username}' registered" - ) - ) - return@post - } - - get("/bank-connection-protocols") { - requireSuperuser(call.request) - call.respond( - HttpStatusCode.OK, - BankProtocolsResponse(listOf("ebics", "loopback")) - ) - return@get - } - - route("/bank-connection-protocols/ebics") { - ebicsBankProtocolRoutes(client) - } - - // Shows the bank accounts belonging to the requesting user. - get("/bank-accounts") { - val bankAccounts = BankAccounts() - transaction { - authenticateRequest(call.request) - // FIXME(dold): Only return accounts the user has at least read access to? - NexusBankAccountEntity.all().forEach { - bankAccounts.accounts.add( - BankAccount( - ownerName = it.accountHolder, - iban = it.iban, - bic = it.bankCode, - nexusBankAccountId = it.bankAccountName - ) - ) - } - } - call.respond(bankAccounts) - return@get - } - post("/bank-accounts/{accountId}/test-camt-ingestion/{type}") { - requireSuperuser(call.request) - processCamtMessage( - ensureNonNull(call.parameters["accountId"]), - XMLUtil.parseStringIntoDom(call.receiveText()), - ensureNonNull(call.parameters["type"]) - ) - call.respond(object {}) - return@post - } - get("/bank-accounts/{accountId}/schedule") { - requireSuperuser(call.request) - val resp = jacksonObjectMapper().createObjectNode() - val ops = jacksonObjectMapper().createObjectNode() - val accountId = ensureNonNull(call.parameters["accountId"]) - resp.set("schedule", ops) - transaction { - NexusBankAccountEntity.findByName(accountId) - ?: throw NexusError(HttpStatusCode.NotFound, "unknown bank account") - NexusScheduledTaskEntity.find { - (NexusScheduledTasksTable.resourceType eq "bank-account") and - (NexusScheduledTasksTable.resourceId eq accountId) - - }.forEach { - val t = jacksonObjectMapper().createObjectNode() - ops.set(it.taskName, t) - t.put("cronspec", it.taskCronspec) - t.put("type", it.taskType) - t.set("params", jacksonObjectMapper().readTree(it.taskParams)) - } - } - call.respond(resp) - return@get - } - - post("/bank-accounts/{accountId}/schedule") { - requireSuperuser(call.request) - val schedSpec = call.receive() - val accountId = ensureNonNull(call.parameters["accountId"]) - transaction { - authenticateRequest(call.request) - NexusBankAccountEntity.findByName(accountId) - ?: throw NexusError(HttpStatusCode.NotFound, "unknown bank account") - try { - NexusCron.parser.parse(schedSpec.cronspec) - } catch (e: IllegalArgumentException) { - throw NexusError(HttpStatusCode.BadRequest, "bad cron spec: ${e.message}") - } - // sanity checks. - when (schedSpec.type) { - "fetch" -> { - jacksonObjectMapper().treeToValue(schedSpec.params, FetchSpecJson::class.java) - ?: throw NexusError(HttpStatusCode.BadRequest, "bad fetch spec") - } - "submit" -> { - } - else -> throw NexusError(HttpStatusCode.BadRequest, "unsupported task type") - } - val oldSchedTask = NexusScheduledTaskEntity.find { - (NexusScheduledTasksTable.taskName eq schedSpec.name) and - (NexusScheduledTasksTable.resourceType eq "bank-account") and - (NexusScheduledTasksTable.resourceId eq accountId) - - }.firstOrNull() - if (oldSchedTask != null) { - throw NexusError(HttpStatusCode.BadRequest, "schedule task already exists") - } - NexusScheduledTaskEntity.new { - resourceType = "bank-account" - resourceId = accountId - this.taskCronspec = schedSpec.cronspec - this.taskName = requireValidResourceName(schedSpec.name) - this.taskType = schedSpec.type - this.taskParams = - jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(schedSpec.params) - } - } - call.respond(object {}) - return@post - } - - get("/bank-accounts/{accountId}/schedule/{taskId}") { - requireSuperuser(call.request) - val taskId = ensureNonNull(call.parameters["taskId"]) - val task = transaction { - NexusScheduledTaskEntity.find { - NexusScheduledTasksTable.taskName eq taskId - }.firstOrNull() - } - if (task == null) throw NexusError(HttpStatusCode.NotFound, "Task ${taskId} wasn't found") - call.respond( - AccountTask( - resourceId = task.resourceId, - resourceType = task.resourceType, - taskName = task.taskName, - taskCronspec = task.taskCronspec, - taskType = task.taskType, - taskParams = task.taskParams, - nextScheduledExecutionSec = task.nextScheduledExecutionSec, - prevScheduledExecutionSec = task.prevScheduledExecutionSec - ) - ) - return@get - } - - delete("/bank-accounts/{accountId}/schedule/{taskId}") { - requireSuperuser(call.request) - logger.info("schedule delete requested") - val accountId = ensureNonNull(call.parameters["accountId"]) - val taskId = ensureNonNull(call.parameters["taskId"]) - transaction { - val bankAccount = NexusBankAccountEntity.findByName(accountId) - if (bankAccount == null) { - throw NexusError(HttpStatusCode.NotFound, "unknown bank account") - } - val oldSchedTask = NexusScheduledTaskEntity.find { - (NexusScheduledTasksTable.taskName eq taskId) and - (NexusScheduledTasksTable.resourceType eq "bank-account") and - (NexusScheduledTasksTable.resourceId eq accountId) - - }.firstOrNull() - oldSchedTask?.delete() - } - call.respond(object {}) - } - - get("/bank-accounts/{accountid}") { - requireSuperuser(call.request) - val accountId = ensureNonNull(call.parameters["accountid"]) - val res = transaction { - val bankAccount = NexusBankAccountEntity.findByName(accountId) - if (bankAccount == null) { - throw NexusError(HttpStatusCode.NotFound, "unknown bank account") - } - val holderEnc = URLEncoder.encode(bankAccount.accountHolder, "UTF-8") - return@transaction makeJsonObject { - prop("defaultBankConnection", bankAccount.defaultBankConnection?.id?.value) - prop("accountPaytoUri", "payto://iban/${bankAccount.iban}?receiver-name=$holderEnc") - } - } - call.respond(res) - } - - // Submit one particular payment to the bank. - post("/bank-accounts/{accountid}/payment-initiations/{uuid}/submit") { - requireSuperuser(call.request) - val uuid = ensureLong(call.parameters["uuid"]) - transaction { - authenticateRequest(call.request) - } - submitPaymentInitiation(client, uuid) - call.respondText("Payment $uuid submitted") - return@post - } - - post("/bank-accounts/{accountid}/submit-all-payment-initiations") { - requireSuperuser(call.request) - val accountId = ensureNonNull(call.parameters["accountid"]) - transaction { - authenticateRequest(call.request) - } - submitAllPaymentInitiations(client, accountId) - call.respond(object {}) - return@post - } - - get("/bank-accounts/{accountid}/payment-initiations") { - requireSuperuser(call.request) - val ret = InitiatedPayments() - transaction { - val bankAccount = requireBankAccount(call, "accountid") - PaymentInitiationEntity.find { - PaymentInitiationsTable.bankAccount eq bankAccount.id.value - }.forEach { - val sd = it.submissionDate - ret.initiatedPayments.add( - PaymentStatus( - status = it.confirmationTransaction?.status, - paymentInitiationId = it.id.value.toString(), - submitted = it.submitted, - creditorIban = it.creditorIban, - creditorName = it.creditorName, - creditorBic = it.creditorBic, - amount = "${it.currency}:${it.sum}", - subject = it.subject, - submissionDate = if (sd != null) { - importDateFromMillis(sd).toDashedDate() - } else null, - preparationDate = importDateFromMillis(it.preparationDate).toDashedDate() - ) - ) - } - } - call.respond(ret) - return@get - } - - // Shows information about one particular payment initiation. - get("/bank-accounts/{accountid}/payment-initiations/{uuid}") { - requireSuperuser(call.request) - val res = transaction { - val paymentInitiation = getPaymentInitiation(ensureLong(call.parameters["uuid"])) - return@transaction object { - val paymentInitiation = paymentInitiation - val paymentStatus = paymentInitiation.confirmationTransaction?.status - } - } - val sd = res.paymentInitiation.submissionDate - call.respond( - PaymentStatus( - paymentInitiationId = res.paymentInitiation.id.value.toString(), - submitted = res.paymentInitiation.submitted, - creditorName = res.paymentInitiation.creditorName, - creditorBic = res.paymentInitiation.creditorBic, - creditorIban = res.paymentInitiation.creditorIban, - amount = "${res.paymentInitiation.currency}:${res.paymentInitiation.sum}", - subject = res.paymentInitiation.subject, - submissionDate = if (sd != null) { - importDateFromMillis(sd).toDashedDate() - } else null, - status = res.paymentStatus, - preparationDate = importDateFromMillis(res.paymentInitiation.preparationDate).toDashedDate() - ) - ) - return@get - } - - delete("/bank-accounts/{accountId}/payment-initiations/{uuid}") { - requireSuperuser(call.request) - val uuid = ensureLong(call.parameters["uuid"]) - transaction { - val paymentInitiation = getPaymentInitiation(uuid) - paymentInitiation.delete() - } - call.respond(NexusMessage(message = "Payment initiation $uuid deleted")) - } - - // Adds a new payment initiation. - post("/bank-accounts/{accountid}/payment-initiations") { - requireSuperuser(call.request) - val body = call.receive() - val accountId = ensureNonNull(call.parameters["accountid"]) - if (!validateBic(body.bic)) { - throw NexusError(HttpStatusCode.BadRequest, "invalid BIC (${body.bic})") - } - val res = transaction { - authenticateRequest(call.request) - val bankAccount = NexusBankAccountEntity.findByName(accountId) - if (bankAccount == null) { - throw NexusError(HttpStatusCode.NotFound, "unknown bank account ($accountId)") - } - val amount = parseAmount(body.amount) - val paymentEntity = addPaymentInitiation( - Pain001Data( - creditorIban = body.iban, - creditorBic = body.bic, - creditorName = body.name, - sum = amount.amount, - currency = amount.currency, - subject = body.subject - ), - bankAccount - ) - return@transaction object { - val uuid = paymentEntity.id.value - } - } - call.respond( - HttpStatusCode.OK, - PaymentInitiationResponse(uuid = res.uuid.toString()) - ) - return@post - } - - // Downloads new transactions from the bank. - post("/bank-accounts/{accountid}/fetch-transactions") { - requireSuperuser(call.request) - val accountid = call.parameters["accountid"] - if (accountid == null) { - throw NexusError( - HttpStatusCode.BadRequest, - "Account id missing" - ) - } - val fetchSpec = if (call.request.hasBody()) { - call.receive() - } else { - FetchSpecLatestJson( - FetchLevel.STATEMENT, - null - ) - } - val newTransactions = fetchBankAccountTransactions(client, fetchSpec, accountid) - call.respond(makeJsonObject { - prop("newTransactions", newTransactions) - }) - return@post - } - - // Asks list of transactions ALREADY downloaded from the bank. - get("/bank-accounts/{accountid}/transactions") { - requireSuperuser(call.request) - val bankAccountId = expectNonNull(call.parameters["accountid"]) - val ret = Transactions() - transaction { - authenticateRequest(call.request) - val bankAccount = NexusBankAccountEntity.findByName(bankAccountId) - if (bankAccount == null) { - throw NexusError(HttpStatusCode.NotFound, "unknown bank account") - } - NexusBankTransactionEntity.find { NexusBankTransactionsTable.bankAccount eq bankAccount.id }.map { - val tx = jacksonObjectMapper().readValue( - it.transactionJson, CamtBankAccountEntry::class.java - ) - ret.transactions.add(tx) - } - } - call.respond(ret) - return@get - } - - // Adds a new bank transport. - post("/bank-connections") { - requireSuperuser(call.request) - // user exists and is authenticated. - val body = call.receive() - requireValidResourceName(body.name) - transaction { - val user = authenticateRequest(call.request) - val existingConn = - NexusBankConnectionEntity.find { NexusBankConnectionsTable.connectionId eq body.name } - .firstOrNull() - if (existingConn != null) { - throw NexusError(HttpStatusCode.Conflict, "connection '${body.name}' exists already") - } - when (body) { - is CreateBankConnectionFromBackupRequestJson -> { - val type = body.data.get("type") - if (type == null || !type.isTextual) { - throw NexusError(HttpStatusCode.BadRequest, "backup needs type") - } - val plugin = getConnectionPlugin(type.textValue()) - plugin.createConnectionFromBackup(body.name, user, body.passphrase, body.data) - } - is CreateBankConnectionFromNewRequestJson -> { - val plugin = getConnectionPlugin(body.type) - plugin.createConnection(body.name, user, body.data) - } - } - } - call.respond(object {}) - } - - post("/bank-connections/delete-connection") { - requireSuperuser(call.request) - val body = call.receive() - transaction { - val conn = - NexusBankConnectionEntity.find { NexusBankConnectionsTable.connectionId eq body.bankConnectionId } - .firstOrNull() ?: throw NexusError( - HttpStatusCode.NotFound, - "Bank connection ${body.bankConnectionId}" - ) - conn.delete() // temporary, and instead just _mark_ it as deleted? - } - call.respond(object {}) - } - - get("/bank-connections") { - requireSuperuser(call.request) - val connList = BankConnectionsList() - transaction { - NexusBankConnectionEntity.all().forEach { - connList.bankConnections.add( - BankConnectionInfo( - name = it.connectionId, - type = it.type - ) - ) - } - } - call.respond(connList) - } - - get("/bank-connections/{connectionName}") { - requireSuperuser(call.request) - val resp = transaction { - val conn = requireBankConnection(call, "connectionName") - getConnectionPlugin(conn.type).getConnectionDetails(conn) - } - call.respond(resp) - } - - post("/bank-connections/{connectionName}/export-backup") { - requireSuperuser(call.request) - transaction { authenticateRequest(call.request) } - val body = call.receive() - val response = run { - val conn = requireBankConnection(call, "connectionName") - getConnectionPlugin(conn.type).exportBackup(conn.connectionId, body.passphrase) - } - call.response.headers.append("Content-Disposition", "attachment") - call.respond( - HttpStatusCode.OK, - response - ) - } - - post("/bank-connections/{connectionName}/connect") { - requireSuperuser(call.request) - val conn = transaction { - authenticateRequest(call.request) - requireBankConnection(call, "connectionName") - } - val plugin = getConnectionPlugin(conn.type) - plugin.connect(client, conn.connectionId) - call.respond(NexusMessage(message = "Connection successful")) - } - - get("/bank-connections/{connectionName}/keyletter") { - requireSuperuser(call.request) - val conn = transaction { - authenticateRequest(call.request) - requireBankConnection(call, "connectionName") - } - val pdfBytes = getConnectionPlugin(conn.type).exportAnalogDetails(conn) - call.respondBytes(pdfBytes, ContentType("application", "pdf")) - } - - get("/bank-connections/{connectionName}/messages") { - requireSuperuser(call.request) - val ret = transaction { - val list = BankMessageList() - val conn = requireBankConnection(call, "connectionName") - NexusBankMessageEntity.find { NexusBankMessagesTable.bankConnection eq conn.id }.map { - list.bankMessages.add( - BankMessageInfo( - it.messageId, - it.code, - it.message.bytes.size.toLong() - ) - ) - } - list - } - call.respond(ret) - } - - get("/bank-connections/{connid}/messages/{msgid}") { - requireSuperuser(call.request) - val ret = transaction { - val msgid = call.parameters["msgid"] - if (msgid == null || msgid == "") { - throw NexusError(HttpStatusCode.BadRequest, "missing or invalid message ID") - } - val msg = NexusBankMessageEntity.find { NexusBankMessagesTable.messageId eq msgid }.firstOrNull() - ?: throw NexusError(HttpStatusCode.NotFound, "bank message not found") - return@transaction object { - val msgContent = msg.message.bytes - } - } - call.respondBytes(ret.msgContent, ContentType("application", "xml")) - } - - get("/facades/{fcid}") { - requireSuperuser(call.request) - val fcid = ensureNonNull(call.parameters["fcid"]) - val ret = transaction { - val f = FacadeEntity.findByName(fcid) ?: throw NexusError( - HttpStatusCode.NotFound, "Facade $fcid does not exist" - ) - // FIXME: this only works for TWG urls. - FacadeShowInfo( - name = f.facadeName, - type = f.type, - twgBaseUrl = call.url { - parameters.clear() - encodedPath = "" - pathComponents("facades", f.facadeName, f.type) - encodedPath += "/" - }, - config = getFacadeState(f.type, f) - ) - } - call.respond(ret) - return@get - } - - get("/facades") { - requireSuperuser(call.request) - val ret = object { - val facades = mutableListOf() - } - transaction { - val user = authenticateRequest(call.request) - FacadeEntity.find { - FacadesTable.creator eq user.id - }.forEach { - ret.facades.add( - FacadeShowInfo( - name = it.facadeName, - type = it.type, - twgBaseUrl = call.url { - parameters.clear() - encodedPath = "" - pathComponents("facades", it.facadeName, it.type) - encodedPath += "/" - }, - config = getFacadeState(it.type, it) - ) - ) - } - } - call.respond(ret) - return@get - } - - delete("/facades/{fcid}") { - requireSuperuser(call.request) - val fcid = ensureNonNull(call.parameters["fcid"]) - transaction { - val f = FacadeEntity.findByName(fcid) ?: throw NexusError( - HttpStatusCode.NotFound, "Facade $fcid does not exist" - ) - f.delete() - } - call.respond({}) - return@delete - } - - post("/facades") { - requireSuperuser(call.request) - val body = call.receive() - requireValidResourceName(body.name) - if (body.type != "taler-wire-gateway") throw NexusError( - HttpStatusCode.NotImplemented, - "Facade type '${body.type}' is not implemented" - ) - val newFacade = try { - transaction { - val user = authenticateRequest(call.request) - FacadeEntity.new { - facadeName = body.name - type = body.type - creator = user - } - } - } catch (e: ExposedSQLException) { - logger.error("Could not persist facade name/type/creator: $e") - throw NexusError( - HttpStatusCode.BadRequest, - "Server could not persist data, possibly due to unavailable facade name" - ) - } - transaction { - TalerFacadeStateEntity.new { - bankAccount = body.config.bankAccount - bankConnection = body.config.bankConnection - reserveTransferLevel = body.config.reserveTransferLevel - facade = newFacade - currency = body.config.currency - } - } - call.respondText("Facade created") - return@post - } - - route("/bank-connections/{connid}") { - - // only ebics specific tasks under this part. - route("/ebics") { - ebicsBankConnectionRoutes(client) - } - post("/fetch-accounts") { - requireSuperuser(call.request) - val conn = transaction { - authenticateRequest(call.request) - requireBankConnection(call, "connid") - } - getConnectionPlugin(conn.type).fetchAccounts(client, conn.connectionId) - call.respond(object {}) - } - - // show all the offered accounts (both imported and non) - get("/accounts") { - requireSuperuser(call.request) - val ret = OfferedBankAccounts() - transaction { - val conn = requireBankConnection(call, "connid") - OfferedBankAccountEntity.find { - OfferedBankAccountsTable.bankConnection eq conn.id.value - }.forEach { offeredAccount -> - val importedId = offeredAccount.imported?.id - val imported = if (importedId != null) { - NexusBankAccountEntity.findById(importedId) - } else { - null - } - ret.accounts.add( - OfferedBankAccount( - ownerName = offeredAccount.accountHolder, - iban = offeredAccount.iban, - bic = offeredAccount.bankCode, - offeredAccountId = offeredAccount.offeredAccountId, - nexusBankAccountId = imported?.bankAccountName - ) - ) - } - } - call.respond(ret) - } - - // import one account into libeufin. - post("/import-account") { - requireSuperuser(call.request) - val body = call.receive() - importBankAccount(call, body.offeredAccountId, body.nexusBankAccountId) - call.respond(object {}) - } - } - route("/facades/{fcid}/taler-wire-gateway") { - talerFacadeRoutes(this, client) - } - - // Hello endpoint. - get("/") { - call.respondText("Hello, this is Nexus.\n") - return@get - } - } - } - logger.info("LibEuFin Nexus running on port $port") - try { - server.start(wait = true) - } catch (e: BindException) { - logger.error(e.message) - exitProcess(1) - } -} + import UtilError + import com.fasterxml.jackson.core.util.DefaultIndenter + import com.fasterxml.jackson.core.util.DefaultPrettyPrinter + import com.fasterxml.jackson.databind.JsonNode + import com.fasterxml.jackson.core.JsonParseException + import com.fasterxml.jackson.databind.DeserializationFeature + import com.fasterxml.jackson.databind.JsonMappingException + import com.fasterxml.jackson.databind.SerializationFeature + import com.fasterxml.jackson.databind.exc.MismatchedInputException + import com.fasterxml.jackson.module.kotlin.KotlinModule + import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException + import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper + import io.ktor.application.* + import io.ktor.client.* + import io.ktor.features.* + import io.ktor.http.* + import io.ktor.jackson.* + import io.ktor.request.* + import io.ktor.response.* + import io.ktor.routing.* + import io.ktor.server.engine.* + import io.ktor.server.netty.* + import io.ktor.util.* + import org.jetbrains.exposed.exceptions.ExposedSQLException + import org.jetbrains.exposed.sql.and + import org.jetbrains.exposed.sql.transactions.transaction + import org.slf4j.event.Level + import tech.libeufin.nexus.* + import tech.libeufin.nexus.bankaccount.* + import tech.libeufin.nexus.ebics.* + import tech.libeufin.nexus.iso20022.CamtBankAccountEntry + import tech.libeufin.util.* + import java.net.BindException + import java.net.URLEncoder + import kotlin.system.exitProcess + import java.net.URL + + import kotlinx.coroutines.asContextElement + import kotlinx.coroutines.* + import java.util.concurrent.atomic.AtomicReference + import kotlinx.coroutines.launch + + /** + * Return facade state depending on the type. + */ + fun getFacadeState(type: String, facade: FacadeEntity): JsonNode { + return transaction { + when (type) { + "taler-wire-gateway", "anastasis" -> { + val state = FacadeStateEntity.find { + FacadeStateTable.facade eq facade.id + }.firstOrNull() + if (state == null) throw NexusError(HttpStatusCode.NotFound, "State of facade ${facade.id} not found") + val node = jacksonObjectMapper().createObjectNode() + node.put("bankConnection", state.bankConnection) + node.put("bankAccount", state.bankAccount) + node + } + else -> throw NexusError(HttpStatusCode.NotFound, "Facade type $type not supported") + } + } + } + + + fun ensureNonNull(param: String?): String { + return param ?: throw NexusError( + HttpStatusCode.BadRequest, "Bad ID given: $param" + ) + } + + fun ensureLong(param: String?): Long { + val asString = ensureNonNull(param) + return asString.toLongOrNull() ?: throw NexusError( + HttpStatusCode.BadRequest, "Parameter is not Long: $param" + ) + } + + fun expectNonNull(param: T?): T { + return param ?: throw NexusError( + HttpStatusCode.BadRequest, + "Non-null value expected." + ) + } + + + fun ApplicationRequest.hasBody(): Boolean { + if (this.isChunked()) { + return true + } + val contentLengthHeaderStr = this.headers["content-length"] + if (contentLengthHeaderStr != null) { + return try { + val cl = contentLengthHeaderStr.toInt() + cl != 0 + } catch (e: NumberFormatException) { + false + } + } + return false + } + + fun ApplicationCall.expectUrlParameter(name: String): String { + return this.request.queryParameters[name] + ?: throw NexusError(HttpStatusCode.BadRequest, "Parameter '$name' not provided in URI") + } + + suspend inline fun ApplicationCall.receiveJson(): T { + try { + return this.receive() + } catch (e: MissingKotlinParameterException) { + throw NexusError(HttpStatusCode.BadRequest, "Missing value for ${e.pathReference}") + } catch (e: MismatchedInputException) { + throw NexusError(HttpStatusCode.BadRequest, "Invalid value for '${e.pathReference}'") + } catch (e: JsonParseException) { + throw NexusError(HttpStatusCode.BadRequest, "Invalid JSON") + } + } + + fun requireBankConnectionInternal(connId: String): NexusBankConnectionEntity { + return transaction { + NexusBankConnectionEntity.find { NexusBankConnectionsTable.connectionId eq connId }.firstOrNull() + } + ?: throw NexusError(HttpStatusCode.NotFound, "bank connection '$connId' not found") + } + + fun requireBankConnection(call: ApplicationCall, parameterKey: String): NexusBankConnectionEntity { + val name = call.parameters[parameterKey] + if (name == null) { + throw NexusError( + HttpStatusCode.NotFound, + "Parameter '${parameterKey}' wasn't found in URI" + ) + } + return requireBankConnectionInternal(name) + } + + val client = HttpClient { followRedirects = true } + + + /** + * Sets the transaction ID within the current context. + * + * @param transactionId The transaction ID to be set. + */ + + private var lastTransactionId: String?=null + + // set transactionID within the last tx id + // NOT thread or coroutine safe + public fun setTransactionId(transactionId: String) { + lastTransactionId=transactionId + } + + val nexusApp: Application.() -> Unit = { + install(CallLogging) { + this.level = Level.DEBUG + this.logger = tech.libeufin.nexus.logger + } + install(ContentNegotiation) { + jackson { + enable(SerializationFeature.INDENT_OUTPUT) + setDefaultPrettyPrinter(DefaultPrettyPrinter().apply { + indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance) + indentObjectsWith(DefaultIndenter(" ", "\n")) + }) + registerModule(KotlinModule(nullisSameAsDefault = true)) + configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + } + } + install(StatusPages) { + exception { cause -> + logger.error("Caught exception while handling '${call.request.uri} (${cause.reason})") + call.respond( + status = cause.statusCode, + message = ErrorResponse( + code = TalerErrorCode.TALER_EC_LIBEUFIN_NEXUS_GENERIC_ERROR.code, + hint = "nexus error, see detail", + detail = cause.reason, + ) + ) + } + exception { cause -> + logger.error("Exception while handling '${call.request.uri}'", cause) + call.respond( + HttpStatusCode.BadRequest, + message = ErrorResponse( + code = TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID.code, + hint = "POSTed data was not valid", + detail = cause.message ?: "not given", + ) + ) + } + exception { cause -> + logger.error("Exception while handling '${call.request.uri}'", cause) + call.respond( + cause.statusCode, + message = ErrorResponse( + code = cause.ec?.code ?: TalerErrorCode.TALER_EC_NONE.code, + hint = "see detail", + detail = cause.reason, + ) + ) + } + exception { cause -> + logger.error("Caught exception while handling '${call.request.uri}' (${cause.reason})") + call.respond( + cause.httpStatusCode, + message = ErrorResponse( + code = TalerErrorCode.TALER_EC_LIBEUFIN_NEXUS_GENERIC_ERROR.code, + hint = "EBICS protocol error", + detail = cause.reason, + ) + ) + } + exception { cause -> + logger.error("Uncaught exception while handling '${call.request.uri}'") + cause.printStackTrace() + call.respond( + HttpStatusCode.InternalServerError, + ErrorResponse( + code = TalerErrorCode.TALER_EC_LIBEUFIN_NEXUS_UNCAUGHT_EXCEPTION.code, + hint = "unexpected exception", + detail = "exception message: ${cause.message}", + ) + ) + } + } + install(RequestBodyDecompression) + intercept(ApplicationCallPipeline.Fallback) { + if (this.call.response.status() == null) { + call.respondText("Not found (no route matched).\n", ContentType.Text.Plain, HttpStatusCode.NotFound) + return@intercept finish() + } + } + routing { + get("/config") { + call.respond( + makeJsonObject { + prop("version", getVersion()) + } + ) + return@get + } + // Shows information about the requesting user. + get("/user") { + val ret = transaction { + val currentUser = authenticateRequest(call.request) + UserResponse( + username = currentUser.username, + superuser = currentUser.superuser + ) + } + call.respond(ret) + return@get + } + + get("/permissions") { + val resp = object { + val permissions = mutableListOf() + } + transaction { + requireSuperuser(call.request) + NexusPermissionEntity.all().map { + resp.permissions.add( + Permission( + subjectType = it.subjectType, + subjectId = it.subjectId, + resourceType = it.resourceType, + resourceId = it.resourceId, + permissionName = it.permissionName, + ) + ) + } + } + call.respond(resp) + } + + post("/permissions") { + val req = call.receive() + val knownPermissions = listOf( + "facade.talerwiregateway.history", "facade.talerwiregateway.transfer", + "facade.anastasis.history" + ) + val permName = req.permission.permissionName.lowercase() + if (!knownPermissions.contains(permName)) { + throw NexusError( + HttpStatusCode.BadRequest, + "Permission $permName not known" + ) + } + transaction { + requireSuperuser(call.request) + val existingPerm = findPermission(req.permission) + when (req.action) { + PermissionChangeAction.GRANT -> { + if (existingPerm == null) { + NexusPermissionEntity.new { + subjectType = req.permission.subjectType + subjectId = req.permission.subjectId + resourceType = req.permission.resourceType + resourceId = req.permission.resourceId + permissionName = permName + + } + } + } + PermissionChangeAction.REVOKE -> { + existingPerm?.delete() + } + } + null + } + call.respond(object {}) + } + + get("/users") { + transaction { + requireSuperuser(call.request) + } + val users = transaction { + transaction { + NexusUserEntity.all().map { + UserInfo(it.username, it.superuser) + } + } + } + val usersResp = UsersResponse(users) + call.respond(usersResp) + return@get + } + + // change a user's password + post("/users/{username}/password") { + val body = call.receiveJson() + val targetUsername = ensureNonNull(call.parameters["username"]) + transaction { + requireSuperuser(call.request) + val targetUser = NexusUserEntity.find { + NexusUsersTable.username eq targetUsername + }.firstOrNull() + if (targetUser == null) throw NexusError( + HttpStatusCode.NotFound, + "Username $targetUsername not found" + ) + targetUser.passwordHash = CryptoUtil.hashpw(body.newPassword) + } + call.respond(NexusMessage(message = "Password successfully changed")) + return@post + } + + // Add a new ordinary user in the system (requires superuser privileges) + post("/users") { + val body = call.receiveJson() + val requestedUsername = requireValidResourceName(body.username) + transaction { + requireSuperuser(call.request) + // check if username is available + val checkUsername = NexusUserEntity.find { + NexusUsersTable.username eq requestedUsername + }.firstOrNull() + if (checkUsername != null) throw NexusError( + HttpStatusCode.Conflict, + "Username $requestedUsername unavailable" + ) + NexusUserEntity.new { + username = requestedUsername + passwordHash = CryptoUtil.hashpw(body.password) + superuser = false + } + } + call.respond( + NexusMessage( + message = "New user '${body.username}' registered" + ) + ) + return@post + } + + get("/bank-connection-protocols") { + requireSuperuser(call.request) + call.respond( + HttpStatusCode.OK, + BankProtocolsResponse(listOf("ebics", "loopback")) + ) + return@get + } + + route("/bank-connection-protocols/ebics") { + ebicsBankProtocolRoutes(client) + } + + // Shows the bank accounts belonging to the requesting user. + get("/bank-accounts") { + val bankAccounts = BankAccounts() + transaction { + authenticateRequest(call.request) + // FIXME(dold): Only return accounts the user has at least read access to? + NexusBankAccountEntity.all().forEach { + bankAccounts.accounts.add( + BankAccount( + ownerName = it.accountHolder, + iban = it.iban, + bic = it.bankCode, + nexusBankAccountId = it.bankAccountName + ) + ) + } + } + call.respond(bankAccounts) + return@get + } + post("/bank-accounts/{accountId}/test-camt-ingestion/{type}") { + requireSuperuser(call.request) + processCamtMessage( + ensureNonNull(call.parameters["accountId"]), + XMLUtil.parseStringIntoDom(call.receiveText()), + ensureNonNull(call.parameters["type"]) + ) + call.respond(object {}) + return@post + } + get("/bank-accounts/{accountId}/schedule") { + requireSuperuser(call.request) + val resp = jacksonObjectMapper().createObjectNode() + val ops = jacksonObjectMapper().createObjectNode() + val accountId = ensureNonNull(call.parameters["accountId"]) + resp.set("schedule", ops) + transaction { + NexusBankAccountEntity.findByName(accountId) + ?: throw NexusError(HttpStatusCode.NotFound, "unknown bank account") + NexusScheduledTaskEntity.find { + (NexusScheduledTasksTable.resourceType eq "bank-account") and + (NexusScheduledTasksTable.resourceId eq accountId) + + }.forEach { + val t = jacksonObjectMapper().createObjectNode() + ops.set(it.taskName, t) + t.put("cronspec", it.taskCronspec) + t.put("type", it.taskType) + t.set("params", jacksonObjectMapper().readTree(it.taskParams)) + } + } + call.respond(resp) + return@get + } + + post("/bank-accounts/{accountId}/schedule") { + requireSuperuser(call.request) + val schedSpec = call.receive() + val accountId = ensureNonNull(call.parameters["accountId"]) + transaction { + authenticateRequest(call.request) + NexusBankAccountEntity.findByName(accountId) + ?: throw NexusError(HttpStatusCode.NotFound, "unknown bank account") + try { + NexusCron.parser.parse(schedSpec.cronspec) + } catch (e: IllegalArgumentException) { + throw NexusError(HttpStatusCode.BadRequest, "bad cron spec: ${e.message}") + } + // sanity checks. + when (schedSpec.type) { + "fetch" -> { + jacksonObjectMapper().treeToValue(schedSpec.params, FetchSpecJson::class.java) + ?: throw NexusError(HttpStatusCode.BadRequest, "bad fetch spec") + } + "submit" -> { + } + else -> throw NexusError(HttpStatusCode.BadRequest, "unsupported task type") + } + val oldSchedTask = NexusScheduledTaskEntity.find { + (NexusScheduledTasksTable.taskName eq schedSpec.name) and + (NexusScheduledTasksTable.resourceType eq "bank-account") and + (NexusScheduledTasksTable.resourceId eq accountId) + + }.firstOrNull() + if (oldSchedTask != null) { + throw NexusError(HttpStatusCode.BadRequest, "schedule task already exists") + } + NexusScheduledTaskEntity.new { + resourceType = "bank-account" + resourceId = accountId + this.taskCronspec = schedSpec.cronspec + this.taskName = requireValidResourceName(schedSpec.name) + this.taskType = schedSpec.type + this.taskParams = + jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(schedSpec.params) + } + } + call.respond(object {}) + return@post + } + + get("/bank-accounts/{accountId}/schedule/{taskId}") { + requireSuperuser(call.request) + val taskId = ensureNonNull(call.parameters["taskId"]) + val task = transaction { + NexusScheduledTaskEntity.find { + NexusScheduledTasksTable.taskName eq taskId + }.firstOrNull() + } + if (task == null) throw NexusError(HttpStatusCode.NotFound, "Task ${taskId} wasn't found") + call.respond( + AccountTask( + resourceId = task.resourceId, + resourceType = task.resourceType, + taskName = task.taskName, + taskCronspec = task.taskCronspec, + taskType = task.taskType, + taskParams = task.taskParams, + nextScheduledExecutionSec = task.nextScheduledExecutionSec, + prevScheduledExecutionSec = task.prevScheduledExecutionSec + ) + ) + return@get + } + + delete("/bank-accounts/{accountId}/schedule/{taskId}") { + requireSuperuser(call.request) + logger.info("schedule delete requested") + val accountId = ensureNonNull(call.parameters["accountId"]) + val taskId = ensureNonNull(call.parameters["taskId"]) + transaction { + val bankAccount = NexusBankAccountEntity.findByName(accountId) + if (bankAccount == null) { + throw NexusError(HttpStatusCode.NotFound, "unknown bank account") + } + val oldSchedTask = NexusScheduledTaskEntity.find { + (NexusScheduledTasksTable.taskName eq taskId) and + (NexusScheduledTasksTable.resourceType eq "bank-account") and + (NexusScheduledTasksTable.resourceId eq accountId) + + }.firstOrNull() + oldSchedTask?.delete() + } + call.respond(object {}) + } + + get("/bank-accounts/{accountid}") { + requireSuperuser(call.request) + val accountId = ensureNonNull(call.parameters["accountid"]) + val res = transaction { + val bankAccount = NexusBankAccountEntity.findByName(accountId) + if (bankAccount == null) { + throw NexusError(HttpStatusCode.NotFound, "unknown bank account") + } + val holderEnc = URLEncoder.encode(bankAccount.accountHolder, Charsets.UTF_8) + val lastSeenBalance = NexusBankBalanceEntity.find { + NexusBankBalancesTable.bankAccount eq bankAccount.id + }.lastOrNull() + return@transaction makeJsonObject { + prop("defaultBankConnection", bankAccount.defaultBankConnection?.id?.value) + prop("accountPaytoUri", "payto://iban/${bankAccount.iban}?receiver-name=$holderEnc") + prop( + "lastSeenBalance", + if (lastSeenBalance != null) { + val sign = if (lastSeenBalance.creditDebitIndicator == "DBIT") "-" else "" + "${sign}${lastSeenBalance.balance}" + } else { + "not downloaded from the bank yet" + } + ) + } + } + call.respond(res) + } + + // Submit one particular payment to the bank. + post("/bank-accounts/{accountid}/payment-initiations/{uuid}/submit") { + requireSuperuser(call.request) + val uuid = ensureLong(call.parameters["uuid"]) + transaction { + authenticateRequest(call.request) + } + submitPaymentInitiation(client, uuid) + call.respondText("Payment $uuid submitted") + return@post + } + + post("/bank-accounts/{accountid}/submit-all-payment-initiations") { + requireSuperuser(call.request) + val accountId = ensureNonNull(call.parameters["accountid"]) + transaction { + authenticateRequest(call.request) + } + submitAllPaymentInitiations(client, accountId) + call.respond(object {}) + return@post + } + + get("/bank-accounts/{accountid}/payment-initiations") { + requireSuperuser(call.request) + val ret = InitiatedPayments() + transaction { + val bankAccount = requireBankAccount(call, "accountid") + PaymentInitiationEntity.find { + PaymentInitiationsTable.bankAccount eq bankAccount.id.value + }.forEach { + val sd = it.submissionDate + ret.initiatedPayments.add( + PaymentStatus( + status = it.confirmationTransaction?.status, + paymentInitiationId = it.id.value.toString(), + submitted = it.submitted, + creditorIban = it.creditorIban, + creditorName = it.creditorName, + creditorBic = it.creditorBic, + amount = "${it.currency}:${it.sum}", + subject = it.subject, + submissionDate = if (sd != null) { + importDateFromMillis(sd).toDashedDate() + } else null, + preparationDate = importDateFromMillis(it.preparationDate).toDashedDate() + ) + ) + } + } + call.respond(ret) + return@get + } + + // Shows information about one particular payment initiation. + get("/bank-accounts/{accountid}/payment-initiations/{uuid}") { + requireSuperuser(call.request) + val res = transaction { + val paymentInitiation = getPaymentInitiation(ensureLong(call.parameters["uuid"])) + return@transaction object { + val paymentInitiation = paymentInitiation + val paymentStatus = paymentInitiation.confirmationTransaction?.status + } + } + val sd = res.paymentInitiation.submissionDate + call.respond( + PaymentStatus( + paymentInitiationId = res.paymentInitiation.id.value.toString(), + submitted = res.paymentInitiation.submitted, + creditorName = res.paymentInitiation.creditorName, + creditorBic = res.paymentInitiation.creditorBic, + creditorIban = res.paymentInitiation.creditorIban, + amount = "${res.paymentInitiation.currency}:${res.paymentInitiation.sum}", + subject = res.paymentInitiation.subject, + submissionDate = if (sd != null) { + importDateFromMillis(sd).toDashedDate() + } else null, + status = res.paymentStatus, + preparationDate = importDateFromMillis(res.paymentInitiation.preparationDate).toDashedDate() + ) + ) + return@get + } + + delete("/bank-accounts/{accountId}/payment-initiations/{uuid}") { + requireSuperuser(call.request) + val uuid = ensureLong(call.parameters["uuid"]) + transaction { + val paymentInitiation = getPaymentInitiation(uuid) + paymentInitiation.delete() + } + call.respond(NexusMessage(message = "Payment initiation $uuid deleted")) + } + + // Adds a new payment initiation. + post("/bank-accounts/{accountid}/payment-initiations") { + requireSuperuser(call.request) + val body = call.receive() + val accountId = ensureNonNull(call.parameters["accountid"]) + if (!validateBic(body.bic)) { + throw NexusError(HttpStatusCode.BadRequest, "invalid BIC (${body.bic})") + } + val res = transaction { + authenticateRequest(call.request) + val bankAccount = NexusBankAccountEntity.findByName(accountId) + if (bankAccount == null) { + throw NexusError(HttpStatusCode.NotFound, "unknown bank account ($accountId)") + } + val amount = parseAmount(body.amount) + val paymentEntity = addPaymentInitiation( + Pain001Data( + creditorIban = body.iban, + creditorBic = body.bic, + creditorName = body.name, + sum = amount.amount, + currency = amount.currency, + subject = body.subject + ), + bankAccount + ) + return@transaction object { + val uuid = paymentEntity.id.value + } + } + call.respond( + HttpStatusCode.OK, + PaymentInitiationResponse(uuid = res.uuid.toString()) + ) + return@post + } + + // Downloads new transactions from the bank. + post("/bank-accounts/{accountid}/fetch-transactions") { + requireSuperuser(call.request) + val accountid = call.parameters["accountid"] + if (accountid == null) { + throw NexusError( + HttpStatusCode.BadRequest, + "Account id missing" + ) + } + val fetchSpec = if (call.request.hasBody()) { + call.receive() + } else { + FetchSpecLatestJson( + FetchLevel.STATEMENT, + null + ) + } + + val ingestionResult = fetchBankAccountTransactions(client, fetchSpec, accountid) + val transactionId = lastTransactionId + + if (transactionId != null) { + call.response.header("Transaction-ID", transactionId) + logger.debug("transactionId:"+transactionId) + } else { + logger.debug("no transactionId") + } + + call.respond(ingestionResult) + return@post + } + + + // Asks list of transactions ALREADY downloaded from the bank. + get("/bank-accounts/{accountid}/transactions") { + requireSuperuser(call.request) + val bankAccountId = expectNonNull(call.parameters["accountid"]) + val ret = Transactions() + transaction { + authenticateRequest(call.request) + val bankAccount = NexusBankAccountEntity.findByName(bankAccountId) + if (bankAccount == null) { + throw NexusError(HttpStatusCode.NotFound, "unknown bank account") + } + NexusBankTransactionEntity.find { NexusBankTransactionsTable.bankAccount eq bankAccount.id }.map { + val tx = jacksonObjectMapper().readValue( + it.transactionJson, CamtBankAccountEntry::class.java + ) + ret.transactions.add(tx) + } + } + call.respond(ret) + return@get + } + + // Adds a new bank transport. + post("/bank-connections") { + requireSuperuser(call.request) + // user exists and is authenticated. + val body = call.receive() + requireValidResourceName(body.name) + transaction { + val user = authenticateRequest(call.request) + val existingConn = + NexusBankConnectionEntity.find { NexusBankConnectionsTable.connectionId eq body.name } + .firstOrNull() + if (existingConn != null) { + throw NexusError(HttpStatusCode.Conflict, "connection '${body.name}' exists already") + } + when (body) { + is CreateBankConnectionFromBackupRequestJson -> { + val type = body.data.get("type") + if (type == null || !type.isTextual) { + throw NexusError(HttpStatusCode.BadRequest, "backup needs type") + } + val plugin = getConnectionPlugin(type.textValue()) + plugin.createConnectionFromBackup(body.name, user, body.passphrase, body.data) + } + is CreateBankConnectionFromNewRequestJson -> { + val plugin = getConnectionPlugin(body.type) + plugin.createConnection(body.name, user, body.data) + } + } + } + call.respond(object {}) + } + + post("/bank-connections/delete-connection") { + requireSuperuser(call.request) + val body = call.receive() + transaction { + val conn = + NexusBankConnectionEntity.find { NexusBankConnectionsTable.connectionId eq body.bankConnectionId } + .firstOrNull() ?: throw NexusError( + HttpStatusCode.NotFound, + "Bank connection ${body.bankConnectionId}" + ) + conn.delete() // temporary, and instead just _mark_ it as deleted? + } + call.respond(object {}) + } + + get("/bank-connections") { + requireSuperuser(call.request) + val connList = BankConnectionsList() + transaction { + NexusBankConnectionEntity.all().forEach { + connList.bankConnections.add( + BankConnectionInfo( + name = it.connectionId, + type = it.type + ) + ) + } + } + call.respond(connList) + } + + get("/bank-connections/{connectionName}") { + requireSuperuser(call.request) + val resp = transaction { + val conn = requireBankConnection(call, "connectionName") + getConnectionPlugin(conn.type).getConnectionDetails(conn) + } + call.respond(resp) + } + + post("/bank-connections/{connectionName}/export-backup") { + requireSuperuser(call.request) + transaction { authenticateRequest(call.request) } + val body = call.receive() + val response = run { + val conn = requireBankConnection(call, "connectionName") + getConnectionPlugin(conn.type).exportBackup(conn.connectionId, body.passphrase) + } + call.response.headers.append("Content-Disposition", "attachment") + call.respond( + HttpStatusCode.OK, + response + ) + } + + post("/bank-connections/{connectionName}/connect") { + requireSuperuser(call.request) + val conn = transaction { + authenticateRequest(call.request) + requireBankConnection(call, "connectionName") + } + val plugin = getConnectionPlugin(conn.type) + plugin.connect(client, conn.connectionId) + call.respond(NexusMessage(message = "Connection successful")) + } + + get("/bank-connections/{connectionName}/keyletter") { + requireSuperuser(call.request) + val conn = transaction { + authenticateRequest(call.request) + requireBankConnection(call, "connectionName") + } + val pdfBytes = getConnectionPlugin(conn.type).exportAnalogDetails(conn) + call.respondBytes(pdfBytes, ContentType("application", "pdf")) + } + + get("/bank-connections/{connectionName}/messages") { + requireSuperuser(call.request) + val ret = transaction { + val list = BankMessageList() + val conn = requireBankConnection(call, "connectionName") + NexusBankMessageEntity.find { NexusBankMessagesTable.bankConnection eq conn.id }.map { + list.bankMessages.add( + BankMessageInfo( + it.messageId, + it.code, + it.message.bytes.size.toLong() + ) + ) + } + list + } + call.respond(ret) + } + + get("/bank-connections/{connid}/messages/{msgid}") { + requireSuperuser(call.request) + val ret = transaction { + val msgid = call.parameters["msgid"] + if (msgid == null || msgid == "") { + throw NexusError(HttpStatusCode.BadRequest, "missing or invalid message ID") + } + val msg = NexusBankMessageEntity.find { NexusBankMessagesTable.messageId eq msgid }.firstOrNull() + ?: throw NexusError(HttpStatusCode.NotFound, "bank message not found") + return@transaction object { + val msgContent = msg.message.bytes + } + } + call.respondBytes(ret.msgContent, ContentType("application", "xml")) + } + + get("/facades/{fcid}") { + requireSuperuser(call.request) + val fcid = ensureNonNull(call.parameters["fcid"]) + val ret = transaction { + val f = FacadeEntity.findByName(fcid) ?: throw NexusError( + HttpStatusCode.NotFound, "Facade $fcid does not exist" + ) + // FIXME: this only works for TWG urls. + FacadeShowInfo( + name = f.facadeName, + type = f.type, + baseUrl = URLBuilder(call.request.getBaseUrl()).apply { + pathComponents("facades", f.facadeName, f.type) + encodedPath += "/" + }.buildString(), + config = getFacadeState(f.type, f) + ) + } + call.respond(ret) + return@get + } + + get("/facades") { + requireSuperuser(call.request) + val ret = object { + val facades = mutableListOf() + } + transaction { + val user = authenticateRequest(call.request) + FacadeEntity.find { + FacadesTable.creator eq user.id + }.forEach { + ret.facades.add( + FacadeShowInfo( + name = it.facadeName, + type = it.type, + baseUrl = URLBuilder(call.request.getBaseUrl()).apply { + pathComponents("facades", it.facadeName, it.type) + encodedPath += "/" + }.buildString(), + config = getFacadeState(it.type, it) + ) + ) + } + } + call.respond(ret) + return@get + } + + delete("/facades/{fcid}") { + requireSuperuser(call.request) + val fcid = ensureNonNull(call.parameters["fcid"]) + transaction { + val f = FacadeEntity.findByName(fcid) ?: throw NexusError( + HttpStatusCode.NotFound, "Facade $fcid does not exist" + ) + f.delete() + } + call.respond({}) + return@delete + } + + post("/facades") { + requireSuperuser(call.request) + val body = call.receive() + requireValidResourceName(body.name) + if (!listOf("taler-wire-gateway", "anastasis").contains(body.type)) + throw NexusError( + HttpStatusCode.NotImplemented, + "Facade type '${body.type}' is not implemented" + ) + val newFacade = try { + transaction { + val user = authenticateRequest(call.request) + FacadeEntity.new { + facadeName = body.name + type = body.type + creator = user + } + } + } catch (e: ExposedSQLException) { + logger.error("Could not persist facade name/type/creator: $e") + throw NexusError( + HttpStatusCode.BadRequest, + "Server could not persist data, possibly due to unavailable facade name" + ) + } + transaction { + FacadeStateEntity.new { + bankAccount = body.config.bankAccount + bankConnection = body.config.bankConnection + reserveTransferLevel = body.config.reserveTransferLevel + facade = newFacade + currency = body.config.currency + } + } + call.respondText("Facade created") + return@post + } + + route("/bank-connections/{connid}") { + + // only ebics specific tasks under this part. + route("/ebics") { + ebicsBankConnectionRoutes(client) + } + post("/fetch-accounts") { + requireSuperuser(call.request) + val conn = transaction { + authenticateRequest(call.request) + requireBankConnection(call, "connid") + } + getConnectionPlugin(conn.type).fetchAccounts(client, conn.connectionId) + call.respond(object {}) + } + + // show all the offered accounts (both imported and non) + get("/accounts") { + requireSuperuser(call.request) + val ret = OfferedBankAccounts() + transaction { + val conn = requireBankConnection(call, "connid") + OfferedBankAccountEntity.find { + OfferedBankAccountsTable.bankConnection eq conn.id.value + }.forEach { offeredAccount -> + val importedId = offeredAccount.imported?.id + val imported = if (importedId != null) { + NexusBankAccountEntity.findById(importedId) + } else { + null + } + ret.accounts.add( + OfferedBankAccount( + ownerName = offeredAccount.accountHolder, + iban = offeredAccount.iban, + bic = offeredAccount.bankCode, + offeredAccountId = offeredAccount.offeredAccountId, + nexusBankAccountId = imported?.bankAccountName + ) + ) + } + } + call.respond(ret) + } + + // import one account into libeufin. + post("/import-account") { + requireSuperuser(call.request) + val body = call.receive() + importBankAccount(call, body.offeredAccountId, body.nexusBankAccountId) + call.respond(object {}) + } + } + route("/facades/{fcid}/taler-wire-gateway") { + talerFacadeRoutes(this) + } + route("/facades/{fcid}/anastasis") { + anastasisFacadeRoutes(this, client) + } + + // Hello endpoint. + get("/") { + call.respondText("Hello, this is Nexus.\n") + return@get + } + } + } + fun serverMain(host: String, port: Int) { + val server = embeddedServer(Netty, port = port, host = host, module = nexusApp) + logger.info("LibEuFin Nexus running on port $port") + try { + server.start(wait = true) + } catch (e: BindException) { + logger.error(e.message) + exitProcess(1) + } + } + \ No newline at end of file