From e59321bf52ac103133e9934c051e34143d452e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Carlos=20Montan=CC=83ez?= Date: Fri, 15 Sep 2023 11:31:23 +0200 Subject: [PATCH 1/2] added exceptions handler management in server --- gradle/libs.versions.toml | 1 + server/build.gradle.kts | 1 + .../com/xebia/functional/xef/server/Server.kt | 8 +- .../server/exceptions/ExceptionsHandler.kt | 32 ++++++ .../server/http/routes/OrganizationRoutes.kt | 99 ++++++++----------- .../xef/server/http/routes/UserRoutes.kt | 20 ++-- .../xef/server/http/routes/XefRoutes.kt | 16 +-- .../server/models/exceptions/Exceptions.kt | 10 ++ .../services/OrganizationRepositoryService.kt | 11 ++- .../server/services/UserRepositoryService.kt | 6 +- 10 files changed, 107 insertions(+), 97 deletions(-) create mode 100644 server/src/main/kotlin/com/xebia/functional/xef/server/exceptions/ExceptionsHandler.kt create mode 100644 server/src/main/kotlin/com/xebia/functional/xef/server/models/exceptions/Exceptions.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c11edaee7..bca7f2cab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -83,6 +83,7 @@ ktor-server-contentNegotiation = { module = "io.ktor:ktor-server-content-negotia ktor-server-resources = { module = "io.ktor:ktor-server-resources", version.ref = "ktor" } ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" } ktor-server-request-validation = { module = "io.ktor:ktor-server-request-validation", version.ref = "ktor" } +ktor-server-status-pages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" } okio-nodefilesystem = { module = "com.squareup.okio:okio-nodefilesystem", version.ref = "okio" } diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 9b6ec4453..e9d3a0baa 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(libs.ktor.server.resources) implementation(libs.ktor.server.cors) implementation(libs.ktor.server.request.validation) + implementation(libs.ktor.server.status.pages) implementation(libs.logback) implementation(libs.openai.client) implementation(libs.suspendApp.core) diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt index dc6146a85..29718ff8e 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt @@ -1,13 +1,14 @@ package com.xebia.functional.xef.server import arrow.continuations.SuspendApp -import arrow.fx.coroutines.resourceScope import arrow.continuations.ktor.server +import arrow.fx.coroutines.resourceScope import com.typesafe.config.ConfigFactory -import com.xebia.functional.xef.server.db.psql.XefDatabaseConfig import com.xebia.functional.xef.server.db.psql.Migrate +import com.xebia.functional.xef.server.db.psql.XefDatabaseConfig import com.xebia.functional.xef.server.db.psql.XefVectorStoreConfig import com.xebia.functional.xef.server.db.psql.XefVectorStoreConfig.Companion.getVectorStoreService +import com.xebia.functional.xef.server.exceptions.exceptionsHandler import com.xebia.functional.xef.server.http.routes.genAIRoutes import com.xebia.functional.xef.server.http.routes.organizationRoutes import com.xebia.functional.xef.server.http.routes.userRoutes @@ -16,7 +17,6 @@ import com.xebia.functional.xef.server.services.RepositoryService import com.xebia.functional.xef.server.services.UserRepositoryService import io.ktor.client.* import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation import io.ktor.client.plugins.auth.* import io.ktor.client.plugins.logging.* import io.ktor.serialization.kotlinx.json.* @@ -30,6 +30,7 @@ import io.ktor.server.routing.* import kotlinx.coroutines.awaitCancellation import org.jetbrains.exposed.sql.Database import org.slf4j.LoggerFactory +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation object Server { @JvmStatic @@ -78,6 +79,7 @@ object Server { } } } + exceptionsHandler() routing { genAIRoutes(ktorClient, vectorStoreService) userRoutes(UserRepositoryService(logger)) diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/exceptions/ExceptionsHandler.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/exceptions/ExceptionsHandler.kt new file mode 100644 index 000000000..845336bd8 --- /dev/null +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/exceptions/ExceptionsHandler.kt @@ -0,0 +1,32 @@ +package com.xebia.functional.xef.server.exceptions + +import com.xebia.functional.xef.server.models.exceptions.XefExceptions +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.plugins.statuspages.* +import io.ktor.server.response.* + +fun Application.exceptionsHandler() { + install(StatusPages) { + exception { call, cause -> + if (cause is XefExceptions) { + call.manageException(cause) + } else { + call.respond(HttpStatusCode.InternalServerError, cause.message ?: "Unexpected error") + } + call.respondText(cause.localizedMessage, status = HttpStatusCode.InternalServerError) + } + status(HttpStatusCode.NotFound) { call, status -> + call.respondText(text = "404: Page Not Found", status = status) + } + } +} + +suspend fun ApplicationCall.manageException(cause: XefExceptions) { + when(cause) { + is XefExceptions.ValidationException -> this.respond(HttpStatusCode.BadRequest, cause.message) + is XefExceptions.AuthorizationException -> this.respond(HttpStatusCode.Unauthorized) + is XefExceptions.OrganizationsException -> this.respond(HttpStatusCode.BadRequest, cause.message) + is XefExceptions.UserException -> this.respond(HttpStatusCode.BadRequest, cause.message) + } +} diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/OrganizationRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/OrganizationRoutes.kt index 2c05c99dc..84bd86032 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/OrganizationRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/OrganizationRoutes.kt @@ -2,6 +2,7 @@ package com.xebia.functional.xef.server.http.routes import com.xebia.functional.xef.server.models.OrganizationRequest import com.xebia.functional.xef.server.models.OrganizationUpdateRequest +import com.xebia.functional.xef.server.models.exceptions.XefExceptions import com.xebia.functional.xef.server.services.OrganizationRepositoryService import io.ktor.http.* import io.ktor.server.application.* @@ -16,74 +17,56 @@ fun Routing.organizationRoutes( ) { authenticate("auth-bearer") { get("/v1/settings/org") { - try { - val token = call.getToken() - val response = orgRepositoryService.getOrganizations(token) - call.respond(response) - } catch (e: Exception) { - call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest) - } + val token = call.getToken() + val response = orgRepositoryService.getOrganizations(token) + call.respond(response) } get("/v1/settings/org/{id}") { - try { - val token = call.getToken() - val id = call.parameters["id"]?.toInt() ?: throw Exception("Invalid id") - val response = orgRepositoryService.getOrganization(token, id) - call.respond(response) - } catch (e: Exception) { - call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest) - } + + val token = call.getToken() + val id = call.getOrganizationId() + val response = orgRepositoryService.getOrganization(token, id) + call.respond(response) } - get("/v1/settings/org/{id}/users"){ - try { - val token = call.getToken() - val id = call.parameters["id"]?.toInt() ?: throw Exception("Invalid id") - val response = orgRepositoryService.getUsersInOrganization(token, id) - call.respond(response) - } catch (e: Exception) { - call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest) - } + get("/v1/settings/org/{id}/users") { + val token = call.getToken() + val id = call.getOrganizationId() + val response = orgRepositoryService.getUsersInOrganization(token, id) + call.respond(response) } post("/v1/settings/org") { - try { - val request = Json.decodeFromString(call.receive()) - val token = call.getToken() - val response = orgRepositoryService.createOrganization(request, token) - call.respond( - status = HttpStatusCode.Created, - response - ) - } catch (e: Exception) { - call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest) - } + + val request = Json.decodeFromString(call.receive()) + val token = call.getToken() + val response = orgRepositoryService.createOrganization(request, token) + call.respond( + status = HttpStatusCode.Created, + response + ) } put("/v1/settings/org/{id}") { - try { - val request = Json.decodeFromString(call.receive()) - val token = call.getToken() - val id = call.parameters["id"]?.toInt() ?: throw Exception("Invalid id") - val response = orgRepositoryService.updateOrganization(token, request, id) - call.respond( - status = HttpStatusCode.NoContent, - response - ) - } catch (e: Exception) { - call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest) - } + val request = Json.decodeFromString(call.receive()) + val token = call.getToken() + val id = call.getOrganizationId() + val response = orgRepositoryService.updateOrganization(token, request, id) + call.respond( + status = HttpStatusCode.NoContent, + response + ) } delete("/v1/settings/org/{id}") { - try { - val token = call.getToken() - val id = call.parameters["id"]?.toInt() ?: throw Exception("Invalid id") - val response = orgRepositoryService.deleteOrganization(token, id) - call.respond( - status = HttpStatusCode.NoContent, - response - ) - } catch (e: Exception) { - call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest) - } + val token = call.getToken() + val id = call.getOrganizationId() + val response = orgRepositoryService.deleteOrganization(token, id) + call.respond( + status = HttpStatusCode.NoContent, + response + ) } } } +private fun ApplicationCall.getOrganizationId(): Int { + return this.parameters["id"]?.toInt() ?: throw XefExceptions.ValidationException("Invalid id") +} + diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/UserRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/UserRoutes.kt index 7fa98368b..9b2681567 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/UserRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/UserRoutes.kt @@ -14,22 +14,14 @@ fun Routing.userRoutes( userRepositoryService: UserRepositoryService ) { post("/register") { - try { - val request = Json.decodeFromString(call.receive()) - val response = userRepositoryService.register(request) - call.respond(response) - } catch (e: Exception) { - call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest) - } + val request = Json.decodeFromString(call.receive()) + val response = userRepositoryService.register(request) + call.respond(response) } post("/login") { - try { - val request = Json.decodeFromString(call.receive()) - val response = userRepositoryService.login(request) - call.respond(response) - } catch (e: Exception) { - call.respondText(e.message ?: "Unexpected error", status = HttpStatusCode.BadRequest) - } + val request = Json.decodeFromString(call.receive()) + val response = userRepositoryService.login(request) + call.respond(response) } } diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/XefRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/XefRoutes.kt index e529efe09..cbd6b6dd8 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/XefRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/XefRoutes.kt @@ -1,6 +1,7 @@ package com.xebia.functional.xef.server.http.routes import com.aallam.openai.api.BetaOpenAI +import com.xebia.functional.xef.server.models.exceptions.XefExceptions import com.xebia.functional.xef.server.services.VectorStoreService import io.ktor.client.* import io.ktor.client.call.* @@ -12,7 +13,6 @@ import io.ktor.server.auth.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -import io.ktor.util.pipeline.* import io.ktor.utils.io.jvm.javaio.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject @@ -113,16 +113,4 @@ private fun ApplicationCall.getProvider(): Provider = ?: Provider.OPENAI fun ApplicationCall.getToken(): String = - principal()?.name ?: throw IllegalArgumentException("No token found") - - -/** - * Responds with the data and converts any potential Throwable into a 404. - */ -private suspend inline fun PipelineContext<*, ApplicationCall>.response( - block: () -> T -) = arrow.core.raise.recover({ - call.respond(block()) -}) { - call.respondText(it.message ?: "Response not found", status = HttpStatusCode.NotFound) -} + principal()?.name ?: throw XefExceptions.AuthorizationException("No token found") diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/models/exceptions/Exceptions.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/models/exceptions/Exceptions.kt new file mode 100644 index 000000000..64a97ab99 --- /dev/null +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/models/exceptions/Exceptions.kt @@ -0,0 +1,10 @@ +package com.xebia.functional.xef.server.models.exceptions + +sealed class XefExceptions( + override val message: String +): Throwable() { + class ValidationException(override val message: String): XefExceptions(message) + class OrganizationsException(override val message: String): XefExceptions(message) + class AuthorizationException(override val message: String): XefExceptions(message) + class UserException(override val message: String): XefExceptions(message) +} diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/services/OrganizationRepositoryService.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/services/OrganizationRepositoryService.kt index 203fe71cd..08330a0e9 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/services/OrganizationRepositoryService.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/services/OrganizationRepositoryService.kt @@ -4,6 +4,7 @@ import com.xebia.functional.xef.server.db.tables.Organization import com.xebia.functional.xef.server.db.tables.User import com.xebia.functional.xef.server.db.tables.UsersTable import com.xebia.functional.xef.server.models.* +import com.xebia.functional.xef.server.models.exceptions.XefExceptions.* import kotlinx.datetime.Clock import org.jetbrains.exposed.sql.SizedCollection import org.jetbrains.exposed.sql.transactions.transaction @@ -89,17 +90,17 @@ class OrganizationRepositoryService( val user = getUser(token) val organization = Organization.findById(id) - ?: throw Exception("Organization not found") + ?: throw OrganizationsException("Organization not found") if (organization.ownerId != user.id) { - throw Exception("User is not the owner of the organization") + throw OrganizationsException("User is not the owner of the organization") } // Updating the organization organization.name = data.name if (data.owner != null) { val newOwner = User.findById(data.owner) - ?: throw Exception("User not found") + ?: throw UserException("User not found") organization.ownerId = newOwner.id } organization.updatedAt = Clock.System.now() @@ -120,7 +121,7 @@ class OrganizationRepositoryService( transaction { val user = getUser(token) val organization = Organization.findById(id) - ?: throw Exception("Organization not found") + ?: throw OrganizationsException("Organization not found") if (organization.ownerId == user.id) { organization.delete() } @@ -128,5 +129,5 @@ class OrganizationRepositoryService( } private fun getUser(token: String) = - User.find { UsersTable.authToken eq token }.firstOrNull() ?: throw Exception("User not found") + User.find { UsersTable.authToken eq token }.firstOrNull() ?: throw UserException("User not found") } diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/services/UserRepositoryService.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/services/UserRepositoryService.kt index 89cd128cd..26703d6e2 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/services/UserRepositoryService.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/services/UserRepositoryService.kt @@ -5,8 +5,8 @@ import com.xebia.functional.xef.server.db.tables.UsersTable import com.xebia.functional.xef.server.models.LoginRequest import com.xebia.functional.xef.server.models.LoginResponse import com.xebia.functional.xef.server.models.RegisterRequest +import com.xebia.functional.xef.server.models.exceptions.XefExceptions.UserException import com.xebia.functional.xef.server.utils.HashUtils -import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.uuid.UUID import kotlinx.uuid.generateUUID import org.jetbrains.exposed.sql.transactions.transaction @@ -21,7 +21,7 @@ class UserRepositoryService( return transaction { if (User.find { UsersTable.email eq request.email }.count() > 0) { - throw Exception("User already exists") + throw UserException("User already exists") } val newSalt = HashUtils.generateSalt() @@ -43,7 +43,7 @@ class UserRepositoryService( logger.info("Login user ${request.email}") return transaction { val user = - User.find { UsersTable.email eq request.email }.firstOrNull() ?: throw Exception("User not found") + User.find { UsersTable.email eq request.email }.firstOrNull() ?: throw UserException("User not found") if (!HashUtils.checkPassword( request.password, From 003338ca0b0ac44c0c8d40aa84ce1e09ec02934c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Carlos=20Montan=CC=83ez?= Date: Mon, 18 Sep 2023 16:07:53 +0200 Subject: [PATCH 2/2] removed unnecessary response --- .../functional/xef/server/exceptions/ExceptionsHandler.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/exceptions/ExceptionsHandler.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/exceptions/ExceptionsHandler.kt index 845336bd8..52ed9c92b 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/exceptions/ExceptionsHandler.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/exceptions/ExceptionsHandler.kt @@ -12,9 +12,8 @@ fun Application.exceptionsHandler() { if (cause is XefExceptions) { call.manageException(cause) } else { - call.respond(HttpStatusCode.InternalServerError, cause.message ?: "Unexpected error") + call.respond(HttpStatusCode.InternalServerError, cause.localizedMessage ?: "Unexpected error") } - call.respondText(cause.localizedMessage, status = HttpStatusCode.InternalServerError) } status(HttpStatusCode.NotFound) { call, status -> call.respondText(text = "404: Page Not Found", status = status)