Merge pull request 'bet_confirmation' (#10) from bet_confirmation into master
continuous-integration/drone/push Build is passing Details

Reviewed-on: #10
pull/12/head
Arthur VALIN 1 year ago
commit 464bdfeee1

@ -5,6 +5,7 @@ import allin.data.mock.MockDataSource
import allin.data.postgres.PostgresDataSource import allin.data.postgres.PostgresDataSource
import allin.routing.* import allin.routing.*
import allin.utils.TokenManager import allin.utils.TokenManager
import allin.utils.TokenManager.Companion.Claims.USERNAME
import allin.utils.kronJob import allin.utils.kronJob
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import io.ktor.serialization.kotlinx.json.* import io.ktor.serialization.kotlinx.json.*
@ -48,7 +49,7 @@ private fun Application.extracted() {
verifier(tokenManager.verifyJWTToken()) verifier(tokenManager.verifyJWTToken())
realm = config.property("realm").getString() realm = config.property("realm").getString()
validate { jwtCredential -> validate { jwtCredential ->
if (jwtCredential.payload.getClaim("username").asString().isNotEmpty()) if (jwtCredential.payload.getClaim(USERNAME).asString().isNotEmpty())
JWTPrincipal(jwtCredential.payload) JWTPrincipal(jwtCredential.payload)
else null else null
} }

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

@ -1,6 +1,8 @@
package allin.data package allin.data
import allin.model.Bet import allin.model.Bet
import allin.model.BetDetail
import allin.model.BetResultDetail
import allin.model.UpdatedBetData import allin.model.UpdatedBetData
import java.time.ZonedDateTime import java.time.ZonedDateTime
@ -12,4 +14,9 @@ interface BetDataSource {
fun removeBet(id: String): Boolean fun removeBet(id: String): Boolean
fun updateBet(data: UpdatedBetData): Boolean fun updateBet(data: UpdatedBetData): Boolean
fun updateBetStatuses(date: ZonedDateTime) fun updateBetStatuses(date: ZonedDateTime)
fun getToConfirm(username: String): List<Bet>
fun confirmBet(betId: String, result: String)
fun getWonNotifications(username: String): List<BetResultDetail>
fun getHistory(username: String): List<BetResultDetail>
fun getCurrent(username: String): List<BetDetail>
} }

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

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

@ -1,12 +1,16 @@
package allin.data.mock package allin.data.mock
import allin.data.BetDataSource import allin.data.BetDataSource
import allin.model.Bet import allin.model.*
import allin.model.BetStatus import allin.model.BetStatus.*
import allin.model.UpdatedBetData
import java.time.ZonedDateTime import java.time.ZonedDateTime
class MockBetDataSource : BetDataSource { class MockBetDataSource(mockData: MockDataSource.MockData) : BetDataSource {
private val bets = mockData.bets
private val results = mockData.results
private val participations = mockData.participations
private val resultNotifications = mockData.resultNotifications
override fun getAllBets(): List<Bet> = bets override fun getAllBets(): List<Bet> = bets
override fun getBetById(id: String): Bet? = override fun getBetById(id: String): Bet? =
bets.find { it.id == id } bets.find { it.id == id }
@ -31,14 +35,91 @@ class MockBetDataSource : BetDataSource {
bets.forEachIndexed { idx, bet -> bets.forEachIndexed { idx, bet ->
if (date >= bet.endRegistration) { if (date >= bet.endRegistration) {
if (date >= bet.endBet) { if (date >= bet.endBet) {
bets[idx] = bet.copy(status = BetStatus.WAITING) bets[idx] = bet.copy(status = WAITING)
} else { } else {
bets[idx] = bet.copy(status = BetStatus.CLOSING) bets[idx] = bet.copy(status = CLOSING)
} }
} }
} }
} }
private val bets by lazy { mutableListOf<Bet>() } override fun getToConfirm(username: String): List<Bet> =
bets.filter { it.createdBy == username && it.status == CLOSING }
override fun confirmBet(betId: String, result: String) {
results.add(
BetResult(
betId = betId,
result = result
)
)
bets.replaceAll {
if (it.id == betId) {
it.copy(status = FINISHED)
} else it
}
participations.filter { it.betId == betId && it.answer == result }
.forEach {
resultNotifications.add(Pair(betId, it.username))
}
}
override fun getWonNotifications(username: String): List<BetResultDetail> {
return bets.map { bet ->
val notification = resultNotifications.find { it.first == bet.id } ?: return@map null
val result = results.find { it.betId == bet.id } ?: return@map null
val participation = participations.find { it.username == username && it.betId == bet.id }
?: return@map null
if (participation.answer == result.result) {
resultNotifications.remove(notification)
BetResultDetail(
betResult = result,
bet = bet,
participation = participation,
amount = participation.stake,
won = true
)
} else null
}.mapNotNull { it }
}
override fun getHistory(username: String): List<BetResultDetail> {
return bets.map { bet ->
val result = results.find { it.betId == bet.id } ?: return@map null
val participation = participations.find { it.username == username && it.betId == bet.id }
?: return@map null
BetResultDetail(
betResult = result,
bet = bet,
participation = participation,
amount = participation.stake,
won = participation.answer == result.result
)
}.mapNotNull { it }
}
override fun getCurrent(username: String): List<BetDetail> {
return bets.map { bet ->
when (bet.status) {
CANCELLED, FINISHED -> return@map null
else -> {
val participation = participations.find { it.username == username && it.betId == bet.id }
?: return@map null
val participations = participations.filter { it.betId == bet.id }
BetDetail(
bet = bet,
answers = getBetAnswerDetail(bet, participations),
participations = participations,
userParticipation = participation
)
}
}
}.mapNotNull { it }
}
} }

@ -4,9 +4,26 @@ import allin.data.AllInDataSource
import allin.data.BetDataSource import allin.data.BetDataSource
import allin.data.ParticipationDataSource import allin.data.ParticipationDataSource
import allin.data.UserDataSource import allin.data.UserDataSource
import allin.model.Bet
import allin.model.BetResult
import allin.model.Participation
import allin.model.User
import java.time.ZonedDateTime
class MockDataSource : AllInDataSource() { class MockDataSource : AllInDataSource() {
override val userDataSource: UserDataSource = MockUserDataSource()
override val betDataSource: BetDataSource = MockBetDataSource() class MockData {
override val participationDataSource: ParticipationDataSource = MockParticipationDataSource() val bets by lazy { mutableListOf<Bet>() }
val results by lazy { mutableListOf<BetResult>() }
val resultNotifications by lazy { mutableListOf<Pair<String, String>>() }
val users by lazy { mutableListOf<User>() }
val lastGifts by lazy { mutableMapOf<String, ZonedDateTime>() }
val participations by lazy { mutableListOf<Participation>() }
}
private val mockData by lazy { MockData() }
override val userDataSource: UserDataSource = MockUserDataSource(mockData)
override val betDataSource: BetDataSource = MockBetDataSource(mockData)
override val participationDataSource: ParticipationDataSource = MockParticipationDataSource(mockData)
} }

@ -3,7 +3,9 @@ package allin.data.mock
import allin.data.ParticipationDataSource import allin.data.ParticipationDataSource
import allin.model.Participation import allin.model.Participation
class MockParticipationDataSource : ParticipationDataSource { class MockParticipationDataSource(mockData: MockDataSource.MockData) : ParticipationDataSource {
private val participations = mockData.participations
override fun addParticipation(participation: Participation) { override fun addParticipation(participation: Participation) {
participations += participations participations += participations
} }
@ -16,7 +18,4 @@ class MockParticipationDataSource : ParticipationDataSource {
override fun deleteParticipation(id: String): Boolean = override fun deleteParticipation(id: String): Boolean =
participations.removeIf { it.id == id } participations.removeIf { it.id == id }
private val participations by lazy { mutableListOf<Participation>() }
} }

@ -5,7 +5,11 @@ import allin.dto.UserDTO
import allin.model.User import allin.model.User
import java.time.ZonedDateTime import java.time.ZonedDateTime
class MockUserDataSource : UserDataSource { class MockUserDataSource(mockData: MockDataSource.MockData) : UserDataSource {
private val users = mockData.users
private val lastGifts = mockData.lastGifts
override fun getUserByUsername(username: String): Pair<UserDTO?, String?> = override fun getUserByUsername(username: String): Pair<UserDTO?, String?> =
users.find { it.username == username }?.let { users.find { it.username == username }?.let {
Pair( Pair(
@ -49,12 +53,4 @@ class MockUserDataSource : UserDataSource {
lastGifts[username] = ZonedDateTime.now() lastGifts[username] = ZonedDateTime.now()
return value return value
} }
private val users by lazy {
mutableListOf<User>()
}
private val lastGifts by lazy {
mutableMapOf<String, ZonedDateTime>()
}
} }

@ -1,15 +1,9 @@
package allin.data.postgres package allin.data.postgres
import allin.data.BetDataSource import allin.data.BetDataSource
import allin.entities.BetsEntity import allin.data.postgres.entities.*
import allin.entities.NO_VALUE import allin.data.postgres.entities.ResponsesEntity.response
import allin.entities.ResponsesEntity import allin.model.*
import allin.entities.ResponsesEntity.response
import allin.entities.YES_VALUE
import allin.model.Bet
import allin.model.BetStatus
import allin.model.BetType
import allin.model.UpdatedBetData
import org.ktorm.database.Database import org.ktorm.database.Database
import org.ktorm.dsl.* import org.ktorm.dsl.*
import java.time.ZoneId import java.time.ZoneId
@ -43,7 +37,29 @@ class PostgresBetDataSource(private val database: Database) : BetDataSource {
} }
) )
private fun QueryRowSet.toParticipation() =
Participation(
id = this[ParticipationsEntity.id]?.toString() ?: "",
betId = this[ParticipationsEntity.betId]?.toString() ?: "",
username = this[ParticipationsEntity.username] ?: "",
answer = this[ParticipationsEntity.answer] ?: "",
stake = this[ParticipationsEntity.stake] ?: 0
)
private fun QueryRowSet.toBetResultDetail() =
BetResultDetail(
betResult = BetResult(
betId = this[BetResultsEntity.betId]?.toString() ?: "",
result = this[BetResultsEntity.result] ?: ""
),
bet = this.toBet(),
participation = this.toParticipation(),
amount = this[ParticipationsEntity.stake] ?: 0,
won = this[ParticipationsEntity.answer] == this[BetResultsEntity.result]
)
private fun Query.mapToBet() = this.map { it.toBet() } private fun Query.mapToBet() = this.map { it.toBet() }
private fun Query.mapToBetResultDetail() = this.map { it.toBetResultDetail() }
override fun getAllBets(): List<Bet> = override fun getAllBets(): List<Bet> =
database.from(BetsEntity).select().mapToBet() database.from(BetsEntity).select().mapToBet()
@ -61,6 +77,94 @@ class PostgresBetDataSource(private val database: Database) : BetDataSource {
.mapToBet() .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)
}
database.from(ParticipationsEntity)
.select()
.where {
(ParticipationsEntity.betId eq UUID.fromString(betId)) and
(ParticipationsEntity.answer eq result)
}
.forEach { participation ->
database.insert(BetResultNotificationsEntity) {
set(it.betId, betId)
set(it.username, participation[ParticipationsEntity.username])
}
}
}
override fun getWonNotifications(username: String): List<BetResultDetail> {
return database.from(BetsEntity)
.innerJoin(ParticipationsEntity, on = BetsEntity.id eq ParticipationsEntity.betId)
.innerJoin(BetResultsEntity, on = BetsEntity.id eq BetResultsEntity.betId)
.innerJoin(BetResultNotificationsEntity, on = BetsEntity.id eq BetResultNotificationsEntity.betId)
.select()
.where {
(BetResultsEntity.result eq ParticipationsEntity.answer) and
(ParticipationsEntity.username eq username)
}.let {
it.forEach { row ->
row[BetsEntity.id]?.let { betId ->
database.delete(BetResultNotificationsEntity) {
(it.betId eq betId) and (it.username eq username)
}
}
}
it
}.mapToBetResultDetail()
}
override fun getHistory(username: String): List<BetResultDetail> {
return database.from(BetsEntity)
.innerJoin(ParticipationsEntity, on = BetsEntity.id eq ParticipationsEntity.betId)
.innerJoin(BetResultsEntity, on = BetsEntity.id eq BetResultsEntity.betId)
.select()
.where { ParticipationsEntity.username eq username }.mapToBetResultDetail()
}
override fun getCurrent(username: String): List<BetDetail> {
return database.from(BetsEntity)
.innerJoin(ParticipationsEntity, on = BetsEntity.id eq ParticipationsEntity.betId)
.select()
.where {
(BetsEntity.status notEq BetStatus.FINISHED) and
(BetsEntity.status notEq BetStatus.CANCELLED) and
(ParticipationsEntity.username eq username)
}.map {
val participations = it[BetsEntity.id]?.let { betId ->
database.from(ParticipationsEntity)
.select().where { ParticipationsEntity.betId eq betId }.map { it.toParticipation() }
} ?: emptyList()
val bet = it.toBet()
BetDetail(
bet = bet,
answers = getBetAnswerDetail(bet, participations),
participations = participations,
userParticipation = it.toParticipation()
)
}
}
override fun addBet(bet: Bet) { override fun addBet(bet: Bet) {
database.insert(BetsEntity) { database.insert(BetsEntity) {
set(it.id, UUID.fromString(bet.id)) set(it.id, UUID.fromString(bet.id))

@ -32,17 +32,12 @@ class PostgresDataSource : AllInDataSource() {
coins double precision, coins double precision,
email VARCHAR(255), email VARCHAR(255),
lastgift timestamp lastgift timestamp
)""".trimIndent() )
""".trimIndent()
) )
database.Execute( database.Execute(
""" """
CREATE TYPE betstatus AS ENUM
('InProgress', 'Waiting', 'Closing', 'Finished', 'Cancelled');
CREATE TYPE bettype AS ENUM
('Match', 'Binary', 'Custom');
CREATE TABLE IF not exists bet ( CREATE TABLE IF not exists bet (
id uuid PRIMARY KEY, id uuid PRIMARY KEY,
theme VARCHAR(255), theme VARCHAR(255),
@ -53,7 +48,22 @@ class PostgresDataSource : AllInDataSource() {
createdby varchar(250), createdby varchar(250),
status varchar(20), status varchar(20),
type varchar(20) type varchar(20)
)""".trimIndent() )
""".trimIndent()
)
database.Execute(
"""
CREATE TABLE IF NOT EXISTS betresult (
betid uuid PRIMARY KEY REFERENCES bet,
result varchar(250)
)
CREATE TABLE IF NOT EXISTS betresultnotification (
betid uuid,
username varchar(250),
CONSTRAINT pk_id_username PRIMARY KEY (betid, username)
)
""".trimIndent()
) )
database.Execute( database.Execute(
@ -64,7 +74,8 @@ class PostgresDataSource : AllInDataSource() {
username varchar(250), username varchar(250),
answer varchar(250), answer varchar(250),
stake int stake int
)""".trimIndent() )
""".trimIndent()
) )
database.Execute( database.Execute(
@ -72,8 +83,9 @@ class PostgresDataSource : AllInDataSource() {
CREATE TABLE IF NOT EXISTS response ( CREATE TABLE IF NOT EXISTS response (
id UUID, id UUID,
response VARCHAR(250), response VARCHAR(250),
CONSTRAINT pk_response_id PRIMARY KEY (id,response) CONSTRAINT pk_response_id PRIMARY KEY (id, response)
)""".trimIndent() )
""".trimIndent()
) )
} }

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

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

@ -1,10 +1,9 @@
package allin.entities package allin.data.postgres.entities
import allin.model.BetStatus import allin.model.BetStatus
import allin.model.BetType import allin.model.BetType
import org.ktorm.entity.Entity import org.ktorm.entity.Entity
import org.ktorm.schema.* import org.ktorm.schema.*
import org.ktorm.support.postgresql.pgEnum
import java.time.ZonedDateTime import java.time.ZonedDateTime
@ -21,12 +20,12 @@ interface BetEntity : Entity<BetEntity> {
object BetsEntity : Table<BetEntity>("bet") { object BetsEntity : Table<BetEntity>("bet") {
val id = uuid("id").primaryKey() val id = uuid("id").primaryKey()
val theme = varchar("theme") val theme = varchar("theme").bindTo { it.theme }
val sentenceBet = varchar("sentencebet") val sentenceBet = varchar("sentencebet").bindTo { it.sentenceBet }
val endRegistration = timestamp("endregistration") val endRegistration = timestamp("endregistration")
val endBet = timestamp("endbet") val endBet = timestamp("endbet")
val isPrivate = boolean("isprivate") val isPrivate = boolean("isprivate").bindTo { it.isPrivate }
val status = pgEnum<BetStatus>("status").bindTo { it.status } val status = enum<BetStatus>("status").bindTo { it.status }
val type = pgEnum<BetType>("type").bindTo { it.type } val type = enum<BetType>("type").bindTo { it.type }
val createdBy = varchar("createdby") val createdBy = varchar("createdby").bindTo { it.createdBy }
} }

@ -0,0 +1,28 @@
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
import java.util.*
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 }
}
interface BetResultNotificationEntity : Entity<BetResultNotificationEntity> {
val betId: UUID
val username: String
}
object BetResultNotificationsEntity : Table<BetResultNotificationEntity>("betresult") {
val betId = uuid("betid").primaryKey()
val username = varchar("username").primaryKey()
}

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

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

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

@ -1,14 +1,15 @@
package allin.model package allin.model
object ApiMessage { object ApiMessage {
const val Welcome = "Welcome on AllIn's API !" const val WELCOME = "Welcome on AllIn's API !"
const val TokenUserNotFound = "User not found with the valid token !" const val TOKEN_USER_NOT_FOUND = "User not found with the valid token !"
const val UserNotFound = "User not found." const val USER_NOT_FOUND = "User not found."
const val BetNotFound = "Bet not found." const val BET_NOT_FOUND = "Bet not found."
const val BetAlreadyExist = "Bet already exists." const val BET_ALREADY_EXIST = "Bet already exists."
const val IncorrectLoginPassword = "Login and/or password incorrect." const val INCORRECT_LOGIN_PASSWORD = "Login and/or password incorrect."
const val UserAlreadyExist = "Mail and/or username already exists." const val USER_ALREADY_EXISTS = "Mail and/or username already exists."
const val InvalidMail = "Invalid mail." const val INVALID_MAIL = "Invalid mail."
const val ParticipationNotFound = "Participation not found." const val PARTICIPATION_NOT_FOUND = "Participation not found."
const val NotEnoughCoins = "Not enough coins." 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 BetResultDetail(
val betResult: BetResult,
val bet: Bet,
val participation: Participation,
val amount: Int,
val won: Boolean
)

@ -1,5 +1,6 @@
package allin.routing package allin.routing
import allin.model.ApiMessage
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
@ -8,7 +9,7 @@ import io.ktor.server.routing.*
fun Application.BasicRouting() { fun Application.BasicRouting() {
routing { routing {
get("/") { 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.dataSource
import allin.ext.hasToken import allin.ext.hasToken
import allin.ext.verifyUserFromToken import allin.ext.verifyUserFromToken
import allin.model.ApiMessage
import allin.model.BetDetail import allin.model.BetDetail
import allin.model.getBetAnswerDetail import allin.model.getBetAnswerDetail
import io.ktor.http.* import io.ktor.http.*
@ -36,7 +37,7 @@ fun Application.BetDetailRouter() {
) )
) )
} else { } else {
call.respond(HttpStatusCode.NotFound, "Bet not found") call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
} }
} }
} }

@ -3,9 +3,7 @@ package allin.routing
import allin.dataSource import allin.dataSource
import allin.ext.hasToken import allin.ext.hasToken
import allin.ext.verifyUserFromToken import allin.ext.verifyUserFromToken
import allin.model.ApiMessage import allin.model.*
import allin.model.Bet
import allin.model.UpdatedBetData
import allin.utils.AppConfig import allin.utils.AppConfig
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
@ -32,7 +30,7 @@ fun Application.BetRouter() {
val id = UUID.randomUUID().toString() val id = UUID.randomUUID().toString()
val username = tokenManagerBet.getUsernameFromToken(principal) val username = tokenManagerBet.getUsernameFromToken(principal)
betDataSource.getBetById(id)?.let { betDataSource.getBetById(id)?.let {
call.respond(HttpStatusCode.Conflict, ApiMessage.BetAlreadyExist) call.respond(HttpStatusCode.Conflict, ApiMessage.BET_ALREADY_EXIST)
} ?: run { } ?: run {
val betWithId = bet.copy(id = id, createdBy = username) val betWithId = bet.copy(id = id, createdBy = username)
betDataSource.addBet(betWithId) betDataSource.addBet(betWithId)
@ -44,12 +42,13 @@ fun Application.BetRouter() {
} }
} }
authenticate {
route("/bets/gets") { get("/bets/gets") {
get { hasToken { principal ->
// if(bets.size>0) verifyUserFromToken(userDataSource, principal) { user, _ ->
call.respond(HttpStatusCode.Accepted, betDataSource.getAllBets()) call.respond(HttpStatusCode.Accepted, betDataSource.getAllBets())
// else call.respond(HttpStatusCode.NoContent) }
}
} }
} }
@ -58,7 +57,7 @@ fun Application.BetRouter() {
val id = call.parameters["id"] ?: "" val id = call.parameters["id"] ?: ""
betDataSource.getBetById(id)?.let { bet -> betDataSource.getBetById(id)?.let { bet ->
call.respond(HttpStatusCode.Accepted, bet) call.respond(HttpStatusCode.Accepted, bet)
} ?: call.respond(HttpStatusCode.NotFound, ApiMessage.BetNotFound) } ?: call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
} }
} }
@ -68,7 +67,7 @@ fun Application.BetRouter() {
if (betDataSource.removeBet(id)) { if (betDataSource.removeBet(id)) {
call.respond(HttpStatusCode.Accepted) call.respond(HttpStatusCode.Accepted)
} else { } else {
call.respond(HttpStatusCode.NotFound, ApiMessage.BetNotFound) call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
} }
} }
@ -79,7 +78,46 @@ fun Application.BetRouter() {
if (betDataSource.updateBet(updatedBetData)) { if (betDataSource.updateBet(updatedBetData)) {
call.respond(HttpStatusCode.Accepted) call.respond(HttpStatusCode.Accepted)
} else { } else {
call.respond(HttpStatusCode.NotFound, ApiMessage.BetNotFound) call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
}
}
}
authenticate {
get("/bets/toConfirm") {
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
val response = betDataSource.getToConfirm(user.username).map {
val participations = participationDataSource.getParticipationFromBetId(it.id)
BetDetail(
it,
getBetAnswerDetail(it, participations),
participations.toList(),
participationDataSource.getParticipationFromUserId(user.username, it.id).lastOrNull()
)
}
call.respond(HttpStatusCode.Accepted, response)
}
}
}
}
authenticate {
get("/bets/getWon") {
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
call.respond(HttpStatusCode.Accepted, betDataSource.getWonNotifications(user.username))
}
}
}
}
authenticate {
get("/bets/history") {
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
call.respond(HttpStatusCode.Accepted, betDataSource.getHistory(user.username))
}
} }
} }
} }
@ -88,17 +126,30 @@ fun Application.BetRouter() {
get("/bets/current") { get("/bets/current") {
hasToken { principal -> hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ -> verifyUserFromToken(userDataSource, principal) { user, _ ->
val currentBets = betDataSource.getBetsNotFinished() call.respond(HttpStatusCode.Accepted, betDataSource.getCurrent(user.username))
.filter { bet -> }
val userParticipation = }
participationDataSource.getParticipationFromUserId(user.username, bet.id) }
userParticipation.isNotEmpty() }
}
authenticate {
call.respond(HttpStatusCode.OK, currentBets) post("/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) call.respond(HttpStatusCode.Created)
} else { } 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)) { if (participationDataSource.deleteParticipation(participationId)) {
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} else { } else {
call.respond(HttpStatusCode.NotFound, ApiMessage.ParticipationNotFound) call.respond(HttpStatusCode.NotFound, ApiMessage.PARTICIPATION_NOT_FOUND)
} }
} }
} }

@ -28,10 +28,10 @@ fun Application.UserRouter() {
post { post {
val tempUser = call.receive<UserRequest>() val tempUser = call.receive<UserRequest>()
if (RegexCheckerUser.isEmailInvalid(tempUser.email)) { 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)) { if (userDataSource.userExists(tempUser.username, tempUser.email)) {
call.respond(HttpStatusCode.Conflict, ApiMessage.UserAlreadyExist) call.respond(HttpStatusCode.Conflict, ApiMessage.USER_ALREADY_EXISTS)
} }
val user = User( val user = User(
@ -57,9 +57,9 @@ fun Application.UserRouter() {
user.first?.let { userDtoWithToken -> user.first?.let { userDtoWithToken ->
userDtoWithToken.token = tokenManagerUser.generateOrReplaceJWTToken(userDtoWithToken) userDtoWithToken.token = tokenManagerUser.generateOrReplaceJWTToken(userDtoWithToken)
call.respond(HttpStatusCode.OK, userDtoWithToken) call.respond(HttpStatusCode.OK, userDtoWithToken)
} ?: call.respond(HttpStatusCode.NotFound, ApiMessage.UserNotFound) } ?: call.respond(HttpStatusCode.NotFound, ApiMessage.USER_NOT_FOUND)
} else { } 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) call.respond(HttpStatusCode.Accepted, password)
} else { } 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() val dailyGift = getDailyGift()
userDataSource.addCoins(userDto.username, dailyGift) userDataSource.addCoins(userDto.username, dailyGift)
call.respond(HttpStatusCode.OK, 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 package allin.utils
class RegexChecker { class RegexChecker {
private val emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$"
private val emailRegex="^[A-Za-z0-9+_.-]+@(.+)$"
fun isEmailInvalid(email: String): Boolean { fun isEmailInvalid(email: String): Boolean {
val emailRegex = Regex(emailRegex) val emailRegex = Regex(emailRegex)
return !emailRegex.matches(email) return !emailRegex.matches(email)
} }
} }

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

@ -1,4 +1,4 @@
secret="secret" secret="secret"
issuer="http://0.0.0.0:8080/" issuer="http://0.0.0.0:8080/"
audience="http://0.0.0.0:8080/" audience="http://0.0.0.0:8080/"
realm="Access to main page" realm="allin"
Loading…
Cancel
Save