diff --git a/docs/build.gradle b/docs/build.gradle index a7cd2ce23c874e71d399c22d0b3faa07c21e3bcb..39b865d4d12edfd731a3beba917756e6d4d8e181 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -13,5 +13,6 @@ sourceSets { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "io.ktor:ktor-server-core:$ktor_version" + implementation "io.ktor:ktor-auth:$ktor_version" implementation "org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.1" } diff --git a/docs/src/me/jfenn/ktordocs/HtmlBuilder.kt b/docs/src/me/jfenn/ktordocs/HtmlBuilder.kt new file mode 100644 index 0000000000000000000000000000000000000000..094ca2577a1897dd5190f549f1b394350e71b8e6 --- /dev/null +++ b/docs/src/me/jfenn/ktordocs/HtmlBuilder.kt @@ -0,0 +1,249 @@ +package me.jfenn.ktordocs + +import io.ktor.http.HttpMethod +import kotlinx.html.* +import kotlinx.html.stream.appendHTML +import me.jfenn.ktordocs.`interface`.HasParams +import me.jfenn.ktordocs.`interface`.HasReferences +import me.jfenn.ktordocs.model.EndpointInfo +import me.jfenn.ktordocs.model.ParameterInfo + +class HtmlBuilder( + val docs: RestDocs +) { + + private fun DIV.referenceInfo(item: HasReferences) { + if (item.references.isNotEmpty()) { + h5 { +"Links" } + ul { + item.references.forEach { reference -> + li { + a(href = reference.url) { +reference.title } + } + } + } + } + } + + private fun DIV.parameterInfo(item: HasParams) { + if (item.params.isNotEmpty()) { + h5 { +"Parameters" } + table("table") { + thead { + tr { + th { +"Name" } + th { +"Type" } + th { +"In" } + th { +"Description" } + } + } + tbody { + item.params.forEach { (name, param) -> + tr { + td { + span("text-monospace font-weight-bold") { +name } + if (param.isRequired) { + span("text-danger") { +"*" } + } + } + td("text-muted") { +param.type } + td("text-muted") { +param.location.value } + td("text-muted") { +param.desc } + } + } + } + } + } + } + + private fun DIV.endpointInfo(endpoint: EndpointInfo) { + div("mb-5") { + div { + h4("d-inline") { + id = endpoint.id + a(href = "#" + endpoint.id) { +endpoint.title } + } + if (endpoint.auth.isNotEmpty()) { + span("float-right") { + endpoint.auth.forEach { + button(type = ButtonType.button, classes = "btn btn-link text-monospace") { + attributes["data-toggle"] = "modal" + attributes["data-target"] = "#auth_${it}" + + +it + } + } + + // "lock" icon + +"\uD83D\uDD12" + } + } + } + endpoint.desc?.let { + p("text-muted") { +it } + } + div("alert bg-light") { + span("mr-3 badge badge-" + when (endpoint.method) { + HttpMethod.Get -> "primary" + HttpMethod.Post -> "success" + HttpMethod.Put -> "warning" + HttpMethod.Patch -> "info" + HttpMethod.Delete -> "danger" + else -> "secondary" + }) { + +endpoint.method.value + } + span("text-monospace") { + +endpoint.path + } + } + + parameterInfo(endpoint) + + h5 { +"Code samples" } + pre("alert bg-light") { + code { + style = "tab-size: 2;" + + +buildString { + appendln("curl -X ${endpoint.method.value} \\") + + endpoint.params.filterValues { it.location == ParameterInfo.In.Header }.forEach { + appendln("\t-H '${it.value.name}: ${it.value.example}' \\") + } + + if (endpoint.method == HttpMethod.Post || endpoint.method == HttpMethod.Put) + appendln("\t-d '{data}' \\") + + appendln("\t${docs.config.baseUrl}${endpoint.path}") + } + } + } + + if (endpoint.responses.isNotEmpty()) { + h5 { +"Responses" } + table("table") { + thead { + tr { + th { +"Code" } + th { +"Description" } + th { +"Value" } + } + } + tbody { + endpoint.responses.forEach { (code, response) -> + tr { + td { + a(href = "https://http.cat/${code.value}") { + span("text-monospace") { +code.value.toString() } + span("text-muted") { +" (${code.description})" } + } + } + td("text-muted") { +response.desc } + td { + response.example?.let { + pre("alert bg-light") { + code { + style = "tab-size: 4;" + +it + } + } + } + } + } + } + } + } + } + + referenceInfo(endpoint) + } + } + + fun toHtml() : String = buildString { + appendln("") + appendHTML().html { + head { + meta(charset = "utf-8") + title(docs.config.title) + + link( + href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.1/css/bootstrap.min.css", + rel = "stylesheet" + ) + + style { + +"table {font-size:0.8rem;display:inline-block;max-width:100%;overflow-x:auto;}" + } + } + body { + div("row m-0") { + div("col-12 col-md-3 bg-dark text-light") { + div("py-4") { + style = "position: sticky; top: 0;" + + div("px-2") { + h3 { +docs.config.title } + p("text-white-50") { +docs.config.desc } + + span("font-weight-bold") { +"Contents" } + } + + docs.endpoints.forEach { + div("p-2 border-bottom border-secondary") { + a(classes = "text-light", href = "#" + it.id) { +it.title } + } + } + } + } + div("col-12 col-md-9") { + div("container my-4") { + h1("mb-4") { +"Endpoints" } + docs.endpoints.forEach { endpointInfo(it) } + } + } + } + + docs.config.authMethods.forEach { (name, auth) -> + modal("auth_${name}", auth.title) { + p { +auth.desc } + + auth.subDesc?.let { + p { small("text-muted") { +it } } + } + + parameterInfo(auth) + referenceInfo(auth) + } + } + + script(src = "https://code.jquery.com/jquery-3.5.1.slim.min.js") {} + script(src = "https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js") {} + script(src = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js") {} + } + } + appendln() + } +} + +fun FlowContent.modal(modalId: String, modalTitle: String, content: DIV.() -> Unit) { + div("modal fade") { + id = modalId + attributes["tabindex"] = "-1" + + div("modal-dialog") { + div("modal-content") { + div("modal-header") { + h5("modal-title") { +modalTitle } + button(type = ButtonType.button, classes = "close") { + attributes["data-dismiss"] = "modal" + unsafe { +"×" } + } + } + div("modal-body") { + content() + } + } + } + } +} diff --git a/docs/src/me/jfenn/ktordocs/RestDocs.kt b/docs/src/me/jfenn/ktordocs/RestDocs.kt index 26e3e78c74dbcaa12fde5cb2bf389b0a0f3a5299..0993b8a8c2b8d08b368f3b3bf6cd00aec56d2092 100644 --- a/docs/src/me/jfenn/ktordocs/RestDocs.kt +++ b/docs/src/me/jfenn/ktordocs/RestDocs.kt @@ -3,182 +3,22 @@ package me.jfenn.ktordocs import io.ktor.http.HttpMethod import kotlinx.html.* import kotlinx.html.stream.appendHTML +import me.jfenn.ktordocs.model.AuthenticationInfo import me.jfenn.ktordocs.model.Configuration import me.jfenn.ktordocs.model.EndpointInfo import me.jfenn.ktordocs.model.ParameterInfo -import me.jfenn.ktordocs.util.slugify class RestDocs( configure: Configuration.() -> Unit ) { val config = Configuration().apply { configure() } + val htmlBuilder = HtmlBuilder(this) val endpoints = ArrayList() - fun add(info: EndpointInfo) { + fun endpoint(info: EndpointInfo) { endpoints.add(info) } - fun FlowContent.endpointInfo(endpoint: EndpointInfo) { - div("my-5") { - h4 { - id = endpoint.id - a(href = "#" + endpoint.id) { +endpoint.title } - } - endpoint.description?.let { - p("text-muted") { +it } - } - div("alert bg-light") { - span("mr-3 badge badge-" + when (endpoint.method) { - HttpMethod.Get -> "primary" - HttpMethod.Post -> "success" - HttpMethod.Put -> "warning" - HttpMethod.Patch -> "info" - HttpMethod.Delete -> "danger" - else -> "secondary" - }) { - +endpoint.method.value - } - span("text-monospace") { - +endpoint.path - } - } - - if (endpoint.params.isNotEmpty()) { - h5 { +"Parameters" } - table("table") { - thead { - tr { - th { +"Name" } - th { +"Type" } - th { +"In" } - th { +"Description" } - } - } - tbody { - endpoint.params.forEach { (name, param) -> - tr { - td { - span("text-monospace") { +name } - if (param.isRequired) { - span("text-danger") { +"*" } - } - } - td("text-muted") { +param.type } - td("text-muted") { +param.location.value } - td("text-muted") { +param.description } - } - } - } - } - } - - h5 { +"Code samples" } - pre("alert bg-light") { - code { - style = "tab-size: 2;" - - +buildString { - appendln("curl -X ${endpoint.method.value} \\") - - endpoint.params.filterValues { it.location == ParameterInfo.In.Header }.forEach { - appendln("\t-H '${it.value.name}: ${it.value.example}' \\") - } - - if (endpoint.method == HttpMethod.Post || endpoint.method == HttpMethod.Put) - appendln("\t-d '{data}' \\") - - appendln("\t${config.baseUrl}${endpoint.path}") - } - } - } - - if (endpoint.responses.isNotEmpty()) { - h5 { +"Responses" } - table("table") { - thead { - tr { - th { +"Code" } - th { +"Description" } - th { +"Value" } - } - } - tbody { - endpoint.responses.forEach { (code, response) -> - tr { - td { - a(href = "https://http.cat/${code.value}") { - span("text-monospace") { +code.value.toString() } - span("text-muted") { +" (${code.description})" } - } - } - td("text-muted") { +(response.description ?: "") } - td { - response.example?.let { - pre("p-3 bg-dark text-light rounded") { - code("py-3") { - style = "tab-size: 4;" - +it - } - } - } ?: run { - +"No example provided." - } - } - } - } - } - } - } - } - } - - fun toHtml() : String = buildString { - appendln("") - appendHTML().html { - head { - meta(charset = "utf-8") - title(config.title) - - link( - href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.1/css/bootstrap.min.css", - rel = "stylesheet" - ) - - style { - +"table {font-size:0.8rem;display:inline-block;max-width:100%;overflow-x:auto;}" - } - } - body { - div("row m-0") { - div("col-12 col-md-3 bg-dark text-light") { - div("py-4") { - style = "position: sticky; top: 0;" - - div("px-2") { - h3 { +config.title } - p { +config.description } - - h4 { +"Contents" } - } - - endpoints.forEach { - div("p-2 border-bottom border-secondary") { - a(classes = "text-light", href = "#" + it.id) { +it.title } - } - } - } - } - div("col-12 col-md-9") { - div("container my-5") { - endpoints.forEach { endpointInfo(it) } - } - } - } - } - } - appendln() - } - } \ No newline at end of file diff --git a/docs/src/me/jfenn/ktordocs/RestDocsFeature.kt b/docs/src/me/jfenn/ktordocs/RestDocsFeature.kt index 413cf78714c9b6203c512482c96a11a3aa001950..2261959e58db9afcd1f478cb760ed66c05a078a8 100644 --- a/docs/src/me/jfenn/ktordocs/RestDocsFeature.kt +++ b/docs/src/me/jfenn/ktordocs/RestDocsFeature.kt @@ -1,6 +1,8 @@ package me.jfenn.ktordocs import io.ktor.application.* +import io.ktor.auth.AuthenticationProvider +import io.ktor.auth.AuthenticationRouteSelector import io.ktor.http.ContentType import io.ktor.response.respondText import io.ktor.routing.* @@ -33,10 +35,19 @@ class RestDocsFeature( }.joinToString("/") } - suspend fun updateRoutes(route: Route, selectors: List = listOf()) { + suspend fun updateRoutes(route: Route, selectors: List = listOf(), inheritEndpoint: EndpointInfo = docs.config.defaultEndpoint) { + var newEndpoint = inheritEndpoint + + (route.selector as? AuthenticationRouteSelector)?.let { selector -> + // add auth methods + newEndpoint = inheritEndpoint.copy( + authentication = selector.names.filterNotNull() + ) + } + (route.selector as? HttpMethodRouteSelector)?.let { selector -> val path = buildPathString(selectors) - val endpoint = docs.config.defaultEndpoint.copy( + val endpoint = inheritEndpoint.copy( path = path, method = selector.method ) @@ -70,7 +81,7 @@ class RestDocsFeature( } catch (e: DocsProxyException) { // caught proxy extension; add endpoint to docs e.configure.invoke(endpoint) - docs.add(endpoint) + docs.endpoint(endpoint) return } catch (e: Throwable) { // do nothing @@ -79,7 +90,7 @@ class RestDocsFeature( } route.children.forEach { - updateRoutes(it, selectors + it.selector) + updateRoutes(it, selectors + it.selector, newEndpoint) } } @@ -107,7 +118,7 @@ fun Route.restDocumentation(path: String = "/") { get(path) { application.feature(RestDocsFeature).apply { - call.respondText(docs.toHtml(), ContentType.Text.Html) + call.respondText(docs.htmlBuilder.toHtml(), ContentType.Text.Html) } } } diff --git a/docs/src/me/jfenn/ktordocs/interface/HasParams.kt b/docs/src/me/jfenn/ktordocs/interface/HasParams.kt new file mode 100644 index 0000000000000000000000000000000000000000..5764e66874e470eb378dca494cd3d3b562b4e779 --- /dev/null +++ b/docs/src/me/jfenn/ktordocs/interface/HasParams.kt @@ -0,0 +1,13 @@ +package me.jfenn.ktordocs.`interface` + +import me.jfenn.ktordocs.model.ParameterInfo + +interface HasParams { + + val params: HashMap + + fun param(name: String, configure: ParameterInfo.() -> Unit = {}) { + params[name] = (params[name] ?: ParameterInfo(name)).apply { configure() } + } + +} \ No newline at end of file diff --git a/docs/src/me/jfenn/ktordocs/interface/HasReferences.kt b/docs/src/me/jfenn/ktordocs/interface/HasReferences.kt new file mode 100644 index 0000000000000000000000000000000000000000..cae677eee4094363c0851b9256f6b27eb251b72d --- /dev/null +++ b/docs/src/me/jfenn/ktordocs/interface/HasReferences.kt @@ -0,0 +1,13 @@ +package me.jfenn.ktordocs.`interface` + +import me.jfenn.ktordocs.model.ReferenceInfo + +interface HasReferences { + + val references: ArrayList + + fun reference(url: String, title: String = url) { + references.add(ReferenceInfo(url, title)) + } + +} \ No newline at end of file diff --git a/docs/src/me/jfenn/ktordocs/model/AuthenticationInfo.kt b/docs/src/me/jfenn/ktordocs/model/AuthenticationInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..dc0143013b0fc4abd78d18cc6126277a988f1fb4 --- /dev/null +++ b/docs/src/me/jfenn/ktordocs/model/AuthenticationInfo.kt @@ -0,0 +1,40 @@ +package me.jfenn.ktordocs.model + +import me.jfenn.ktordocs.`interface`.HasParams +import me.jfenn.ktordocs.`interface`.HasReferences + +data class AuthenticationInfo( + val name: String, + var title: String = name, + var desc: String = "No description provided.", + var subDesc: String? = null, + internal var type: Type = Type.Unknown +) : HasReferences, HasParams { + + override var references = arrayListOf( + ReferenceInfo("https://ktor.io/servers/features/authentication", "Ktor Documentation") + ) + + override val params = HashMap() + + sealed class Type(val value: String) { + object Unknown : Type("") + object Basic : Type("basic") + object Form : Type("form") + } + + fun type(type: Type) { + when (type) { + Type.Basic -> { + subDesc = "The username and password are provided as raw values in a request header." + param("Basic") { + desc = "A header containing the username and password, separated by a colon (':') character." + location = ParameterInfo.In.Header + example = "username:password" + } + } + else -> {} + } + } + +} \ No newline at end of file diff --git a/docs/src/me/jfenn/ktordocs/model/Configuration.kt b/docs/src/me/jfenn/ktordocs/model/Configuration.kt index 579fcd5104b4fcbd43cbab6f8366265810f7690d..3a3970ab404688f38fab3dea88afa0d00499fd93 100644 --- a/docs/src/me/jfenn/ktordocs/model/Configuration.kt +++ b/docs/src/me/jfenn/ktordocs/model/Configuration.kt @@ -7,7 +7,13 @@ class Configuration { var baseUrl = "http://localhost:8080" var title = "REST API" - var description = "This documentation describes the website's API endpoints." + var desc = "This documentation describes the website's API endpoints." + + internal val authMethods = HashMap() + + fun authMethod(name: String, configure: AuthenticationInfo.() -> Unit) { + authMethods[name] = (authMethods[name] ?: AuthenticationInfo(name)).apply { configure() } + } internal var defaultEndpoint = EndpointInfo("/", HttpMethod.Get) diff --git a/docs/src/me/jfenn/ktordocs/model/EndpointInfo.kt b/docs/src/me/jfenn/ktordocs/model/EndpointInfo.kt index da03937a21abbf757da08950b31f734a4a40580c..ee6e1afa57e927c54c6abdf7efba116960d42cd0 100644 --- a/docs/src/me/jfenn/ktordocs/model/EndpointInfo.kt +++ b/docs/src/me/jfenn/ktordocs/model/EndpointInfo.kt @@ -2,46 +2,50 @@ package me.jfenn.ktordocs.model import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode +import me.jfenn.ktordocs.`interface`.HasParams +import me.jfenn.ktordocs.`interface`.HasReferences import me.jfenn.ktordocs.util.slugify class EndpointInfo( val path: String, val method: HttpMethod, var title: String = path, - var description: String? = null -) { + var desc: String? = null, + var auth: List = listOf() +) : HasReferences, HasParams { val id get() = title.slugify() - internal val params = HashMap() + override var references = ArrayList() + override val params = HashMap() internal val responses = HashMap() - fun param(name: String, configure: ParameterInfo.() -> Unit = {}) { - params[name] = (params[name] ?: ParameterInfo(name)).apply { configure() } - } - fun responds(code: HttpStatusCode = HttpStatusCode.OK, configure: ResponseInfo.() -> Unit = {}) { responses[code] = (responses[code] ?: ResponseInfo(code)).apply { configure() } } fun copy( path: String = this.path, - method: HttpMethod = this.method + method: HttpMethod = this.method, + authentication: List = this.auth ) : EndpointInfo { return EndpointInfo( path = path, method = method, title = title, - description = description - ).also { + desc = desc, + auth = authentication + ).also { new -> params.forEach { (name, param) -> - it.params[name] = param.copy() + new.params[name] = param.copy() } responses.forEach { (code, response) -> - it.responses[code] = response.copy() + new.responses[code] = response.copy() } + + new.references = ArrayList(references.map { it.copy() }) } } diff --git a/docs/src/me/jfenn/ktordocs/model/ParameterInfo.kt b/docs/src/me/jfenn/ktordocs/model/ParameterInfo.kt index bf76c2c3771fb3956df24815bc200f790a3a4066..b5db255d70c4a7ce9ae564ddc7fc69f4f3aab9d8 100644 --- a/docs/src/me/jfenn/ktordocs/model/ParameterInfo.kt +++ b/docs/src/me/jfenn/ktordocs/model/ParameterInfo.kt @@ -2,7 +2,7 @@ package me.jfenn.ktordocs.model data class ParameterInfo( val name: String, - var description: String = "No description provided.", + var desc: String = "No description provided.", var type: String = TYPE_STRING, var isRequired: Boolean = false, var location: In = In.Query, diff --git a/docs/src/me/jfenn/ktordocs/model/ReferenceInfo.kt b/docs/src/me/jfenn/ktordocs/model/ReferenceInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..bf738c02686358c70645d246f2898fb127910cb4 --- /dev/null +++ b/docs/src/me/jfenn/ktordocs/model/ReferenceInfo.kt @@ -0,0 +1,10 @@ +package me.jfenn.ktordocs.model + +import io.ktor.http.HttpStatusCode +import kotlin.reflect.KClass +import kotlin.reflect.full.declaredMemberProperties + +data class ReferenceInfo( + val url: String, + val title: String = url +) \ No newline at end of file diff --git a/docs/src/me/jfenn/ktordocs/model/ResponseInfo.kt b/docs/src/me/jfenn/ktordocs/model/ResponseInfo.kt index 46f4deedba533354cf6275dd7667148da86c3d7b..21ee51c42315bb49e4c0c4b37542de1e226e527d 100644 --- a/docs/src/me/jfenn/ktordocs/model/ResponseInfo.kt +++ b/docs/src/me/jfenn/ktordocs/model/ResponseInfo.kt @@ -6,7 +6,7 @@ import kotlin.reflect.full.declaredMemberProperties data class ResponseInfo( val code: HttpStatusCode, - var description: String? = null, + var desc: String = "No description provided.", var example: String? = null ) { diff --git a/example/build.gradle b/example/build.gradle index d1090d404e606540f1ea5a342fa05117807499a3..a2fa245f21034f0f55b07a7e752938abec4ff0d8 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -15,6 +15,7 @@ sourceSets { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "io.ktor:ktor-server-netty:$ktor_version" + implementation "io.ktor:ktor-auth:$ktor_version" implementation "ch.qos.logback:logback-classic:$logback_version" testImplementation "io.ktor:ktor-server-tests:$ktor_version" diff --git a/example/src/Application.kt b/example/src/Application.kt index 6de02ce3a23f19bd48b684510fd054d8517d1c45..178a0f27aebb1f436ed3facbf2c25f237b7df12d 100644 --- a/example/src/Application.kt +++ b/example/src/Application.kt @@ -2,15 +2,18 @@ package me.jfenn import io.ktor.application.* import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode import io.ktor.response.* -import io.ktor.request.* import io.ktor.routing.get import io.ktor.routing.post import io.ktor.routing.route import io.ktor.routing.routing +import io.ktor.auth.Authentication +import io.ktor.auth.UserIdPrincipal +import io.ktor.auth.authenticate +import io.ktor.auth.basic import me.jfenn.ktordocs.RestDocsFeature import me.jfenn.ktordocs.docs +import me.jfenn.ktordocs.model.AuthenticationInfo import me.jfenn.ktordocs.model.ParameterInfo import me.jfenn.ktordocs.restDocumentation @@ -19,16 +22,32 @@ fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module() { + install(Authentication) { + basic("basicAuth") { + realm = "Ktor Server" + validate { credentials -> + if (credentials.name == credentials.password) + UserIdPrincipal(credentials.name) + else null + } + } + } install(RestDocsFeature) { baseUrl = "https://example.com" title = "Example API" - description = "Basic documentation generated for testing & demo purposes." + desc = "Basic documentation generated for testing & demo purposes." + + authMethod("basicAuth") { + type(AuthenticationInfo.Type.Basic) + title = "Basic Authentication" + desc = "A simple authentication method intended for debugging, which only checks whether a username is equal to the provided password." + } defaultProperties { param("accept") { - description = "The content type to return - such as 'application/json'" + desc = "The content type to return - such as 'application/json'" location = ParameterInfo.In.Header type = "Content Type" example = "application/json" @@ -41,7 +60,7 @@ fun Application.module() { get("/hello") { docs { title = "Hello world" - description = "Responds with 'hello world' :)" + desc = "Responds with 'hello world' :)" } call.respondText("""{"hello": "world"}""", ContentType.Application.Json) @@ -50,13 +69,13 @@ fun Application.module() { get("/user/{username?}") { docs { title = "Get user info" - description = "Returns an object representation of the requested user ID." + desc = "Returns an object representation of the requested user ID." param("username") { type = "SHA1 hash" - description = "A valid ID or username of the user to fetch." + desc = "A valid ID or username of the user to fetch." } - responds(HttpStatusCode.OK) { - description = "If the user has been found" + responds { + desc = "If the user has been found" exampleJson(UserInfo::class) } } @@ -66,22 +85,24 @@ fun Application.module() { """.trimIndent()) } - post("/user/{username}") { - docs { - title = "Modify user info" - description = "Updates the provided user info in the database." - param("username") { - type = "SHA1 hash" - description = "A valid ID or username of the user to update." - } - responds(HttpStatusCode.OK) { - description = "If the user was successfully updated" + authenticate("basicAuth") { + post("/user/{username}") { + docs { + title = "Modify user info" + desc = "Updates the provided user info in the database." + param("username") { + type = "SHA1 hash" + desc = "A valid ID or username of the user to update." + } + responds { + desc = "If the user was successfully updated" + } } } } } - restDocumentation("/docs") + restDocumentation("/") } } diff --git a/gradle.properties b/gradle.properties index c378215efcb982c35315c319ec71f8cc424d21b0..1fafb7220889407d57f817d86756ae2011c4e5a0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ ktor_version=1.3.2 kotlin.code.style=official -kotlin_version=1.3.70 +kotlin_version=1.3.72 logback_version=1.2.1