From 734c6c6fb17194688c0e9d30c6b14ce747856f7b Mon Sep 17 00:00:00 2001 From: luevard Date: Wed, 7 Feb 2024 23:39:54 +0100 Subject: [PATCH] :sparkles: Add Swagger's comments --- Sources/pom.xml | 8 +- .../src/main/kotlin/allin/model/ApiMessage.kt | 1 + .../main/kotlin/allin/routing/BasicRouting.kt | 14 +- .../kotlin/allin/routing/BetDetailRouter.kt | 21 +- .../main/kotlin/allin/routing/BetRouter.kt | 232 ++++++++++++++---- .../allin/routing/ParticipationRouter.kt | 42 +++- .../main/kotlin/allin/routing/UserRouter.kt | 164 ++++++++++--- 7 files changed, 399 insertions(+), 83 deletions(-) diff --git a/Sources/pom.xml b/Sources/pom.xml index a9e4877..9536a23 100644 --- a/Sources/pom.xml +++ b/Sources/pom.xml @@ -149,6 +149,12 @@ swagger-codegen-generators 1.0.38 + + + org.hamcrest + hamcrest-core + 1.3 + ${project.basedir}/src/main/kotlin @@ -164,7 +170,7 @@ org.jetbrains.kotlin ${kotlin.version} - 1.8 + 11 diff --git a/Sources/src/main/kotlin/allin/model/ApiMessage.kt b/Sources/src/main/kotlin/allin/model/ApiMessage.kt index eaaf6ca..a09c1e1 100644 --- a/Sources/src/main/kotlin/allin/model/ApiMessage.kt +++ b/Sources/src/main/kotlin/allin/model/ApiMessage.kt @@ -12,4 +12,5 @@ object ApiMessage { const val PARTICIPATION_NOT_FOUND = "Participation not found." const val NOT_ENOUGH_COINS = "Not enough coins." const val NO_GIFT = "Can't get daily gift." + const val USER_CANT_BE_DELETE = "This user can't be delete now !" } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/routing/BasicRouting.kt b/Sources/src/main/kotlin/allin/routing/BasicRouting.kt index 1824916..535b8a6 100644 --- a/Sources/src/main/kotlin/allin/routing/BasicRouting.kt +++ b/Sources/src/main/kotlin/allin/routing/BasicRouting.kt @@ -1,6 +1,8 @@ package allin.routing import allin.model.ApiMessage +import io.github.smiley4.ktorswaggerui.dsl.get +import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -8,7 +10,17 @@ import io.ktor.server.routing.* fun Application.BasicRouting() { routing { - get("/") { + get("/", { + description = "Hello World of Allin API" + response { + HttpStatusCode.OK to { + description = "Successful Request" + } + HttpStatusCode.InternalServerError to { + description = "Something unexpected happened" + } + } + }) { call.respond(ApiMessage.WELCOME) } } diff --git a/Sources/src/main/kotlin/allin/routing/BetDetailRouter.kt b/Sources/src/main/kotlin/allin/routing/BetDetailRouter.kt index 6654765..519b9eb 100644 --- a/Sources/src/main/kotlin/allin/routing/BetDetailRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/BetDetailRouter.kt @@ -6,11 +6,14 @@ import allin.ext.verifyUserFromToken import allin.model.ApiMessage import allin.model.BetDetail import allin.model.getBetAnswerDetail +import io.github.smiley4.ktorswaggerui.dsl.get import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* import io.ktor.server.response.* import io.ktor.server.routing.* +import java.util.* fun Application.BetDetailRouter() { @@ -20,7 +23,23 @@ fun Application.BetDetailRouter() { routing { authenticate { - get("/betdetail/get/{id}") { + get("/betdetail/get/{id}", { + description = "Retrieves the details of a specific bet" + request { + headerParameter("JWT token of the logged user") + pathParameter("Id of the desired detail bet") + } + response { + HttpStatusCode.Accepted to { + description = "The bet can be returned" + body() + } + HttpStatusCode.NotFound to { + description = "Bet not found in the selected source" + body(ApiMessage.BET_NOT_FOUND) + } + } + }) { hasToken { principal -> verifyUserFromToken(userDataSource, principal) { user, _ -> val id = call.parameters["id"].toString() diff --git a/Sources/src/main/kotlin/allin/routing/BetRouter.kt b/Sources/src/main/kotlin/allin/routing/BetRouter.kt index f0b70ed..02ac6b6 100644 --- a/Sources/src/main/kotlin/allin/routing/BetRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/BetRouter.kt @@ -5,12 +5,17 @@ import allin.ext.hasToken import allin.ext.verifyUserFromToken import allin.model.* import allin.utils.AppConfig +import io.github.smiley4.ktorswaggerui.dsl.get +import io.github.smiley4.ktorswaggerui.dsl.post +import io.github.smiley4.ktorswaggerui.dsl.route import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +import io.swagger.annotations.Api import java.util.* val tokenManagerBet = AppConfig.tokenManager @@ -22,69 +27,154 @@ fun Application.BetRouter() { val participationDataSource = this.dataSource.participationDataSource routing { - route("/bets/add") { - authenticate { - post { - hasToken { principal -> - val bet = call.receive() - val id = UUID.randomUUID().toString() - val username = tokenManagerBet.getUsernameFromToken(principal) - betDataSource.getBetById(id)?.let { - call.respond(HttpStatusCode.Conflict, ApiMessage.BET_ALREADY_EXIST) - } ?: run { - val betWithId = bet.copy(id = id, createdBy = username) - betDataSource.addBet(betWithId) - call.respond(HttpStatusCode.Created, betWithId) + authenticate { + post("/bets/add", { + description = "Allows a user to create a new bet" + request { + headerParameter("JWT token of the logged user") + body { + description = "Bet to add in the selected source" + } + } + response { + HttpStatusCode.Created to { + description = "the bet has been added" + body() { + description = "Bet with assigned id" } } - + HttpStatusCode.Conflict to { + description = "Id already exist" + body(ApiMessage.BET_ALREADY_EXIST) + } + } + }) { + hasToken { principal -> + val bet = call.receive() + val id = UUID.randomUUID().toString() + val username = tokenManagerBet.getUsernameFromToken(principal) + betDataSource.getBetById(id)?.let { + call.respond(HttpStatusCode.Conflict, ApiMessage.BET_ALREADY_EXIST) + } ?: run { + val betWithId = bet.copy(id = id, createdBy = username) + betDataSource.addBet(betWithId) + call.respond(HttpStatusCode.Created, betWithId) + } } - } } authenticate { - get("/bets/gets") { + get("/bets/gets", { + description = "Allows you to recover all bets" + request { + headerParameter("JWT token of the logged user") + } + response { + HttpStatusCode.Accepted to { + description = "The list of bets is available" + body>() { + description = "List of all bet in the selected source" + } + } + } + }) { hasToken { principal -> - verifyUserFromToken(userDataSource, principal) { user, _ -> + verifyUserFromToken(userDataSource, principal) { _, _ -> call.respond(HttpStatusCode.Accepted, betDataSource.getAllBets()) } } } } - route("/bets/get/{id}") { - get { - val id = call.parameters["id"] ?: "" - betDataSource.getBetById(id)?.let { bet -> - call.respond(HttpStatusCode.Accepted, bet) - } ?: call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND) + get("/bets/get/{id}", { + description = "Retrieves a specific bet" + request { + pathParameter("Id of the desired bet") + } + response { + HttpStatusCode.Accepted to { + description = "The bet is available" + body { + description = "Desired bet" + } + } + HttpStatusCode.NotFound to { + description = "Bet not found in the selected source" + body(ApiMessage.BET_NOT_FOUND) + } } + }) { + val id = call.parameters["id"] ?: "" + betDataSource.getBetById(id)?.let { bet -> + call.respond(HttpStatusCode.Accepted, bet) + } ?: call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND) } - route("/bets/delete") { - post { - val id = call.receive>()["id"] ?: "" - if (betDataSource.removeBet(id)) { - call.respond(HttpStatusCode.Accepted) - } else { - call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND) + post("/bets/delete", { + description = "Delete a specific bet" + request { + body> { + description = "Id of the desired bet" } - + } + response { + HttpStatusCode.Accepted to { + description = "The bet has been deleted" + } + HttpStatusCode.NotFound to { + description = "Bet not found in the selected source" + body(ApiMessage.BET_NOT_FOUND) + } + } + }) { + val id = call.receive>()["id"] ?: "" + if (betDataSource.removeBet(id)) { + call.respond(HttpStatusCode.Accepted) + } else { + call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND) } } - route("bets/update") { - post { - val updatedBetData = call.receive() - if (betDataSource.updateBet(updatedBetData)) { - call.respond(HttpStatusCode.Accepted) - } else { - call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND) + + post("bets/update", { + description = "Update a specific bet" + request { + body { + description = "Information of the updated bet" } } + response { + HttpStatusCode.Accepted to { + description = "The bet has been updated" + } + HttpStatusCode.NotFound to { + description = "Bet not found in the selected source" + body(ApiMessage.BET_NOT_FOUND) + } + } + }) { + val updatedBetData = call.receive() + if (betDataSource.updateBet(updatedBetData)) { + call.respond(HttpStatusCode.Accepted) + } else { + call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND) + } } authenticate { - get("/bets/toConfirm") { + get("/bets/toConfirm", { + description = "Allows a user to know which bets can be validated" + request { + headerParameter("JWT token of the logged user") + } + response { + HttpStatusCode.Accepted to { + description = "The list of bets that can be validated is available" + body>() { + description = "list of bets that can be validated" + } + } + } + }) { hasToken { principal -> verifyUserFromToken(userDataSource, principal) { user, _ -> val response = betDataSource.getToConfirm(user.username).map { @@ -103,7 +193,20 @@ fun Application.BetRouter() { } authenticate { - get("/bets/getWon") { + get("/bets/getWon", { + description = "Allows a user to know their won bets" + request { + headerParameter("JWT token of the logged user") + } + response { + HttpStatusCode.Accepted to { + description = "The list of won bets is available" + body>() { + description = "List of won bets" + } + } + } + }) { hasToken { principal -> verifyUserFromToken(userDataSource, principal) { user, _ -> call.respond(HttpStatusCode.Accepted, betDataSource.getWonNotifications(user.username)) @@ -113,7 +216,20 @@ fun Application.BetRouter() { } authenticate { - get("/bets/history") { + get("/bets/history", { + description = "Allows a user to know own history of bets" + request { + headerParameter("JWT token of the logged user") + } + response { + HttpStatusCode.Accepted to { + description = "Bet history is available" + body>() { + description = "Betting history list" + } + } + } + }) { hasToken { principal -> verifyUserFromToken(userDataSource, principal) { user, _ -> call.respond(HttpStatusCode.Accepted, betDataSource.getHistory(user.username)) @@ -123,7 +239,20 @@ fun Application.BetRouter() { } authenticate { - get("/bets/current") { + get("/bets/current", { + description = "Allows a user to know current bets" + request { + headerParameter("JWT token of the logged user") + } + response { + HttpStatusCode.Accepted to { + description = "List of current bets is available" + body> { + description = "List of current bets" + } + } + } + }) { hasToken { principal -> verifyUserFromToken(userDataSource, principal) { user, _ -> call.respond(HttpStatusCode.Accepted, betDataSource.getCurrent(user.username)) @@ -133,7 +262,24 @@ fun Application.BetRouter() { } authenticate { - post("/bets/confirm/{id}") { + post("/bets/confirm/{id}", { + description = "allows the creator of a bet to confrm the final answer" + request { + headerParameter("JWT token of the logged user") + pathParameter("Id of the desired bet") + body { + description = "Final answer of the bet" + } + } + response { + HttpStatusCode.OK to { + description = "The final answer has been set" + } + HttpStatusCode.Unauthorized to { + description = "The user is not the creator of the bet" + } + } + }) { hasToken { principal -> verifyUserFromToken(userDataSource, principal) { user, _ -> val betId = call.parameters["id"] ?: "" diff --git a/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt b/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt index 3995a88..226b503 100644 --- a/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt @@ -6,9 +6,12 @@ import allin.ext.verifyUserFromToken import allin.model.ApiMessage import allin.model.Participation import allin.model.ParticipationRequest +import io.github.smiley4.ktorswaggerui.dsl.delete +import io.github.smiley4.ktorswaggerui.dsl.post import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -22,7 +25,25 @@ fun Application.ParticipationRouter() { routing { authenticate { - post("/participations/add") { + post("/participations/add", { + description = "Allows a user to add a stake to a bet" + request { + headerParameter("JWT token of the logged user") + body { + description = "Participation in a bet" + } + } + response { + HttpStatusCode.Created to { + description = "The stake has been bet" + } + HttpStatusCode.Forbidden to { + description = "User does not have enough coins" + body(ApiMessage.NOT_ENOUGH_COINS) + } + } + + }) { hasToken { principal -> val participation = call.receive() verifyUserFromToken(userDataSource, principal) { user, _ -> @@ -46,7 +67,24 @@ fun Application.ParticipationRouter() { } } } - delete("/participations/delete") { + delete("/participations/delete", { + description = "Allows to delete a participation to a bet" + request { + headerParameter("JWT token of the logged user") + body { + description = "Id of the participation" + } + } + response { + HttpStatusCode.NotFound to { + description = "Participation was not found" + body(ApiMessage.PARTICIPATION_NOT_FOUND) + } + HttpStatusCode.NoContent to { + description = "The operation was successful" + } + } + }) { hasToken { val participationId = call.receive() if (participationDataSource.deleteParticipation(participationId)) { diff --git a/Sources/src/main/kotlin/allin/routing/UserRouter.kt b/Sources/src/main/kotlin/allin/routing/UserRouter.kt index 746d825..660f2b0 100644 --- a/Sources/src/main/kotlin/allin/routing/UserRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/UserRouter.kt @@ -1,16 +1,21 @@ package allin.routing import allin.dataSource +import allin.dto.UserDTO import allin.ext.hasToken import allin.ext.verifyUserFromToken import allin.model.* import allin.utils.AppConfig +import io.github.smiley4.ktorswaggerui.dsl.get +import io.github.smiley4.ktorswaggerui.dsl.post import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +import io.swagger.annotations.Api import java.util.* val RegexCheckerUser = AppConfig.regexChecker @@ -24,54 +29,113 @@ fun Application.UserRouter() { val userDataSource = this.dataSource.userDataSource routing { - route("/users/register") { - post { - val tempUser = call.receive() - if (RegexCheckerUser.isEmailInvalid(tempUser.email)) { - call.respond(HttpStatusCode.Forbidden, ApiMessage.INVALID_MAIL) + post("/users/register", { + description = "Allows a user to register" + request { + body { + description = "User information" } - if (userDataSource.userExists(tempUser.username, tempUser.email)) { - call.respond(HttpStatusCode.Conflict, ApiMessage.USER_ALREADY_EXISTS) + } + response { + HttpStatusCode.Created to { + description = "User created" + body() + } + HttpStatusCode.Conflict to { + description = "Email or username already taken" + body(ApiMessage.USER_ALREADY_EXISTS) + } + HttpStatusCode.Forbidden to { + description = "Email invalid" + body(ApiMessage.INVALID_MAIL) } - - val user = User( - id = UUID.randomUUID().toString(), - username = tempUser.username, - email = tempUser.email, - password = tempUser.password, - nbCoins = DEFAULT_COINS, - token = null - ) - CryptManagerUser.passwordCrypt(user) - user.token = tokenManagerUser.generateOrReplaceJWTToken(user) - userDataSource.addUser(user) - call.respond(HttpStatusCode.Created, user) } + }) { + val tempUser = call.receive() + if (RegexCheckerUser.isEmailInvalid(tempUser.email)) { + call.respond(HttpStatusCode.Forbidden, ApiMessage.INVALID_MAIL) + } + if (userDataSource.userExists(tempUser.username, tempUser.email)) { + call.respond(HttpStatusCode.Conflict, ApiMessage.USER_ALREADY_EXISTS) + } + + val user = User( + id = UUID.randomUUID().toString(), + username = tempUser.username, + email = tempUser.email, + password = tempUser.password, + nbCoins = DEFAULT_COINS, + token = null + ) + CryptManagerUser.passwordCrypt(user) + user.token = tokenManagerUser.generateOrReplaceJWTToken(user) + userDataSource.addUser(user) + call.respond(HttpStatusCode.Created, user) } - route("/users/login") { - post { - val checkUser = call.receive() - val user = userDataSource.getUserByUsername(checkUser.login) - if (CryptManagerUser.passwordDecrypt(user.second ?: "", checkUser.password)) { - user.first?.let { userDtoWithToken -> - userDtoWithToken.token = tokenManagerUser.generateOrReplaceJWTToken(userDtoWithToken) - call.respond(HttpStatusCode.OK, userDtoWithToken) - } ?: call.respond(HttpStatusCode.NotFound, ApiMessage.USER_NOT_FOUND) - } else { - call.respond(HttpStatusCode.NotFound, ApiMessage.INCORRECT_LOGIN_PASSWORD) + post("/users/login", { + description = "Allows a user to login" + request { + body { + description = "User information" } } + response { + HttpStatusCode.OK to { + description = "User logged in" + body() + } + HttpStatusCode.NotFound to { + description = "Invalid credentials" + body(ApiMessage.USER_NOT_FOUND) + } + } + }) { + val checkUser = call.receive() + val user = userDataSource.getUserByUsername(checkUser.login) + if (CryptManagerUser.passwordDecrypt(user.second ?: "", checkUser.password)) { + user.first?.let { userDtoWithToken -> + userDtoWithToken.token = tokenManagerUser.generateOrReplaceJWTToken(userDtoWithToken) + call.respond(HttpStatusCode.OK, userDtoWithToken) + } ?: call.respond(HttpStatusCode.NotFound, ApiMessage.USER_NOT_FOUND) + } else { + call.respond(HttpStatusCode.NotFound, ApiMessage.INCORRECT_LOGIN_PASSWORD) + } } authenticate { - post("/users/delete") { + post("/users/delete", { + description = "Allow you to delete your account" + + request { + headerParameter("JWT token of the logged user") + body { + description = "User information" + } + } + response { + HttpStatusCode.InternalServerError to { + description = "User can't be delete" + body(ApiMessage.USER_CANT_BE_DELETE) + } + HttpStatusCode.Accepted to { + body { + description = "Password of the user" + } + } + HttpStatusCode.NotFound to { + description = "User not found" + body(ApiMessage.INCORRECT_LOGIN_PASSWORD) + } + } + + }) { hasToken { principal -> verifyUserFromToken(userDataSource, principal) { _, password -> val checkUser = call.receive() if (CryptManagerUser.passwordDecrypt(password, checkUser.password)) { if (!userDataSource.deleteUser(checkUser.login)) { - call.respond(HttpStatusCode.InternalServerError, "This user can't be delete now !") + call.respond(HttpStatusCode.InternalServerError, ApiMessage.USER_CANT_BE_DELETE) } call.respond(HttpStatusCode.Accepted, password) } else { @@ -82,14 +146,44 @@ fun Application.UserRouter() { } } - get("/users/token") { + get("/users/token", { + description = "Allows you to retrieve the user linked to a JWT token" + request { + headerParameter("JWT token of the user") + } + response { + HttpStatusCode.OK to { + body { + description = "Limited user information" + } + } + } + }) { hasToken { principal -> verifyUserFromToken(userDataSource, principal) { userDto, _ -> call.respond(HttpStatusCode.OK, userDto) } } } - get("/users/gift") { + get("/users/gift", { + description = "Allows you to collect your daily gift" + request { + headerParameter("JWT token of the logged user") + } + response { + HttpStatusCode.OK to { + description = "Daily gift allowed !" + body() { + description = "Number of coins offered" + } + } + HttpStatusCode.MethodNotAllowed to { + description = "You can't have you daily gift now" + body(ApiMessage.NO_GIFT) + } + } + + }) { hasToken { principal -> verifyUserFromToken(userDataSource, principal) { userDto, _ -> if (userDataSource.canHaveDailyGift(userDto.username)) {