diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e064e4c66..d5c71f8bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,6 +39,7 @@ progressbar = "0.10.0" jmf = "2.1.1e" mp3-wav-converter = "1.0.4" yamlkt="0.13.0" +tegral = "0.0.4" [libraries] @@ -115,7 +116,13 @@ opentelemetry-extension-kotlin = { module = "io.opentelemetry:opentelemetry-exte progressbar = { module = "me.tongfei:progressbar", version.ref = "progressbar" } jmf = { module = "javax.media:jmf", version.ref = "jmf" } mp3-wav-converter = { module = "com.sipgate:mp3-wav", version.ref = "mp3-wav-converter" } - +tegral-catalog = { module = "guru.zoroark.tegral:tegral-catalog", version.ref = "tegral" } +tegral-core = { module = "guru.zoroark.tegral:tegral-core", version.ref = "tegral" } +tegral-openapi-dsl = { module = "guru.zoroark.tegral:tegral-openapi-dsl", version.ref = "tegral" } +tegral-openapi-scriptdef = { module = "guru.zoroark.tegral:tegral-openapi-scriptdef", version.ref = "tegral" } +tegral-openapi-ktor = { module = "guru.zoroark.tegral:tegral-openapi-ktor", version.ref = "tegral" } +tegral-openapi-ktor-resources = { module = "guru.zoroark.tegral:tegral-openapi-ktor-resources", version.ref = "tegral" } +tegral-openapi-ktorui = { module = "guru.zoroark.tegral:tegral-openapi-ktorui", version.ref = "tegral" } [bundles] diff --git a/server/build.gradle.kts b/server/build.gradle.kts index b9398e1e7..c473972cc 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -45,6 +45,12 @@ dependencies { implementation(libs.ktor.server.status.pages) implementation(libs.suspendApp.core) implementation(libs.suspendApp.ktor) + implementation(libs.tegral.core) + implementation(libs.tegral.openapi.dsl) + implementation(libs.tegral.openapi.scriptdef) + implementation(libs.tegral.openapi.ktor) + implementation(libs.tegral.openapi.ktor.resources) + implementation(libs.tegral.openapi.ktorui) implementation(libs.uuid) implementation(projects.xefCore) implementation(projects.xefPostgresql) 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 2566c167f..ff5bf31b6 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 @@ -11,6 +11,10 @@ import com.xebia.functional.xef.server.http.routes.xefRoutes import com.xebia.functional.xef.server.services.hikariDataSource import com.xebia.functional.xef.server.services.vectorStoreService import com.xebia.functional.xef.store.migrations.runDatabaseMigrations +import guru.zoroark.tegral.openapi.ktor.TegralOpenApiKtor +import guru.zoroark.tegral.openapi.ktor.openApiEndpoint +import guru.zoroark.tegral.openapi.ktorui.TegralSwaggerUiKtor +import guru.zoroark.tegral.openapi.ktorui.swaggerUiEndpoint import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO @@ -77,10 +81,14 @@ object Server { authenticate { tokenCredential -> UserIdPrincipal(tokenCredential.token) } } } + install(TegralOpenApiKtor) { title = "Xef Server" } + install(TegralSwaggerUiKtor) exceptionsHandler() routing { xefRoutes(logger) aiRoutes(ktorClient) + openApiEndpoint("/openapi") + swaggerUiEndpoint(path = "/docs", openApiPath = "/openapi") } } awaitCancellation() diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AIRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AIRoutes.kt index 5eebf337b..12486d47c 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AIRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AIRoutes.kt @@ -1,11 +1,16 @@ package com.xebia.functional.xef.server.http.routes +import com.xebia.functional.openai.generated.model.ChatCompletionRequestMessage import com.xebia.functional.xef.server.models.Token -import com.xebia.functional.xef.server.models.exceptions.XefExceptions +import guru.zoroark.tegral.openapi.dsl.schema +import guru.zoroark.tegral.openapi.ktor.resources.ResourceDescription +import guru.zoroark.tegral.openapi.ktor.resources.describeResource +import guru.zoroark.tegral.openapi.ktor.resources.postD import io.ktor.client.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* +import io.ktor.resources.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.request.* @@ -22,7 +27,7 @@ fun Routing.aiRoutes(client: HttpClient) { val openAiUrl = "https://api.openai.com/v1" authenticate("auth-bearer") { - post("/chat/completions") { + postD { val token = call.getToken() val byteArrayBody = call.receiveChannel().toByteArray() val body = byteArrayBody.toString(Charsets.UTF_8) @@ -37,7 +42,7 @@ fun Routing.aiRoutes(client: HttpClient) { } } - post("/embeddings") { + postD { val token = call.getToken() val context = call.receiveChannel().toByteArray() client.makeRequest(call, "$openAiUrl/embeddings", context, token) @@ -45,6 +50,40 @@ fun Routing.aiRoutes(client: HttpClient) { } } +@Resource("/chat/completions") +class ChatCompletionRoutes { + companion object : + ResourceDescription by describeResource({ + tags += "AI" + post { + description = "Create chat completions" + body { + description = "The chat details" + required = true + json { schema() } + } + HttpStatusCode.OK.value response { description = "Chat completions" } + } + }) +} + +@Resource("/embeddings") +class EmbeddingsRoutes { + companion object : + ResourceDescription by describeResource({ + tags += "AI" + post { + description = "Create embeddings" + body { + description = "The context" + required = true + json { schema() } + } + HttpStatusCode.OK.value response { description = "Embeddings" } + } + }) +} + private val conflictingRequestHeaders = listOf("Host", "Content-Type", "Content-Length", "Accept", "Accept-Encoding") private val conflictingResponseHeaders = listOf("Content-Length") @@ -97,12 +136,3 @@ internal fun HeadersBuilder.copyFrom(headers: Headers) = headers .filter { key, _ -> !conflictingRequestHeaders.any { it.equals(key, true) } } .forEach { key, values -> appendMissing(key, values) } - -fun ApplicationCall.getToken(): Token = - principal()?.name?.let { Token(it) } - ?: throw XefExceptions.AuthorizationException("No token found") - -fun ApplicationCall.getId(): Int = getInt("id") - -fun ApplicationCall.getInt(field: String): Int = - this.parameters[field]?.toInt() ?: throw XefExceptions.ValidationException("Invalid $field") diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/ApplicationCallExtensions.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/ApplicationCallExtensions.kt new file mode 100644 index 000000000..976f7a84f --- /dev/null +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/ApplicationCallExtensions.kt @@ -0,0 +1,23 @@ +package com.xebia.functional.xef.server.http.routes + +import arrow.core.raise.catch +import com.xebia.functional.xef.server.models.Token +import com.xebia.functional.xef.server.models.exceptions.XefExceptions +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import kotlinx.serialization.json.Json + +fun ApplicationCall.getToken(): Token = + principal()?.name?.let { Token(it) } + ?: throw XefExceptions.AuthorizationException("No token found") + +fun ApplicationCall.getId(): Int = getInt("id") + +fun ApplicationCall.getInt(field: String): Int = + this.parameters[field]?.toInt() ?: throw XefExceptions.ValidationException("Invalid $field") + +suspend inline fun ApplicationCall.decodeFromStringRequest(): T = + catch({ Json.decodeFromString(this.receive()) }) { + throw XefExceptions.ValidationException("Invalid ${T::class.simpleName}") + } 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 6d0af5ec9..72c56f98c 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 @@ -1,53 +1,142 @@ package com.xebia.functional.xef.server.http.routes +import com.xebia.functional.xef.server.models.OrganizationFullResponse import com.xebia.functional.xef.server.models.OrganizationRequest import com.xebia.functional.xef.server.models.OrganizationUpdateRequest +import com.xebia.functional.xef.server.models.UserResponse import com.xebia.functional.xef.server.services.OrganizationRepositoryService +import guru.zoroark.tegral.openapi.dsl.schema +import guru.zoroark.tegral.openapi.ktor.resources.* import io.ktor.http.* +import io.ktor.resources.* 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 +import io.ktor.server.routing.Routing +import io.swagger.v3.oas.models.media.StringSchema fun Routing.organizationRoutes(orgRepositoryService: OrganizationRepositoryService) { authenticate("auth-bearer") { - get("/v1/settings/org") { + getD { val token = call.getToken() val response = orgRepositoryService.getOrganizations(token) call.respond(response) } - get("/v1/settings/org/{id}") { + postD { + val request = call.decodeFromStringRequest() val token = call.getToken() - val id = call.getId() - val response = orgRepositoryService.getOrganization(token, id) - call.respond(response) + val response = orgRepositoryService.createOrganization(request, token) + call.respond(status = HttpStatusCode.Created, response) } - get("/v1/settings/org/{id}/users") { + getD { val token = call.getToken() val id = call.getId() - val response = orgRepositoryService.getUsersInOrganization(token, id) + val response = orgRepositoryService.getOrganization(token, id) call.respond(response) } - post("/v1/settings/org") { - 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}") { - val request = Json.decodeFromString(call.receive()) + putD { + val request = call.decodeFromStringRequest() val token = call.getToken() val id = call.getId() val response = orgRepositoryService.updateOrganization(token, request, id) call.respond(status = HttpStatusCode.NoContent, response) } - delete("/v1/settings/org/{id}") { + deleteD { val token = call.getToken() val id = call.getId() val response = orgRepositoryService.deleteOrganization(token, id) call.respond(status = HttpStatusCode.NoContent, response) } + getD { + val token = call.getToken() + val id = call.getId() + val response = orgRepositoryService.getUsersInOrganization(token, id) + call.respond(response) + } } } + +@Resource("/v1/settings/org") +class OrganizationRoutes { + companion object : + ResourceDescription by describeResource({ + get { + description = "Get all organizations" + tags += "Organization" + HttpStatusCode.OK.value response + { + description = "List of organizations" + json { schema>() } + } + } + post { + description = "Create an organization" + tags += "Organization" + body { + description = "The organization to create" + required = true + json { schema() } + } + } + }) +} + +@Resource("/v1/settings/org/{id}") +class OrganizationDetailsRoutes { + companion object : + ResourceDescription by describeResource({ + "id" pathParameter + { + description = "Organization ID" + required = true + schema = StringSchema() + example = "org_123" + } + get { + description = "Get organization details" + tags += "Organization" + HttpStatusCode.OK.value response + { + description = "Organization details" + json { schema() } + } + } + put { + description = "Update organization details" + tags += "Organization" + body { + description = "The organization details to update" + required = true + json { schema() } + } + HttpStatusCode.NoContent.value response { description = "Organization updated" } + } + delete { + description = "Delete organization" + tags += "Organization" + } + }) +} + +@Resource("/v1/settings/org/{id}/users") +class OrganizationUsersRoutes { + companion object : + ResourceDescription by describeResource({ + "id" pathParameter + { + description = "Organization ID" + required = true + schema = StringSchema() + example = "org_123" + } + get { + description = "Get users in organization" + tags += "Organization" + HttpStatusCode.OK.value response + { + description = "List of users in organization" + json { schema>() } + } + } + }) +} diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/ProjectsRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/ProjectsRoutes.kt index 8fe15e9f7..9d34f0991 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/ProjectsRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/ProjectsRoutes.kt @@ -1,53 +1,150 @@ package com.xebia.functional.xef.server.http.routes +import com.xebia.functional.xef.server.models.ProjectFullResponse import com.xebia.functional.xef.server.models.ProjectRequest +import com.xebia.functional.xef.server.models.ProjectSimpleResponse import com.xebia.functional.xef.server.models.ProjectUpdateRequest import com.xebia.functional.xef.server.services.ProjectRepositoryService +import guru.zoroark.tegral.openapi.dsl.schema +import guru.zoroark.tegral.openapi.ktor.resources.* import io.ktor.http.* +import io.ktor.resources.* 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 +import io.ktor.server.routing.Routing +import io.swagger.v3.oas.models.media.StringSchema fun Routing.projectsRoutes(projectRepositoryService: ProjectRepositoryService) { authenticate("auth-bearer") { - get("/v1/settings/projects") { + getD { val token = call.getToken() val response = projectRepositoryService.getProjects(token) call.respond(response) } - get("/v1/settings/projects/{id}") { + postD { + val request = call.decodeFromStringRequest() val token = call.getToken() - val id = call.getId() - val response = projectRepositoryService.getProject(token, id) - call.respond(response) + val response = projectRepositoryService.createProject(request, token) + call.respond(status = HttpStatusCode.Created, response) } - get("/v1/settings/projects/org/{id}") { + getD { val token = call.getToken() val id = call.getId() - val response = projectRepositoryService.getProjectsByOrganization(token, id) + val response = projectRepositoryService.getProject(token, id) call.respond(response) } - post("/v1/settings/projects") { - val request = Json.decodeFromString(call.receive()) - val token = call.getToken() - val response = projectRepositoryService.createProject(request, token) - call.respond(status = HttpStatusCode.Created, response) - } - put("/v1/settings/projects/{id}") { - val request = Json.decodeFromString(call.receive()) + putD { + val request = call.decodeFromStringRequest() val token = call.getToken() val id = call.getId() val response = projectRepositoryService.updateProject(token, request, id) call.respond(status = HttpStatusCode.NoContent, response) } - delete("/v1/settings/projects/{id}") { + deleteD { val token = call.getToken() val id = call.getId() val response = projectRepositoryService.deleteProject(token, id) call.respond(status = HttpStatusCode.NoContent, response) } + getD { + val token = call.getToken() + val id = call.getId() + val response = projectRepositoryService.getProjectsByOrganization(token, id) + call.respond(response) + } } } + +@Resource("/v1/settings/projects") +class ProjectsRoutes { + companion object : + ResourceDescription by describeResource({ + tags += "Projects" + get { + description = "Get all projects" + HttpStatusCode.OK.value response + { + description = "The list of projects" + json { schema() } + } + } + post { + description = "Create a project" + body { + description = "The project to create" + required = true + json { schema() } + } + HttpStatusCode.Created.value response + { + description = "The created project" + json { schema() } + } + } + }) +} + +@Resource("/v1/settings/projects/{id}") +class ProjectDetailsRoutes { + companion object : + ResourceDescription by describeResource({ + tags += "Projects" + "id" pathParameter + { + description = "Project ID" + required = true + schema = StringSchema() + example = "project_ABC_123" + } + get { + description = "Get a project" + HttpStatusCode.OK.value response + { + description = "The project" + json { schema() } + } + } + put { + description = "Update a project" + body { + description = "The project to update" + required = true + json { schema() } + } + HttpStatusCode.NoContent.value response + { + description = "The updated project" + json { schema() } + } + } + delete { + description = "Delete a project" + HttpStatusCode.NoContent.value response { description = "The deleted project" } + } + }) +} + +@Resource("/v1/settings/projects/org/{id}") +class ProjectsByOrganizationRoutes { + companion object : + ResourceDescription by describeResource({ + tags += "Projects" + "id" pathParameter + { + description = "Organization ID" + required = true + schema = StringSchema() + example = "org_ABC_123" + } + get { + description = "Get all projects by organization" + tags += "Projects" + HttpStatusCode.OK.value response + { + description = "The list of projects" + json { schema() } + } + } + }) +} 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 index 52891fc15..fb0ae1bd3 100644 --- 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 @@ -1,47 +1,135 @@ package com.xebia.functional.xef.server.http.routes +import com.xebia.functional.xef.server.models.TokenFullResponse import com.xebia.functional.xef.server.models.TokenRequest +import com.xebia.functional.xef.server.models.TokenSimpleResponse import com.xebia.functional.xef.server.models.TokenUpdateRequest import com.xebia.functional.xef.server.services.TokenRepositoryService +import guru.zoroark.tegral.openapi.dsl.schema +import guru.zoroark.tegral.openapi.ktor.resources.* import io.ktor.http.* +import io.ktor.resources.* 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 +import io.ktor.server.routing.Routing +import io.swagger.v3.oas.models.media.StringSchema fun Routing.tokensRoutes(tokenRepositoryService: TokenRepositoryService) { authenticate("auth-bearer") { - get("/v1/settings/tokens") { + getD { 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()) + postD { + val request = call.decodeFromStringRequest() 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()) + putD { + val request = call.decodeFromStringRequest() 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}") { + deleteD { val token = call.getToken() val id = call.getId() val response = tokenRepositoryService.deleteToken(token, id) call.respond(status = HttpStatusCode.NoContent, response) } + getD { + val token = call.getToken() + val id = call.getId() + val response = tokenRepositoryService.getTokensByProject(token, id) + call.respond(response) + } } } + +@Resource("/v1/settings/tokens") +class TokensRoutes { + companion object : + ResourceDescription by describeResource({ + tags += "Tokens" + get { + description = "Get all tokens" + HttpStatusCode.OK.value response + { + description = "The list of tokens" + json { schema() } + } + } + post { + description = "Create a new token" + body { + description = "The token to create" + required = true + json { schema() } + } + HttpStatusCode.Created.value response + { + description = "The created token" + json { schema() } + } + } + }) +} + +@Resource("/v1/settings/tokens/{id}") +class TokenDetailsRoutes { + companion object : + ResourceDescription by describeResource({ + tags += "Tokens" + "id" pathParameter + { + description = "Token ID" + required = true + schema = StringSchema() + example = "token_ABC_123" + } + put { + description = "Update a token" + body { + description = "The token to update" + required = true + json { schema() } + } + HttpStatusCode.NoContent.value response + { + description = "The updated token" + json { schema() } + } + } + delete { + description = "Delete a token" + HttpStatusCode.NoContent.value response { description = "The deleted token" } + } + }) +} + +@Resource("/v1/settings/tokens/projects/{id}") +class TokensByProjectRoutes { + companion object : + ResourceDescription by describeResource({ + tags += "Tokens" + "id" pathParameter + { + description = "Project ID" + required = true + schema = StringSchema() + example = "project_ABC_123" + } + get { + description = "Get all tokens for a project" + HttpStatusCode.OK.value response + { + description = "The list of tokens" + json { schema() } + } + } + }) +} 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 efde1173d..18be8f1ed 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 @@ -1,25 +1,71 @@ package com.xebia.functional.xef.server.http.routes 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.services.UserRepositoryService +import guru.zoroark.tegral.openapi.dsl.schema +import guru.zoroark.tegral.openapi.ktor.resources.ResourceDescription +import guru.zoroark.tegral.openapi.ktor.resources.describeResource +import guru.zoroark.tegral.openapi.ktor.resources.postD import io.ktor.http.* +import io.ktor.resources.* import io.ktor.server.application.* -import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -import kotlinx.serialization.json.Json fun Routing.userRoutes(userRepositoryService: UserRepositoryService) { - post("/register") { - val request = Json.decodeFromString(call.receive()) + postD { + val request = call.decodeFromStringRequest() val response = userRepositoryService.register(request) call.respond(response) } - post("/login") { - val request = Json.decodeFromString(call.receive()) + postD { + val request = call.decodeFromStringRequest() val response = userRepositoryService.login(request) call.respond(response) } } + +@Resource("/register") +class UserRegisterRoutes { + companion object : + ResourceDescription by describeResource({ + tags += "User" + post { + description = "Register a new user" + body { + description = "The user details" + required = true + json { schema() } + } + HttpStatusCode.OK.value response + { + description = "User registered successfully" + json { schema() } + } + } + }) +} + +@Resource("/login") +class UserLoginRoutes { + companion object : + ResourceDescription by describeResource({ + tags += "User" + post { + description = "Login a user" + body { + description = "The user details" + required = true + json { schema() } + } + HttpStatusCode.OK.value response + { + description = "User logged in successfully" + json { schema() } + } + } + }) +} 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 47b15f346..6092dc2cc 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 @@ -5,7 +5,7 @@ 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.github.oshai.kotlinlogging.KLogger -import io.ktor.server.routing.* +import io.ktor.server.routing.Routing fun Routing.xefRoutes(logger: KLogger) { userRoutes(UserRepositoryService(logger))