From adb90be2791aa8b685bd01b6afe2979e462d2b5a Mon Sep 17 00:00:00 2001 From: Javi Pacheco Date: Wed, 27 Sep 2023 13:57:26 +0200 Subject: [PATCH] Endpoints for Tokens (#461) * Endpoints for tokens * Typo * Test fixed --- .../docs/postman/xef_postman_collection.json | 205 +++++++++++++++++- .../server/exceptions/ExceptionsHandler.kt | 1 + .../xef/server/http/routes/TokensRoutes.kt | 61 ++++++ .../xef/server/http/routes/XefRoutes.kt | 3 +- .../xef/server/models/ProvidersConfig.kt | 13 +- .../functional/xef/server/models/Requests.kt | 12 + .../functional/xef/server/models/Responses.kt | 19 +- .../server/models/exceptions/Exceptions.kt | 1 + .../services/ProjectRepositoryService.kt | 2 +- .../server/services/TokenRepositoryService.kt | 129 +++++++++++ .../xef/server/postgresql/XefDatabaseTest.kt | 1 - 11 files changed, 430 insertions(+), 17 deletions(-) create mode 100644 server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/TokensRoutes.kt create mode 100644 server/src/main/kotlin/com/xebia/functional/xef/server/services/TokenRepositoryService.kt diff --git a/server/docs/postman/xef_postman_collection.json b/server/docs/postman/xef_postman_collection.json index f88162f67..9c956cd8d 100644 --- a/server/docs/postman/xef_postman_collection.json +++ b/server/docs/postman/xef_postman_collection.json @@ -366,7 +366,7 @@ "response": [] }, { - "name": "Get Project", + "name": "Get Projects By Org", "protocolProfileBehavior": { "disableBodyPruning": true }, @@ -393,7 +393,7 @@ } }, "url": { - "raw": "{{url}}/v1/settings/projects/1", + "raw": "{{url}}/v1/settings/projects/org/1", "host": [ "{{url}}" ], @@ -401,6 +401,7 @@ "v1", "settings", "projects", + "org", "1" ] } @@ -423,19 +424,215 @@ "method": "DELETE", "header": [], "url": { - "raw": "{{url}}/v1/settings/projects/1", + "raw": "{{url}}/v1/settings/projects/4", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "settings", + "projects", + "4" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Xef Tokens", + "item": [ + { + "name": "Create Token", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"My Token\",\n \"projectId\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/settings/tokens", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "settings", + "tokens" + ] + } + }, + "response": [] + }, + { + "name": "Update Token", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"My Token\",\n \"providerConfig\": {\n \"open_ai\": {\n \"token\": \"openai_token\",\n \"url\": null\n },\n \"gcp\": {\n \"token\": \"openai_token\",\n \"project_id\": \"my_project_id\",\n \"location\": \"my_location\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/settings/tokens/1", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "settings", + "tokens", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Get Tokens", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/settings/tokens", "host": [ "{{url}}" ], "path": [ "v1", "settings", + "tokens" + ] + } + }, + "response": [] + }, + { + "name": "Get Tokens by Project", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/settings/tokens/projects/1", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "settings", + "tokens", "projects", "1" ] } }, "response": [] + }, + { + "name": "Delete tokens", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{url}}/v1/settings/tokens/1", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "settings", + "tokens", + "1" + ] + } + }, + "response": [] } ] }, @@ -472,7 +669,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"name\": \"JC2\",\n \"email\": \"jc2@xebia.com\",\n \"password\": \"1234\"\n}", + "raw": "{\n \"name\": \"JC\",\n \"email\": \"jc2@xebia.com\",\n \"password\": \"1234\"\n}", "options": { "raw": { "language": "json" 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 0eaee384a..aa688fcaa 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 @@ -27,6 +27,7 @@ suspend fun ApplicationCall.manageException(cause: XefExceptions) { is XefExceptions.AuthorizationException -> this.respond(HttpStatusCode.Unauthorized) is XefExceptions.OrganizationsException -> this.respond(HttpStatusCode.BadRequest, cause.message) is XefExceptions.ProjectException -> this.respond(HttpStatusCode.BadRequest, cause.message) + is XefExceptions.XefTokenException -> 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/TokensRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/TokensRoutes.kt new file mode 100644 index 000000000..caab0b29e --- /dev/null +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/TokensRoutes.kt @@ -0,0 +1,61 @@ +package com.xebia.functional.xef.server.http.routes + +import com.xebia.functional.xef.server.models.TokenRequest +import com.xebia.functional.xef.server.models.TokenUpdateRequest +import com.xebia.functional.xef.server.services.TokenRepositoryService +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.json.Json + +fun Routing.tokensRoutes( + tokenRepositoryService: TokenRepositoryService +) { + authenticate("auth-bearer") { + get("/v1/settings/tokens") { + val token = call.getToken() + val response = tokenRepositoryService.getTokens(token) + call.respond(response) + } + get("/v1/settings/tokens/projects/{id}") { + val token = call.getToken() + val id = call.getId() + val response = tokenRepositoryService.getTokensByProject(token, id) + call.respond(response) + } + post("/v1/settings/tokens") { + + val request = Json.decodeFromString(call.receive()) + val token = call.getToken() + val response = tokenRepositoryService.createToken(request, token) + call.respond( + status = HttpStatusCode.Created, + response + ) + } + put("/v1/settings/tokens/{id}") { + val request = Json.decodeFromString(call.receive()) + val token = call.getToken() + val id = call.getId() + val response = tokenRepositoryService.updateToken(token, request, id) + call.respond( + status = HttpStatusCode.NoContent, + response + ) + } + delete("/v1/settings/tokens/{id}") { + val token = call.getToken() + val id = call.getId() + val response = tokenRepositoryService.deleteToken(token, id) + call.respond( + status = HttpStatusCode.NoContent, + 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 7d4e9ed9b..1fb2ff2cf 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 @@ -2,8 +2,8 @@ package com.xebia.functional.xef.server.http.routes import com.xebia.functional.xef.server.services.OrganizationRepositoryService import com.xebia.functional.xef.server.services.ProjectRepositoryService +import com.xebia.functional.xef.server.services.TokenRepositoryService import com.xebia.functional.xef.server.services.UserRepositoryService -import io.ktor.client.* import io.ktor.server.routing.* import org.slf4j.Logger @@ -13,4 +13,5 @@ fun Routing.xefRoutes( userRoutes(UserRepositoryService(logger)) organizationRoutes(OrganizationRepositoryService(logger)) projectsRoutes(ProjectRepositoryService(logger)) + tokensRoutes(TokenRepositoryService(logger)) } diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/models/ProvidersConfig.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/models/ProvidersConfig.kt index 359c52696..9a967335e 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/models/ProvidersConfig.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/models/ProvidersConfig.kt @@ -4,18 +4,15 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -@SerialName("open_ai") data class OpenAIConf( - val name: String, val token: String, - val url: String + val url: String? ) @Serializable -@SerialName("gcp") data class GCPConf( - val name: String, val token: String, + @SerialName("project_id") val projectId: String, val location: String ) @@ -26,4 +23,8 @@ data class ProvidersConfig( val openAI: OpenAIConf?, @SerialName("gcp") val gcp: GCPConf? -) +) { + companion object { + val empty = ProvidersConfig(null, null) + } +} diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/models/Requests.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/models/Requests.kt index e561a2445..5d382e707 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/models/Requests.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/models/Requests.kt @@ -37,3 +37,15 @@ data class ProjectUpdateRequest( val name: String, val orgId: Int? = null ) + +@Serializable +data class TokenRequest( + val name: String, + val projectId: Int +) + +@Serializable +data class TokenUpdateRequest( + val name: String, + val providerConfig: ProvidersConfig +) diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/models/Responses.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/models/Responses.kt index cb5bb14eb..f9611415e 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/models/Responses.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/models/Responses.kt @@ -3,6 +3,7 @@ package com.xebia.functional.xef.server.models import com.xebia.functional.xef.server.db.tables.Organization import com.xebia.functional.xef.server.db.tables.Project import com.xebia.functional.xef.server.db.tables.User +import com.xebia.functional.xef.server.db.tables.XefTokens import kotlinx.serialization.Serializable @Serializable @@ -14,9 +15,9 @@ data class UserResponse(val id: Int, val name: String) fun User.toUserResponse() = UserResponse(id.value, name) @Serializable -data class OrganizationSimpleResponse(val name: String) +data class OrganizationSimpleResponse(val id: Int, val name: String) -fun Organization.toOrganizationSimpleResponse() = OrganizationSimpleResponse(name) +fun Organization.toOrganizationSimpleResponse() = OrganizationSimpleResponse(id.value, name) @Serializable data class OrganizationFullResponse(val id: Int, val name: String, val owner: Int, val users: Long) @@ -24,11 +25,21 @@ data class OrganizationFullResponse(val id: Int, val name: String, val owner: In fun Organization.toOrganizationFullResponse() = OrganizationFullResponse(id.value, name, ownerId.value, users.count()) @Serializable -data class ProjectSimpleResponse(val name: String, val orgId: Int) +data class ProjectSimpleResponse(val id: Int, val name: String, val orgId: Int) -fun Project.toProjectSimpleResponse() = ProjectSimpleResponse(name, orgId.value) +fun Project.toProjectSimpleResponse() = ProjectSimpleResponse(id.value, name, orgId.value) @Serializable data class ProjectFullResponse(val id: Int, val name: String, val org: OrganizationFullResponse) fun Project.toProjectFullResponse(org: Organization) = ProjectFullResponse(id.value, name, org.toOrganizationFullResponse()) + +@Serializable +data class TokenSimpleResponse(val id: Int, val projectId: Int, val userId : Int, val name: String, val token: String) + +fun XefTokens.toTokenSimpleResponse() = TokenSimpleResponse(id.value, projectId.value, userId.value, name, token) + +@Serializable +data class TokenFullResponse(val id: Int, val project: ProjectSimpleResponse, val name: String, val token: String) + +fun XefTokens.toTokenFullResponse(project: ProjectSimpleResponse) = TokenFullResponse(id.value, project, name, token) 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 index 1507cead9..fba942d15 100644 --- 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 @@ -6,6 +6,7 @@ sealed class XefExceptions( class ValidationException(override val message: String): XefExceptions(message) class OrganizationsException(override val message: String): XefExceptions(message) class ProjectException(override val message: String): XefExceptions(message) + class XefTokenException(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/ProjectRepositoryService.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/services/ProjectRepositoryService.kt index d1bcac9b6..033ff5d60 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/services/ProjectRepositoryService.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/services/ProjectRepositoryService.kt @@ -29,7 +29,7 @@ class ProjectRepositoryService( name = data.name orgId = organization.id } - ProjectSimpleResponse(project.name, project.orgId.value) + project.toProjectSimpleResponse() } } diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/services/TokenRepositoryService.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/services/TokenRepositoryService.kt new file mode 100644 index 000000000..2f5b3cc41 --- /dev/null +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/services/TokenRepositoryService.kt @@ -0,0 +1,129 @@ +package com.xebia.functional.xef.server.services + +import com.xebia.functional.xef.server.db.tables.* +import com.xebia.functional.xef.server.models.* +import com.xebia.functional.xef.server.models.exceptions.XefExceptions.* +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.slf4j.Logger +import java.util.UUID + +class TokenRepositoryService( + private val logger: Logger +) { + fun createToken( + data: TokenRequest, + userToken: Token + ): TokenSimpleResponse { + return transaction { + val user = userToken.getUser() + + val project = + Project.findById(data.projectId) ?: throw OrganizationsException("Project not found") + + logger.info("Creating token with name ${data.name} from project ${project.name}") + + if (user.organizations.none { it.id == project.orgId }) { + throw OrganizationsException("User is not part of the organization of the ${project.name} project") + } + + val newXefToken = XefTokens.new { + name = data.name + userId = user.id + projectId = project.id + token = UUID.randomUUID().toString() + providersConfig = ProvidersConfig.empty + } + + newXefToken.toTokenSimpleResponse() + } + } + + fun getTokens( + userToken: Token + ): List { + logger.info("Getting tokens") + return transaction { + val user = userToken.getUser() + + val userProjects = + Project.find { ProjectsTable.orgId inList user.organizations.map { it.id } }.mapNotNull { project -> + val org = user.organizations.find { org -> org.id == project.orgId } + org?.let { + project.toProjectSimpleResponse() + } + } + + XefTokens.find { XefTokensTable.userId eq user.id }.mapNotNull { xefToken -> + userProjects.find { project -> project.id == xefToken.projectId.value }?.let { + xefToken.toTokenFullResponse(it) + } + } + + } + } + + fun getTokensByProject( + userToken: Token, + projectId: Int + ): List { + logger.info("Getting tokens by project") + return transaction { + val user = userToken.getUser() + + XefTokens.find((XefTokensTable.userId eq user.id) and (XefTokensTable.projectId eq projectId)).map { xefToken -> + val project = Project.findById(xefToken.projectId) ?: throw ProjectException("Project not found") + xefToken.toTokenFullResponse(project.toProjectSimpleResponse()) + } + } + } + + fun updateToken( + userToken: Token, + data: TokenUpdateRequest, + id: Int + ): TokenFullResponse { + logger.info("Updating token with name: ${data.name}") + return transaction { + val user = userToken.getUser() + + val xefToken = XefTokens.findById(id) ?: throw XefTokenException("Token not found") + + val project = Project.findById(xefToken.projectId) ?: throw ProjectException("Project not found") + + if (user.organizations.none { it.id == project.orgId }) { + throw OrganizationsException("User is not part of the organization of this project") + } + + val xefTokenUpdated = xefToken.apply { + name = data.name + providersConfig = data.providerConfig + } + + xefTokenUpdated.toTokenFullResponse(project.toProjectSimpleResponse()) + } + } + + fun deleteToken( + userToken: Token, + id: Int + ) { + logger.info("Deleting token: $id") + transaction { + val user = userToken.getUser() + + val xefToken = XefTokens.findById(id) ?: throw XefTokenException("Token not found") + + val project = Project.findById(xefToken.projectId) ?: throw ProjectException("Project not found") + + if (user.organizations.none { it.id == project.orgId }) { + throw OrganizationsException("User is not part of the organization of this project") + } + + XefTokensTable.deleteWhere { + this.id eq id + } + } + } +} diff --git a/server/src/test/kotlin/com/xebia/functional/xef/server/postgresql/XefDatabaseTest.kt b/server/src/test/kotlin/com/xebia/functional/xef/server/postgresql/XefDatabaseTest.kt index c15354048..00cbabc01 100644 --- a/server/src/test/kotlin/com/xebia/functional/xef/server/postgresql/XefDatabaseTest.kt +++ b/server/src/test/kotlin/com/xebia/functional/xef/server/postgresql/XefDatabaseTest.kt @@ -133,7 +133,6 @@ class XefDatabaseTest { val config = ProvidersConfig( openAI = OpenAIConf( - name = "dev", token = "testToken", url = "testUrl" ),