diff --git a/.gitignore b/.gitignore index c426c32..f0d8881 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,6 @@ out/ /.nb-gradle/ ### VS Code ### -.vscode/ \ No newline at end of file +.vscode/ + +**/src/target/** \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/Application.kt b/Sources/src/main/kotlin/allin/Application.kt index 93119eb..657f576 100644 --- a/Sources/src/main/kotlin/allin/Application.kt +++ b/Sources/src/main/kotlin/allin/Application.kt @@ -1,19 +1,19 @@ package allin -import allin.model.User import allin.routing.BasicRouting import allin.routing.BetRouter +import allin.routing.ParticipationRouter import allin.routing.UserRouter +import allin.utils.TokenManager import com.typesafe.config.ConfigFactory import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* import io.ktor.server.config.* import io.ktor.server.engine.* import io.ktor.server.netty.* import io.ktor.server.plugins.contentnegotiation.* -import allin.utils.TokenManager -import io.ktor.server.auth.* -import io.ktor.server.auth.jwt.* fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0") { @@ -27,9 +27,9 @@ private fun Application.extracted() { authentication { jwt { verifier(tokenManager.verifyJWTToken()) - realm=config.property("realm").getString() + realm = config.property("realm").getString() validate { jwtCredential -> - if(jwtCredential.payload.getClaim("username").asString().isNotEmpty()) + if (jwtCredential.payload.getClaim("username").asString().isNotEmpty()) JWTPrincipal(jwtCredential.payload) else null } @@ -41,4 +41,5 @@ private fun Application.extracted() { BasicRouting() UserRouter() BetRouter() + ParticipationRouter() } diff --git a/Sources/src/main/kotlin/allin/ext/PipelineContextExt.kt b/Sources/src/main/kotlin/allin/ext/PipelineContextExt.kt new file mode 100644 index 0000000..0d9cfd9 --- /dev/null +++ b/Sources/src/main/kotlin/allin/ext/PipelineContextExt.kt @@ -0,0 +1,23 @@ +package allin.ext + +import allin.model.ApiMessage +import allin.model.User +import allin.routing.users +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.util.pipeline.* + +suspend fun PipelineContext<*, ApplicationCall>.hasToken(content: suspend (principal: JWTPrincipal) -> Unit) = + call.principal()?.let { content(it) } ?: call.respond(HttpStatusCode.Unauthorized) + +suspend fun PipelineContext<*, ApplicationCall>.verifyUserFromToken( + principal: JWTPrincipal, + content: suspend (user: User) -> Unit +) { + val username = principal.payload.getClaim("username").asString() + users.find { it.username == username }?.let { content(it) } + ?: call.respond(HttpStatusCode.NotFound, ApiMessage.TokenUserNotFound) +} \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/model/ApiMessage.kt b/Sources/src/main/kotlin/allin/model/ApiMessage.kt new file mode 100644 index 0000000..deaa55f --- /dev/null +++ b/Sources/src/main/kotlin/allin/model/ApiMessage.kt @@ -0,0 +1,13 @@ +package allin.model + +object ApiMessage { + const val Welcome = "Welcome on AllIn's API !" + const val TokenUserNotFound = "User not found with the valid token !" + const val BetNotFound = "Bet not found." + const val BetAlreadyExist = "Bet already exists." + const val IncorrectLoginPassword = "Login and/or password incorrect." + const val UserAlreadyExist = "Mail and/or username already exists." + const val InvalidMail = "Invalid mail." + const val ParticipationNotFound = "Participation not found." + const val NotEnoughCoins = "Not enough coins." +} \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/model/Bet.kt b/Sources/src/main/kotlin/allin/model/Bet.kt index 5d4bc13..cb895e1 100644 --- a/Sources/src/main/kotlin/allin/model/Bet.kt +++ b/Sources/src/main/kotlin/allin/model/Bet.kt @@ -1,19 +1,37 @@ package allin.model -import allin.dto.UserDTOWithToken -import allin.serializer.DateSerializer +import allin.serializer.ZonedDateTimeSerializer import kotlinx.serialization.Serializable +import java.time.ZonedDateTime import java.util.* @Serializable -data class Bet(val id: Int, val theme: String, val sentenceBet: String, @Serializable(DateSerializer::class) val endRegistration: Date, @Serializable(DateSerializer::class) var endBet : Date, var isPrivate : Boolean, var response : MutableList, val createdBy : String) +data class Bet( + val id: String, + val theme: String, + val sentenceBet: String, + @Serializable(ZonedDateTimeSerializer::class) val endRegistration: ZonedDateTime, + @Serializable(ZonedDateTimeSerializer::class) var endBet: ZonedDateTime, + var isPrivate: Boolean, + var response: MutableList, + val createdBy: String +) @Serializable -data class UpdatedBetData(val id: Int,@Serializable(DateSerializer::class) val endBet: Date, val isPrivate: Boolean, val response: MutableList) +data class UpdatedBetData( + val id: String, + @Serializable(ZonedDateTimeSerializer::class) val endBet: ZonedDateTime, + val isPrivate: Boolean, + val response: MutableList +) @Serializable -data class BetWithoutId(val theme: String, val sentenceBet: String, @Serializable(DateSerializer::class) val endRegistration: Date, @Serializable(DateSerializer::class) var endBet : Date, var isPrivate : Boolean, var response : MutableList, val createdBy : String) - -fun convertBetWithoutIdToBet(betWithoutId: BetWithoutId,id : Int, username : String): Bet { - return Bet(id,betWithoutId.theme,betWithoutId.sentenceBet,betWithoutId.endRegistration, betWithoutId.endBet, betWithoutId.isPrivate, betWithoutId.response, username) -} +data class BetWithoutId( + val theme: String, + val sentenceBet: String, + @Serializable(ZonedDateTimeSerializer::class) val endRegistration: ZonedDateTime, + @Serializable(ZonedDateTimeSerializer::class) var endBet: ZonedDateTime, + var isPrivate: Boolean, + var response: MutableList, + val createdBy: String +) diff --git a/Sources/src/main/kotlin/allin/model/BetAction.kt b/Sources/src/main/kotlin/allin/model/BetAction.kt deleted file mode 100644 index f830e2c..0000000 --- a/Sources/src/main/kotlin/allin/model/BetAction.kt +++ /dev/null @@ -1,5 +0,0 @@ -package allin.model - -import allin.dto.UserDTOWithToken -data class BetAction(val id:Int, val coins: Int, val user: String, val bet: Int) -data class BetActionCompleted(val id:Int, val coins: Int, val user: UserDTOWithToken, val bet: Bet) diff --git a/Sources/src/main/kotlin/allin/model/Participation.kt b/Sources/src/main/kotlin/allin/model/Participation.kt new file mode 100644 index 0000000..b2e8495 --- /dev/null +++ b/Sources/src/main/kotlin/allin/model/Participation.kt @@ -0,0 +1,19 @@ +package allin.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Participation( + val id: String, + val betId: String, + val username: String, + val answer: String, + val stake: Int +) + +@Serializable +data class ParticipationRequest( + val betId: String, + val answer: String, + val stake: Int +) \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/model/User.kt b/Sources/src/main/kotlin/allin/model/User.kt index d01dc50..6234988 100644 --- a/Sources/src/main/kotlin/allin/model/User.kt +++ b/Sources/src/main/kotlin/allin/model/User.kt @@ -1,10 +1,26 @@ package allin.model -import allin.dto.UserDTO import kotlinx.serialization.Serializable @Serializable -data class User(val username: String, val email: String, var password: String, var nbCoins: Int = 1000, var token: String? = null) +data class User( + val id: String, + val username: String, + val email: String, + var password: String, + var nbCoins: Int = 1000, + var token: String? = null +) @Serializable -data class CheckUser(val login: String,val password: String) \ No newline at end of file +data class UserRequest( + val username: String, + val email: String, + var password: String +) + +@Serializable +data class CheckUser( + val login: String, + val password: String +) diff --git a/Sources/src/main/kotlin/allin/routing/BasicRouting.kt b/Sources/src/main/kotlin/allin/routing/BasicRouting.kt index 0d78278..af603ab 100644 --- a/Sources/src/main/kotlin/allin/routing/BasicRouting.kt +++ b/Sources/src/main/kotlin/allin/routing/BasicRouting.kt @@ -1,13 +1,14 @@ package allin.routing +import allin.model.ApiMessage import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* -fun Application.BasicRouting(){ +fun Application.BasicRouting() { routing { get("/") { - call.respond("Bienvenue sur l'API de AlLin!") + call.respond(ApiMessage.Welcome) } } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/routing/BetActionRouter.kt b/Sources/src/main/kotlin/allin/routing/BetActionRouter.kt deleted file mode 100644 index aa68a74..0000000 --- a/Sources/src/main/kotlin/allin/routing/BetActionRouter.kt +++ /dev/null @@ -1,13 +0,0 @@ -package allin.routing - -import allin.model.BetAction -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.routing.* - -fun Application.BetActionRouter(){ - routing { - route("/BetAction/add"){ - } - } -} diff --git a/Sources/src/main/kotlin/allin/routing/BetRouter.kt b/Sources/src/main/kotlin/allin/routing/BetRouter.kt index 997cee5..f55abde 100644 --- a/Sources/src/main/kotlin/allin/routing/BetRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/BetRouter.kt @@ -1,65 +1,85 @@ package allin.routing -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.routing.* -import allin.model.* + +import allin.ext.hasToken +import allin.ext.verifyUserFromToken +import allin.model.ApiMessage +import allin.model.Bet +import allin.model.BetWithoutId +import allin.model.UpdatedBetData import allin.utils.AppConfig 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 java.util.* val bets = mutableListOf() -val tokenManagerBet= AppConfig.tokenManager +val tokenManagerBet = AppConfig.tokenManager -fun CreateId() : Int{ - return bets.size -} - -fun Application.BetRouter(){ - routing{ - route("/bets/add"){ - post{ +fun Application.BetRouter() { + routing { + route("/bets/add") { + post { val bet = call.receive() - val id = CreateId() + val id = UUID.randomUUID().toString() val username = tokenManagerBet.getUsernameFromToken(bet.createdBy) - val findbet = bets.find { it.id == id } - if(findbet==null){ - val betWithId = convertBetWithoutIdToBet(bet,id,username) + bets.find { it.id == id }?.let { + call.respond(HttpStatusCode.Conflict, ApiMessage.BetAlreadyExist) + } ?: run { + val betWithId = Bet( + id, + bet.theme, + bet.sentenceBet, + bet.endRegistration, + bet.endBet, + bet.isPrivate, + bet.response, + username + ) bets.add(betWithId) call.respond(HttpStatusCode.Created, betWithId) } - call.respond(HttpStatusCode.Conflict,"Bet already exist") } } - route("/bets/gets"){ - get{ - // if(bets.size>0) - call.respond(HttpStatusCode.Accepted, bets.toList()) - // else call.respond(HttpStatusCode.NoContent) + route("/bets/gets") { + get { + // if(bets.size>0) + call.respond(HttpStatusCode.Accepted, bets.toList()) + // else call.respond(HttpStatusCode.NoContent) } } - route("/bets/delete"){ - post{ - val idbet = call.receive>()["id"] - val findbet = bets.find { it.id == idbet } - if(findbet==null){ - call.respond(HttpStatusCode.NotFound, "Bet doesnt find") - } - bets.remove(findbet) - findbet as Bet - call.respond(HttpStatusCode.Accepted, findbet) + route("/bets/delete") { + post { + val idbet = call.receive>()["id"] + bets.find { it.id == idbet }?.let { findbet -> + bets.remove(findbet) + call.respond(HttpStatusCode.Accepted, findbet) + } ?: call.respond(HttpStatusCode.NotFound, ApiMessage.BetNotFound) } } - route("bets/update"){ - post{ + route("bets/update") { + post { val updatedBetData = call.receive() - val findbet = bets.find { it.id == updatedBetData.id } - if (findbet == null) { - call.respond(HttpStatusCode.NotFound, "Bet not found") - } else { + bets.find { it.id == updatedBetData.id }?.let { findbet -> findbet.endBet = updatedBetData.endBet findbet.isPrivate = updatedBetData.isPrivate findbet.response = updatedBetData.response call.respond(HttpStatusCode.Accepted, findbet) + } ?: call.respond(HttpStatusCode.NotFound, ApiMessage.BetNotFound) + } + } + + authenticate { + get("/bets/current") { + hasToken { principal -> + verifyUserFromToken(principal) { user -> + val bets = participations + .filter { it.username == user.username } + .mapNotNull { itParticipation -> bets.find { it.id == itParticipation.betId } } + call.respond(HttpStatusCode.OK, bets) + } } } } diff --git a/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt b/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt new file mode 100644 index 0000000..11ac4da --- /dev/null +++ b/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt @@ -0,0 +1,56 @@ +package allin.routing + +import allin.ext.hasToken +import allin.ext.verifyUserFromToken +import allin.model.ApiMessage +import allin.model.Participation +import allin.model.ParticipationRequest +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 java.util.* + +val participations = mutableListOf() + +fun Application.ParticipationRouter() { + routing { + authenticate { + post("/participations/add") { + hasToken { principal -> + val participation = call.receive() + verifyUserFromToken(principal) { user -> + if (user.nbCoins >= participation.stake) { + participations.add( + Participation( + id = UUID.randomUUID().toString(), + betId = participation.betId, + username = user.username, + answer = participation.answer, + stake = participation.stake + ) + ) + call.respond(HttpStatusCode.Created) + } else { + call.respond(HttpStatusCode.Forbidden, ApiMessage.NotEnoughCoins) + } + } + } + } + delete("/participations/delete") { + hasToken { principal -> + val participationId = call.receive() + participations.find { it.id == participationId }?.let { participation -> + verifyUserFromToken(principal) { user -> + user.nbCoins += participation.stake + participations.remove(participation) + call.respond(HttpStatusCode.NoContent) + } + } ?: call.respond(HttpStatusCode.NotFound, ApiMessage.ParticipationNotFound) + } + } + } + } +} \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/routing/UserRouter.kt b/Sources/src/main/kotlin/allin/routing/UserRouter.kt index 18489be..5cf3ba3 100644 --- a/Sources/src/main/kotlin/allin/routing/UserRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/UserRouter.kt @@ -1,81 +1,92 @@ package allin.routing -import allin.dto.* +import allin.dto.convertUserToUserDTO +import allin.dto.convertUserToUserDTOToken +import allin.ext.hasToken +import allin.ext.verifyUserFromToken +import allin.model.ApiMessage import allin.model.CheckUser import allin.model.User +import allin.model.UserRequest import allin.utils.AppConfig 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 java.util.* val users = mutableListOf() -val RegexCheckerUser= AppConfig.regexChecker -val CryptManagerUser= AppConfig.cryptManager -val tokenManagerUser=AppConfig.tokenManager - +val RegexCheckerUser = AppConfig.regexChecker +val CryptManagerUser = AppConfig.cryptManager +val tokenManagerUser = AppConfig.tokenManager +const val DEFAULT_COINS = 500 fun Application.UserRouter() { routing { - route("/users/register"){ + route("/users/register") { post { - val TempUser = call.receive() - if (RegexCheckerUser.isEmailInvalid(TempUser.email)){ - call.respond(HttpStatusCode.Forbidden,"Input a valid mail !") + val tempUser = call.receive() + if (RegexCheckerUser.isEmailInvalid(tempUser.email)) { + call.respond(HttpStatusCode.Forbidden, ApiMessage.InvalidMail) } - val user = users.find { it.username == TempUser.username || it.email == TempUser.email } - if(user == null) { - CryptManagerUser.passwordCrypt(TempUser) - TempUser.token=tokenManagerUser.generateOrReplaceJWTToken(TempUser) - users.add(TempUser) - call.respond(HttpStatusCode.Created, TempUser) + users.find { it.username == tempUser.username || it.email == tempUser.email }?.let { user -> + call.respond(HttpStatusCode.Conflict, ApiMessage.UserAlreadyExist) + } ?: run { + 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) + users.add(user) + call.respond(HttpStatusCode.Created, user) } - call.respond(HttpStatusCode.Conflict,"Mail or/and username already exist") } } route("/users/login") { post { val checkUser = call.receive() - val user = users.find { it.username == checkUser.login || it.email == checkUser.login } - if (user != null && CryptManagerUser.passwordDecrypt(user,checkUser.password)) { - user.token=tokenManagerUser.generateOrReplaceJWTToken(user) - call.respond(HttpStatusCode.OK, convertUserToUserDTOToken(user)) - } else { - call.respond(HttpStatusCode.NotFound,"Login and/or password incorrect.") - } + users.find { it.username == checkUser.login || it.email == checkUser.login }?.let { user -> + if (CryptManagerUser.passwordDecrypt(user, checkUser.password)) { + user.token = tokenManagerUser.generateOrReplaceJWTToken(user) + call.respond(HttpStatusCode.OK, convertUserToUserDTOToken(user)) + } else { + call.respond(HttpStatusCode.NotFound, ApiMessage.IncorrectLoginPassword) + } + } ?: call.respond(HttpStatusCode.NotFound, ApiMessage.IncorrectLoginPassword) } } - route("/users/delete") { - post { - val checkUser = call.receive() - val user = users.find { it.username == checkUser.login || it.email == checkUser.login } - if (user != null && user.password == checkUser.password) { - users.remove(user) - call.respond(HttpStatusCode.Accepted,convertUserToUserDTO(user)) - } else { - call.respond(HttpStatusCode.NotFound,"Login and/or password incorrect.") + authenticate { + post("/users/delete") { + hasToken { principal -> + verifyUserFromToken(principal) { user -> + val checkUser = call.receive() + if (user.username == checkUser.login && user.password == checkUser.password) { + users.remove(user) + call.respond(HttpStatusCode.Accepted, convertUserToUserDTO(user)) + } else { + call.respond(HttpStatusCode.NotFound, ApiMessage.IncorrectLoginPassword) + } + } } } - } - - authenticate { + get("/users/token") { - val principal = call.principal() - val username = principal!!.payload.getClaim("username").asString() - val user = users.find { it.username == username } - if (user != null) { - call.respond(HttpStatusCode.OK,convertUserToUserDTO(user)) - } else { - call.respond(HttpStatusCode.NotFound, "User not found with the valid token !") + hasToken { principal -> + verifyUserFromToken(principal) { user -> + call.respond(HttpStatusCode.OK, convertUserToUserDTO(user)) + } } } } - } } diff --git a/Sources/src/main/kotlin/allin/serializer/DateSerializer.kt b/Sources/src/main/kotlin/allin/serializer/DateSerializer.kt index 6002c28..e18b220 100644 --- a/Sources/src/main/kotlin/allin/serializer/DateSerializer.kt +++ b/Sources/src/main/kotlin/allin/serializer/DateSerializer.kt @@ -1,27 +1,25 @@ package allin.serializer -import kotlinx.serialization.* +import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import java.text.SimpleDateFormat -import java.util.* +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime -@Serializer(Date::class) -class DateSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING) +object ZonedDateTimeSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("ZonedDateTime", PrimitiveKind.LONG) - val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.FRANCE) - - override fun deserialize(decoder: Decoder): Date { - val dateString = decoder.decodeString() - return formatter.parse(dateString) + override fun serialize(encoder: Encoder, value: ZonedDateTime) { + encoder.encodeLong(value.toEpochSecond()) } - override fun serialize(encoder: Encoder, value: Date) { - val dateString = formatter.format(value) - encoder.encodeString(dateString) + override fun deserialize(decoder: Decoder): ZonedDateTime { + val epoch = decoder.decodeLong() + return ZonedDateTime.ofInstant(Instant.ofEpochSecond(epoch), ZoneId.systemDefault()) } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/utils/TokenManager.kt b/Sources/src/main/kotlin/allin/utils/TokenManager.kt index d35ede5..dbebe03 100644 --- a/Sources/src/main/kotlin/allin/utils/TokenManager.kt +++ b/Sources/src/main/kotlin/allin/utils/TokenManager.kt @@ -10,19 +10,18 @@ import java.util.* class TokenManager private constructor(val config: HoconApplicationConfig) { - val audience=config.property("audience").getString() - val secret=config.property("secret").getString() - val issuer=config.property("issuer").getString() - fun generateJWTToken(user : User): String { + val audience = config.property("audience").getString() + val secret = config.property("secret").getString() + val issuer = config.property("issuer").getString() + fun generateJWTToken(user: User): String { val expirationDate = System.currentTimeMillis() + 604800000 // une semaine en miliseconde - val token = JWT.create() + return JWT.create() .withAudience(audience) .withIssuer(issuer) .withClaim("username", user.username) .withExpiresAt(Date(expirationDate)) .sign(Algorithm.HMAC256(secret)) - return token } fun verifyJWTToken(): JWTVerifier { @@ -34,10 +33,10 @@ class TokenManager private constructor(val config: HoconApplicationConfig) { fun generateOrReplaceJWTToken(user: User): String { val userToken = getUserToken(user) - if (userToken != null && !isTokenExpired(userToken)) { - return userToken + return if (userToken != null && !isTokenExpired(userToken)) { + userToken } else { - return generateJWTToken(user) + generateJWTToken(user) } } @@ -50,10 +49,11 @@ class TokenManager private constructor(val config: HoconApplicationConfig) { return user.token } - fun getUsernameFromToken(token: String) : String{ + fun getUsernameFromToken(token: String): String { val decodedJWT: DecodedJWT = JWT.decode(token) return decodedJWT.getClaim("username").asString() } + companion object { private var instance: TokenManager? = null fun getInstance(config: HoconApplicationConfig): TokenManager {