Start bet confirmation
continuous-integration/drone/push Build is passing Details

pull/10/head
Arthur VALIN 1 year ago
parent 7ba9d2715d
commit 9e1fcee33b

@ -5,6 +5,7 @@ import allin.data.mock.MockDataSource
import allin.data.postgres.PostgresDataSource
import allin.routing.*
import allin.utils.TokenManager
import allin.utils.TokenManager.Companion.Claims.USERNAME
import allin.utils.kronJob
import com.typesafe.config.ConfigFactory
import io.ktor.serialization.kotlinx.json.*
@ -48,7 +49,7 @@ private fun Application.extracted() {
verifier(tokenManager.verifyJWTToken())
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
}

@ -1,12 +1,7 @@
package allin.data
abstract class AllInDataSource {
abstract val userDataSource: UserDataSource
abstract val betDataSource: BetDataSource
abstract val participationDataSource: ParticipationDataSource
}

@ -12,4 +12,6 @@ interface BetDataSource {
fun removeBet(id: String): Boolean
fun updateBet(data: UpdatedBetData): Boolean
fun updateBetStatuses(date: ZonedDateTime)
fun getToConfirm(username: String): List<Bet>
fun confirmBet(betId: String, result: String)
}

@ -3,16 +3,8 @@ package allin.data
import allin.model.Participation
interface ParticipationDataSource {
fun addParticipation(participation: Participation)
fun getParticipationFromBetId(betid: String): List<Participation>
fun getParticipationFromUserId(username: String, betid: String): List<Participation>
fun deleteParticipation(id: String): Boolean
}

@ -6,18 +6,10 @@ import allin.model.User
interface UserDataSource {
fun getUserByUsername(username: String): Pair<UserDTO?, String?>
fun addUser(user: User)
fun deleteUser(username: String): Boolean
fun addCoins(username: String, amount: Int)
fun removeCoins(username: String, amount: Int)
fun userExists(username: String, email: String): Boolean
fun canHaveDailyGift(username: String): Boolean
}

@ -2,6 +2,7 @@ package allin.data.mock
import allin.data.BetDataSource
import allin.model.Bet
import allin.model.BetResult
import allin.model.BetStatus
import allin.model.UpdatedBetData
import java.time.ZonedDateTime
@ -39,6 +40,24 @@ class MockBetDataSource : BetDataSource {
}
}
override fun getToConfirm(username: String): List<Bet> =
bets.filter { it.createdBy == username && it.status == BetStatus.CLOSING }
override fun confirmBet(betId: String, result: String) {
results.add(
BetResult(
betId = betId,
result = result
)
)
bets.replaceAll {
if (it.id == betId) {
it.copy(status = BetStatus.FINISHED)
} else it
}
}
private val bets by lazy { mutableListOf<Bet>() }
private val results by lazy { mutableListOf<BetResult>() }
}

@ -1,11 +1,8 @@
package allin.data.postgres
import allin.data.BetDataSource
import allin.entities.BetsEntity
import allin.entities.NO_VALUE
import allin.entities.ResponsesEntity
import allin.entities.ResponsesEntity.response
import allin.entities.YES_VALUE
import allin.data.postgres.entities.*
import allin.data.postgres.entities.ResponsesEntity.response
import allin.model.Bet
import allin.model.BetStatus
import allin.model.BetType
@ -61,6 +58,27 @@ class PostgresBetDataSource(private val database: Database) : BetDataSource {
.mapToBet()
}
override fun getToConfirm(username: String): List<Bet> {
return database.from(BetsEntity)
.select()
.where {
(BetsEntity.createdBy eq username) and
(BetsEntity.status eq BetStatus.CLOSING)
}.mapToBet()
}
override fun confirmBet(betId: String, result: String) {
database.insert(BetResultsEntity) {
set(it.betId, betId)
set(it.result, result)
}
database.update(BetsEntity) {
where { BetsEntity.id eq UUID.fromString(betId) }
set(BetsEntity.status, BetStatus.FINISHED)
}
}
override fun addBet(bet: Bet) {
database.insert(BetsEntity) {
set(it.id, UUID.fromString(bet.id))

@ -32,7 +32,8 @@ class PostgresDataSource : AllInDataSource() {
coins double precision,
email VARCHAR(255),
lastgift timestamp
)""".trimIndent()
)
""".trimIndent()
)
database.Execute(
@ -53,7 +54,17 @@ class PostgresDataSource : AllInDataSource() {
createdby varchar(250),
status varchar(20),
type varchar(20)
)""".trimIndent()
)
""".trimIndent()
)
database.Execute(
"""
CREATE TABLE IF NOT EXISTS betresult (
betid uuid PRIMARY KEY REFERENCES bet,
result varchar(250),
)
""".trimIndent()
)
database.Execute(
@ -64,7 +75,8 @@ class PostgresDataSource : AllInDataSource() {
username varchar(250),
answer varchar(250),
stake int
)""".trimIndent()
)
""".trimIndent()
)
database.Execute(
@ -73,7 +85,8 @@ class PostgresDataSource : AllInDataSource() {
id UUID,
response VARCHAR(250),
CONSTRAINT pk_response_id PRIMARY KEY (id,response)
)""".trimIndent()
)
""".trimIndent()
)
}

@ -1,7 +1,7 @@
package allin.data.postgres
import allin.data.ParticipationDataSource
import allin.entities.ParticipationsEntity
import allin.data.postgres.entities.ParticipationsEntity
import allin.model.Participation
import org.ktorm.database.Database
import org.ktorm.dsl.*
@ -30,28 +30,23 @@ class PostgresParticipationDataSource(private val database: Database) : Particip
}
}
override fun getParticipationFromBetId(betid: String): List<Participation> {
return database.from(ParticipationsEntity)
override fun getParticipationFromBetId(betid: String): List<Participation> =
database.from(ParticipationsEntity)
.select()
.where { ParticipationsEntity.betId eq UUID.fromString(betid) }
.mapToParticipation()
}
override fun getParticipationFromUserId(username: String, betid: String): List<Participation> {
return database.from(ParticipationsEntity)
override fun getParticipationFromUserId(username: String, betid: String): List<Participation> =
database.from(ParticipationsEntity)
.select()
.where { (ParticipationsEntity.betId eq UUID.fromString(betid)) and (ParticipationsEntity.username eq username) }
.mapToParticipation()
}
fun getParticipationEntity(): List<Participation> {
return database.from(ParticipationsEntity).select().mapToParticipation()
}
fun getParticipationEntity(): List<Participation> =
database.from(ParticipationsEntity).select().mapToParticipation()
override fun deleteParticipation(id: String): Boolean {
return database.delete(ParticipationsEntity) {
override fun deleteParticipation(id: String): Boolean =
database.delete(ParticipationsEntity) {
it.id eq UUID.fromString(id)
} > 0
}
}

@ -1,8 +1,8 @@
package allin.data.postgres
import allin.data.UserDataSource
import allin.data.postgres.entities.UsersEntity
import allin.dto.UserDTO
import allin.entities.UsersEntity
import allin.model.User
import allin.utils.ExecuteWithResult
import org.ktorm.database.Database

@ -1,4 +1,4 @@
package allin.entities
package allin.data.postgres.entities
import allin.model.BetStatus
import allin.model.BetType
@ -21,12 +21,12 @@ interface BetEntity : Entity<BetEntity> {
object BetsEntity : Table<BetEntity>("bet") {
val id = uuid("id").primaryKey()
val theme = varchar("theme")
val sentenceBet = varchar("sentencebet")
val theme = varchar("theme").bindTo { it.theme }
val sentenceBet = varchar("sentencebet").bindTo { it.sentenceBet }
val endRegistration = timestamp("endregistration")
val endBet = timestamp("endbet")
val isPrivate = boolean("isprivate")
val isPrivate = boolean("isprivate").bindTo { it.isPrivate }
val status = pgEnum<BetStatus>("status").bindTo { it.status }
val type = pgEnum<BetType>("type").bindTo { it.type }
val createdBy = varchar("createdby")
val createdBy = varchar("createdby").bindTo { it.createdBy }
}

@ -0,0 +1,17 @@
package allin.data.postgres.entities
import org.ktorm.entity.Entity
import org.ktorm.schema.Table
import org.ktorm.schema.uuid
import org.ktorm.schema.varchar
interface BetResultEntity : Entity<BetResultEntity> {
val bet: BetEntity
val result: String
}
object BetResultsEntity : Table<BetResultEntity>("betresult") {
val betId = uuid("betid").primaryKey().references(BetsEntity) { it.bet }
val result = varchar("result").bindTo { it.result }
}

@ -1,4 +1,4 @@
package allin.entities
package allin.data.postgres.entities
import org.ktorm.entity.Entity
import org.ktorm.schema.Table
@ -8,16 +8,16 @@ import org.ktorm.schema.varchar
interface ParticipationEntity : Entity<ParticipationEntity> {
val id: String
val betId: String
val bet: BetEntity
val username: String
val answer: String
val stake: Int
}
object ParticipationsEntity : Table<BetEntity>("participation") {
object ParticipationsEntity : Table<ParticipationEntity>("participation") {
val id = uuid("id").primaryKey()
val betId = uuid("bet")
val betId = uuid("bet").references(BetsEntity) { it.bet }
val username = varchar("username")
val answer = varchar("answer")
val stake = int("stake")

@ -1,4 +1,4 @@
package allin.entities
package allin.data.postgres.entities
import org.ktorm.entity.Entity
import org.ktorm.schema.Table

@ -1,4 +1,4 @@
package allin.entities
package allin.data.postgres.entities
import org.ktorm.entity.Entity
import org.ktorm.schema.*
@ -12,10 +12,10 @@ interface UserEntity : Entity<UserEntity> {
object UsersEntity : Table<UserEntity>("utilisateur") {
val id = uuid("id").primaryKey()
val username = varchar("username")
val password = varchar("password")
val nbCoins = int("coins")
val email = varchar("email")
val username = varchar("username").bindTo { it.username }
val password = varchar("password").bindTo { it.password }
val nbCoins = int("coins").bindTo { it.nbCoins }
val email = varchar("email").bindTo { it.email }
val lastGift = timestamp("lastgift")
}

@ -1,4 +1,12 @@
package allin.dto
import kotlinx.serialization.Serializable
@Serializable
data class UserDTO(val id: String, val username: String, val email: String, val nbCoins: Int, var token:String?)
data class UserDTO(
val id: String,
val username: String,
val email: String,
val nbCoins: Int,
var token: String?
)

@ -3,6 +3,7 @@ package allin.ext
import allin.data.UserDataSource
import allin.dto.UserDTO
import allin.model.ApiMessage
import allin.utils.TokenManager.Companion.Claims.USERNAME
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
@ -10,18 +11,16 @@ 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<JWTPrincipal>()?.let { content(it) } ?: call.respond(HttpStatusCode.Unauthorized)
suspend fun PipelineContext<*, ApplicationCall>.verifyUserFromToken(
userDataSource: UserDataSource,
principal: JWTPrincipal,
content: suspend (user: UserDTO, password: String) -> Unit
) {
val username = principal.payload.getClaim("username").asString()
val username = principal.payload.getClaim(USERNAME).asString()
val userPassword = userDataSource.getUserByUsername(username)
userPassword.first?.let { content(it, userPassword.second ?: "") }
?: call.respond(HttpStatusCode.NotFound, ApiMessage.TokenUserNotFound)
?: call.respond(HttpStatusCode.NotFound, ApiMessage.TOKEN_USER_NOT_FOUND)
}

@ -0,0 +1,10 @@
package allin.model
import kotlinx.serialization.Serializable
@Serializable
data class AllInResponse<T : Any>(
val value: T,
val toConfirm: List<Bet>,
val won: List<BetResult>
)

@ -1,14 +1,15 @@
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 UserNotFound = "User not found."
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."
const val WELCOME = "Welcome on AllIn's API !"
const val TOKEN_USER_NOT_FOUND = "User not found with the valid token !"
const val USER_NOT_FOUND = "User not found."
const val BET_NOT_FOUND = "Bet not found."
const val BET_ALREADY_EXIST = "Bet already exists."
const val INCORRECT_LOGIN_PASSWORD = "Login and/or password incorrect."
const val USER_ALREADY_EXISTS = "Mail and/or username already exists."
const val INVALID_MAIL = "Invalid mail."
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."
}

@ -0,0 +1,9 @@
package allin.model
import kotlinx.serialization.Serializable
@Serializable
data class BetResult(
val betId: String,
val result: String
)

@ -0,0 +1,12 @@
package allin.model
import kotlinx.serialization.Serializable
@Serializable
data class ParticipationDetail(
val id: String,
val bet: Bet,
val username: String,
val answer: String,
val stake: Int
)

@ -1,5 +1,6 @@
package allin.routing
import allin.model.ApiMessage
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@ -8,7 +9,7 @@ import io.ktor.server.routing.*
fun Application.BasicRouting() {
routing {
get("/") {
call.respond("Bienvenue sur l'API de AlLin!")
call.respond(ApiMessage.WELCOME)
}
}
}

@ -3,6 +3,7 @@ package allin.routing
import allin.dataSource
import allin.ext.hasToken
import allin.ext.verifyUserFromToken
import allin.model.ApiMessage
import allin.model.BetDetail
import allin.model.getBetAnswerDetail
import io.ktor.http.*
@ -36,7 +37,7 @@ fun Application.BetDetailRouter() {
)
)
} else {
call.respond(HttpStatusCode.NotFound, "Bet not found")
call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
}
}
}

@ -3,6 +3,7 @@ package allin.routing
import allin.dataSource
import allin.ext.hasToken
import allin.ext.verifyUserFromToken
import allin.model.AllInResponse
import allin.model.ApiMessage
import allin.model.Bet
import allin.model.UpdatedBetData
@ -32,7 +33,7 @@ fun Application.BetRouter() {
val id = UUID.randomUUID().toString()
val username = tokenManagerBet.getUsernameFromToken(principal)
betDataSource.getBetById(id)?.let {
call.respond(HttpStatusCode.Conflict, ApiMessage.BetAlreadyExist)
call.respond(HttpStatusCode.Conflict, ApiMessage.BET_ALREADY_EXIST)
} ?: run {
val betWithId = bet.copy(id = id, createdBy = username)
betDataSource.addBet(betWithId)
@ -44,12 +45,18 @@ fun Application.BetRouter() {
}
}
route("/bets/gets") {
get {
// if(bets.size>0)
call.respond(HttpStatusCode.Accepted, betDataSource.getAllBets())
// else call.respond(HttpStatusCode.NoContent)
authenticate {
get("/bets/gets") {
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
val response = AllInResponse(
value = betDataSource.getAllBets(),
won = emptyList(),
toConfirm = betDataSource.getToConfirm(user.username)
)
call.respond(HttpStatusCode.Accepted, response)
}
}
}
}
@ -58,7 +65,7 @@ fun Application.BetRouter() {
val id = call.parameters["id"] ?: ""
betDataSource.getBetById(id)?.let { bet ->
call.respond(HttpStatusCode.Accepted, bet)
} ?: call.respond(HttpStatusCode.NotFound, ApiMessage.BetNotFound)
} ?: call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
}
}
@ -68,7 +75,7 @@ fun Application.BetRouter() {
if (betDataSource.removeBet(id)) {
call.respond(HttpStatusCode.Accepted)
} else {
call.respond(HttpStatusCode.NotFound, ApiMessage.BetNotFound)
call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
}
}
@ -79,7 +86,7 @@ fun Application.BetRouter() {
if (betDataSource.updateBet(updatedBetData)) {
call.respond(HttpStatusCode.Accepted)
} else {
call.respond(HttpStatusCode.NotFound, ApiMessage.BetNotFound)
call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
}
}
}
@ -100,5 +107,25 @@ fun Application.BetRouter() {
}
}
}
authenticate {
get("/bets/confirm/{id}") {
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
val betId = call.parameters["id"] ?: ""
val result = call.receive<String>()
if (betDataSource.getBetById(betId)?.createdBy == user.username) {
betDataSource.confirmBet(betId, result)
call.respond(HttpStatusCode.OK)
} else {
call.respond(HttpStatusCode.Unauthorized)
}
}
}
}
}
}
}

@ -41,7 +41,7 @@ fun Application.ParticipationRouter() {
call.respond(HttpStatusCode.Created)
} else {
call.respond(HttpStatusCode.Forbidden, ApiMessage.NotEnoughCoins)
call.respond(HttpStatusCode.Forbidden, ApiMessage.NOT_ENOUGH_COINS)
}
}
}
@ -52,7 +52,7 @@ fun Application.ParticipationRouter() {
if (participationDataSource.deleteParticipation(participationId)) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, ApiMessage.ParticipationNotFound)
call.respond(HttpStatusCode.NotFound, ApiMessage.PARTICIPATION_NOT_FOUND)
}
}
}

@ -28,10 +28,10 @@ fun Application.UserRouter() {
post {
val tempUser = call.receive<UserRequest>()
if (RegexCheckerUser.isEmailInvalid(tempUser.email)) {
call.respond(HttpStatusCode.Forbidden, ApiMessage.InvalidMail)
call.respond(HttpStatusCode.Forbidden, ApiMessage.INVALID_MAIL)
}
if (userDataSource.userExists(tempUser.username, tempUser.email)) {
call.respond(HttpStatusCode.Conflict, ApiMessage.UserAlreadyExist)
call.respond(HttpStatusCode.Conflict, ApiMessage.USER_ALREADY_EXISTS)
}
val user = User(
@ -57,9 +57,9 @@ fun Application.UserRouter() {
user.first?.let { userDtoWithToken ->
userDtoWithToken.token = tokenManagerUser.generateOrReplaceJWTToken(userDtoWithToken)
call.respond(HttpStatusCode.OK, userDtoWithToken)
} ?: call.respond(HttpStatusCode.NotFound, ApiMessage.UserNotFound)
} ?: call.respond(HttpStatusCode.NotFound, ApiMessage.USER_NOT_FOUND)
} else {
call.respond(HttpStatusCode.NotFound, ApiMessage.IncorrectLoginPassword)
call.respond(HttpStatusCode.NotFound, ApiMessage.INCORRECT_LOGIN_PASSWORD)
}
}
}
@ -75,7 +75,7 @@ fun Application.UserRouter() {
}
call.respond(HttpStatusCode.Accepted, password)
} else {
call.respond(HttpStatusCode.NotFound, "Login and/or password incorrect.")
call.respond(HttpStatusCode.NotFound, ApiMessage.INCORRECT_LOGIN_PASSWORD)
}
}
@ -96,7 +96,7 @@ fun Application.UserRouter() {
val dailyGift = getDailyGift()
userDataSource.addCoins(userDto.username, dailyGift)
call.respond(HttpStatusCode.OK, dailyGift)
} else call.respond(HttpStatusCode.MethodNotAllowed, "Can't get daily gift.")
} else call.respond(HttpStatusCode.MethodNotAllowed, ApiMessage.NO_GIFT)
}
}
}

@ -1,13 +1,10 @@
package allin.utils
class RegexChecker {
private val emailRegex="^[A-Za-z0-9+_.-]+@(.+)$"
private val emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$"
fun isEmailInvalid(email: String): Boolean {
val emailRegex = Regex(emailRegex)
return !emailRegex.matches(email)
}
}

@ -2,26 +2,26 @@ package allin.utils
import allin.dto.UserDTO
import allin.model.User
import allin.utils.TokenManager.Companion.Claims.USERNAME
import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.interfaces.DecodedJWT
import io.ktor.server.auth.jwt.*
import io.ktor.server.config.*
import java.util.*
class TokenManager private constructor(val config: HoconApplicationConfig) {
class TokenManager private constructor(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 {
private val audience = config.property("audience").getString()
private val secret = config.property("secret").getString()
private val issuer = config.property("issuer").getString()
private fun generateJWTToken(user: User): String {
val expirationDate = System.currentTimeMillis() + 604800000 // une semaine en miliseconde
return JWT.create()
.withAudience(audience)
.withIssuer(issuer)
.withClaim("username", user.username)
.withClaim(USERNAME, user.username)
.withExpiresAt(Date(expirationDate))
.sign(Algorithm.HMAC256(secret))
}
@ -52,30 +52,34 @@ class TokenManager private constructor(val config: HoconApplicationConfig) {
}
}
fun generateJWTToken(user: UserDTO): String {
private fun generateJWTToken(user: UserDTO): String {
val expirationDate = System.currentTimeMillis() + 604800000 // une semaine en miliseconde
return JWT.create()
.withAudience(audience)
.withIssuer(issuer)
.withClaim("username", user.username)
.withClaim(USERNAME, user.username)
.withExpiresAt(Date(expirationDate))
.sign(Algorithm.HMAC256(secret))
}
fun isTokenExpired(token: String): Boolean {
private fun isTokenExpired(token: String): Boolean {
val expirationTime = JWT.decode(token).expiresAt.time
return System.currentTimeMillis() > expirationTime
}
fun getUserToken(user: User): String? = user.token
fun getUserToken(user: UserDTO): String? = user.token
private fun getUserToken(user: User): String? = user.token
private fun getUserToken(user: UserDTO): String? = user.token
fun getUsernameFromToken(principal: JWTPrincipal): String {
return principal.payload.getClaim("username").asString()
return principal.payload.getClaim(USERNAME).asString()
}
companion object {
object Claims {
const val USERNAME = "username"
}
private var instance: TokenManager? = null
fun getInstance(config: HoconApplicationConfig): TokenManager {
return instance ?: synchronized(this) {

Loading…
Cancel
Save