diff --git a/Sources/src/main/kotlin/allin/Application.kt b/Sources/src/main/kotlin/allin/Application.kt index 46ca90c..4f2c22e 100644 --- a/Sources/src/main/kotlin/allin/Application.kt +++ b/Sources/src/main/kotlin/allin/Application.kt @@ -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 } diff --git a/Sources/src/main/kotlin/allin/data/AllInDataSource.kt b/Sources/src/main/kotlin/allin/data/AllInDataSource.kt index f2c1406..a0861bb 100644 --- a/Sources/src/main/kotlin/allin/data/AllInDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/AllInDataSource.kt @@ -1,12 +1,7 @@ package allin.data abstract class AllInDataSource { - abstract val userDataSource: UserDataSource - - abstract val betDataSource: BetDataSource - - abstract val participationDataSource: ParticipationDataSource } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/BetDataSource.kt b/Sources/src/main/kotlin/allin/data/BetDataSource.kt index cb543e0..19a4d3d 100644 --- a/Sources/src/main/kotlin/allin/data/BetDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/BetDataSource.kt @@ -1,6 +1,8 @@ package allin.data import allin.model.Bet +import allin.model.BetDetail +import allin.model.BetResultDetail import allin.model.UpdatedBetData import java.time.ZonedDateTime @@ -12,4 +14,9 @@ interface BetDataSource { fun removeBet(id: String): Boolean fun updateBet(data: UpdatedBetData): Boolean fun updateBetStatuses(date: ZonedDateTime) + fun getToConfirm(username: String): List + fun confirmBet(betId: String, result: String) + fun getWonNotifications(username: String): List + fun getHistory(username: String): List + fun getCurrent(username: String): List } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/ParticipationDataSource.kt b/Sources/src/main/kotlin/allin/data/ParticipationDataSource.kt index d0dfb09..1e6b3d8 100644 --- a/Sources/src/main/kotlin/allin/data/ParticipationDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/ParticipationDataSource.kt @@ -3,16 +3,8 @@ package allin.data import allin.model.Participation interface ParticipationDataSource { - - fun addParticipation(participation: Participation) - - fun getParticipationFromBetId(betid: String): List - - fun getParticipationFromUserId(username: String, betid: String): List - - fun deleteParticipation(id: String): Boolean } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/UserDataSource.kt b/Sources/src/main/kotlin/allin/data/UserDataSource.kt index 48680df..da98808 100644 --- a/Sources/src/main/kotlin/allin/data/UserDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/UserDataSource.kt @@ -6,18 +6,10 @@ import allin.model.User interface UserDataSource { fun getUserByUsername(username: String): Pair - - 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 } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/mock/MockBetDataSource.kt b/Sources/src/main/kotlin/allin/data/mock/MockBetDataSource.kt index a2d04a0..ca8e046 100644 --- a/Sources/src/main/kotlin/allin/data/mock/MockBetDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/mock/MockBetDataSource.kt @@ -1,12 +1,16 @@ package allin.data.mock import allin.data.BetDataSource -import allin.model.Bet -import allin.model.BetStatus -import allin.model.UpdatedBetData +import allin.model.* +import allin.model.BetStatus.* 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 = bets override fun getBetById(id: String): Bet? = bets.find { it.id == id } @@ -31,14 +35,91 @@ class MockBetDataSource : BetDataSource { bets.forEachIndexed { idx, bet -> if (date >= bet.endRegistration) { if (date >= bet.endBet) { - bets[idx] = bet.copy(status = BetStatus.WAITING) + bets[idx] = bet.copy(status = WAITING) } else { - bets[idx] = bet.copy(status = BetStatus.CLOSING) + bets[idx] = bet.copy(status = CLOSING) } } } } - private val bets by lazy { mutableListOf() } + override fun getToConfirm(username: String): List = + 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 { + 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 { + 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 { + 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 } -} \ No newline at end of file + + BetDetail( + bet = bet, + answers = getBetAnswerDetail(bet, participations), + participations = participations, + userParticipation = participation + ) + } + } + }.mapNotNull { it } + } +} diff --git a/Sources/src/main/kotlin/allin/data/mock/MockDataSource.kt b/Sources/src/main/kotlin/allin/data/mock/MockDataSource.kt index 1cd0b9d..998f05c 100644 --- a/Sources/src/main/kotlin/allin/data/mock/MockDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/mock/MockDataSource.kt @@ -4,9 +4,26 @@ import allin.data.AllInDataSource import allin.data.BetDataSource import allin.data.ParticipationDataSource 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() { - override val userDataSource: UserDataSource = MockUserDataSource() - override val betDataSource: BetDataSource = MockBetDataSource() - override val participationDataSource: ParticipationDataSource = MockParticipationDataSource() + + class MockData { + val bets by lazy { mutableListOf() } + val results by lazy { mutableListOf() } + val resultNotifications by lazy { mutableListOf>() } + val users by lazy { mutableListOf() } + val lastGifts by lazy { mutableMapOf() } + val participations by lazy { mutableListOf() } + } + + private val mockData by lazy { MockData() } + + override val userDataSource: UserDataSource = MockUserDataSource(mockData) + override val betDataSource: BetDataSource = MockBetDataSource(mockData) + override val participationDataSource: ParticipationDataSource = MockParticipationDataSource(mockData) } diff --git a/Sources/src/main/kotlin/allin/data/mock/MockParticipationDataSource.kt b/Sources/src/main/kotlin/allin/data/mock/MockParticipationDataSource.kt index c9f22af..39fc209 100644 --- a/Sources/src/main/kotlin/allin/data/mock/MockParticipationDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/mock/MockParticipationDataSource.kt @@ -3,7 +3,9 @@ package allin.data.mock import allin.data.ParticipationDataSource import allin.model.Participation -class MockParticipationDataSource : ParticipationDataSource { +class MockParticipationDataSource(mockData: MockDataSource.MockData) : ParticipationDataSource { + private val participations = mockData.participations + override fun addParticipation(participation: Participation) { participations += participations } @@ -16,7 +18,4 @@ class MockParticipationDataSource : ParticipationDataSource { override fun deleteParticipation(id: String): Boolean = participations.removeIf { it.id == id } - - private val participations by lazy { mutableListOf() } - } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/mock/MockUserDataSource.kt b/Sources/src/main/kotlin/allin/data/mock/MockUserDataSource.kt index b6e7a2b..42883b9 100644 --- a/Sources/src/main/kotlin/allin/data/mock/MockUserDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/mock/MockUserDataSource.kt @@ -5,7 +5,11 @@ import allin.dto.UserDTO import allin.model.User 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 = users.find { it.username == username }?.let { Pair( @@ -49,12 +53,4 @@ class MockUserDataSource : UserDataSource { lastGifts[username] = ZonedDateTime.now() return value } - - private val users by lazy { - mutableListOf() - } - - private val lastGifts by lazy { - mutableMapOf() - } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresBetDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresBetDataSource.kt index 4c50b09..5bc55f0 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresBetDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresBetDataSource.kt @@ -1,15 +1,9 @@ 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.model.Bet -import allin.model.BetStatus -import allin.model.BetType -import allin.model.UpdatedBetData +import allin.data.postgres.entities.* +import allin.data.postgres.entities.ResponsesEntity.response +import allin.model.* import org.ktorm.database.Database import org.ktorm.dsl.* 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.mapToBetResultDetail() = this.map { it.toBetResultDetail() } override fun getAllBets(): List = database.from(BetsEntity).select().mapToBet() @@ -61,6 +77,94 @@ class PostgresBetDataSource(private val database: Database) : BetDataSource { .mapToBet() } + override fun getToConfirm(username: String): List { + 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 { + 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 { + 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 { + 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) { database.insert(BetsEntity) { set(it.id, UUID.fromString(bet.id)) diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt index 02e18ad..32adc22 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt @@ -32,17 +32,12 @@ class PostgresDataSource : AllInDataSource() { coins double precision, email VARCHAR(255), lastgift timestamp - )""".trimIndent() + ) + """.trimIndent() ) 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 ( id uuid PRIMARY KEY, theme VARCHAR(255), @@ -53,7 +48,22 @@ 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) + ) + CREATE TABLE IF NOT EXISTS betresultnotification ( + betid uuid, + username varchar(250), + CONSTRAINT pk_id_username PRIMARY KEY (betid, username) + ) + """.trimIndent() ) database.Execute( @@ -64,7 +74,8 @@ class PostgresDataSource : AllInDataSource() { username varchar(250), answer varchar(250), stake int - )""".trimIndent() + ) + """.trimIndent() ) database.Execute( @@ -72,8 +83,9 @@ class PostgresDataSource : AllInDataSource() { CREATE TABLE IF NOT EXISTS response ( id UUID, response VARCHAR(250), - CONSTRAINT pk_response_id PRIMARY KEY (id,response) - )""".trimIndent() + CONSTRAINT pk_response_id PRIMARY KEY (id, response) + ) + """.trimIndent() ) } diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresParticipationDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresParticipationDataSource.kt index ad50c6e..bb14527 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresParticipationDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresParticipationDataSource.kt @@ -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 { - return database.from(ParticipationsEntity) + override fun getParticipationFromBetId(betid: String): List = + database.from(ParticipationsEntity) .select() .where { ParticipationsEntity.betId eq UUID.fromString(betid) } .mapToParticipation() - } - override fun getParticipationFromUserId(username: String, betid: String): List { - return database.from(ParticipationsEntity) + override fun getParticipationFromUserId(username: String, betid: String): List = + database.from(ParticipationsEntity) .select() .where { (ParticipationsEntity.betId eq UUID.fromString(betid)) and (ParticipationsEntity.username eq username) } .mapToParticipation() - } - fun getParticipationEntity(): List { - return database.from(ParticipationsEntity).select().mapToParticipation() - } + fun getParticipationEntity(): List = + 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 - } - } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresUserDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresUserDataSource.kt index e3bb91e..ba499a9 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresUserDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresUserDataSource.kt @@ -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 diff --git a/Sources/src/main/kotlin/allin/entities/BetEntity.kt b/Sources/src/main/kotlin/allin/data/postgres/entities/BetEntity.kt similarity index 58% rename from Sources/src/main/kotlin/allin/entities/BetEntity.kt rename to Sources/src/main/kotlin/allin/data/postgres/entities/BetEntity.kt index 0830ab8..6c82b1e 100644 --- a/Sources/src/main/kotlin/allin/entities/BetEntity.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/entities/BetEntity.kt @@ -1,10 +1,9 @@ -package allin.entities +package allin.data.postgres.entities import allin.model.BetStatus import allin.model.BetType import org.ktorm.entity.Entity import org.ktorm.schema.* -import org.ktorm.support.postgresql.pgEnum import java.time.ZonedDateTime @@ -21,12 +20,12 @@ interface BetEntity : Entity { object BetsEntity : Table("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 status = pgEnum("status").bindTo { it.status } - val type = pgEnum("type").bindTo { it.type } - val createdBy = varchar("createdby") + val isPrivate = boolean("isprivate").bindTo { it.isPrivate } + val status = enum("status").bindTo { it.status } + val type = enum("type").bindTo { it.type } + val createdBy = varchar("createdby").bindTo { it.createdBy } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/postgres/entities/BetResultEntity.kt b/Sources/src/main/kotlin/allin/data/postgres/entities/BetResultEntity.kt new file mode 100644 index 0000000..cbaafc3 --- /dev/null +++ b/Sources/src/main/kotlin/allin/data/postgres/entities/BetResultEntity.kt @@ -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 { + val bet: BetEntity + val result: String +} + +object BetResultsEntity : Table("betresult") { + val betId = uuid("betid").primaryKey().references(BetsEntity) { it.bet } + val result = varchar("result").bindTo { it.result } +} + +interface BetResultNotificationEntity : Entity { + val betId: UUID + val username: String +} + +object BetResultNotificationsEntity : Table("betresult") { + val betId = uuid("betid").primaryKey() + val username = varchar("username").primaryKey() +} \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/entities/ParticipationEntity.kt b/Sources/src/main/kotlin/allin/data/postgres/entities/ParticipationEntity.kt similarity index 69% rename from Sources/src/main/kotlin/allin/entities/ParticipationEntity.kt rename to Sources/src/main/kotlin/allin/data/postgres/entities/ParticipationEntity.kt index 231d620..6793c0c 100644 --- a/Sources/src/main/kotlin/allin/entities/ParticipationEntity.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/entities/ParticipationEntity.kt @@ -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 { val id: String - val betId: String + val bet: BetEntity val username: String val answer: String val stake: Int } -object ParticipationsEntity : Table("participation") { +object ParticipationsEntity : Table("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") diff --git a/Sources/src/main/kotlin/allin/entities/ResponseEntity.kt b/Sources/src/main/kotlin/allin/data/postgres/entities/ResponseEntity.kt similarity index 92% rename from Sources/src/main/kotlin/allin/entities/ResponseEntity.kt rename to Sources/src/main/kotlin/allin/data/postgres/entities/ResponseEntity.kt index 5da1b16..4a826ef 100644 --- a/Sources/src/main/kotlin/allin/entities/ResponseEntity.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/entities/ResponseEntity.kt @@ -1,4 +1,4 @@ -package allin.entities +package allin.data.postgres.entities import org.ktorm.entity.Entity import org.ktorm.schema.Table diff --git a/Sources/src/main/kotlin/allin/entities/UserEntity.kt b/Sources/src/main/kotlin/allin/data/postgres/entities/UserEntity.kt similarity index 55% rename from Sources/src/main/kotlin/allin/entities/UserEntity.kt rename to Sources/src/main/kotlin/allin/data/postgres/entities/UserEntity.kt index 6009cdc..62c5f26 100644 --- a/Sources/src/main/kotlin/allin/entities/UserEntity.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/entities/UserEntity.kt @@ -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 { object UsersEntity : Table("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") } diff --git a/Sources/src/main/kotlin/allin/dto/UserDTO.kt b/Sources/src/main/kotlin/allin/dto/UserDTO.kt index 2ed2a42..c34686f 100644 --- a/Sources/src/main/kotlin/allin/dto/UserDTO.kt +++ b/Sources/src/main/kotlin/allin/dto/UserDTO.kt @@ -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? +) diff --git a/Sources/src/main/kotlin/allin/ext/PipelineContextExt.kt b/Sources/src/main/kotlin/allin/ext/PipelineContextExt.kt index 0d5db8c..3328e43 100644 --- a/Sources/src/main/kotlin/allin/ext/PipelineContextExt.kt +++ b/Sources/src/main/kotlin/allin/ext/PipelineContextExt.kt @@ -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()?.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) } \ 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 index 9f47ea3..eaaf6ca 100644 --- a/Sources/src/main/kotlin/allin/model/ApiMessage.kt +++ b/Sources/src/main/kotlin/allin/model/ApiMessage.kt @@ -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." } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/model/BetResult.kt b/Sources/src/main/kotlin/allin/model/BetResult.kt new file mode 100644 index 0000000..b1f5335 --- /dev/null +++ b/Sources/src/main/kotlin/allin/model/BetResult.kt @@ -0,0 +1,9 @@ +package allin.model + +import kotlinx.serialization.Serializable + +@Serializable +data class BetResult( + val betId: String, + val result: String +) \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/model/BetResultDetail.kt b/Sources/src/main/kotlin/allin/model/BetResultDetail.kt new file mode 100644 index 0000000..82fdb6a --- /dev/null +++ b/Sources/src/main/kotlin/allin/model/BetResultDetail.kt @@ -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 +) \ 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 7bebf88..1824916 100644 --- a/Sources/src/main/kotlin/allin/routing/BasicRouting.kt +++ b/Sources/src/main/kotlin/allin/routing/BasicRouting.kt @@ -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) } } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/routing/BetDetailRouter.kt b/Sources/src/main/kotlin/allin/routing/BetDetailRouter.kt index a8d9243..6654765 100644 --- a/Sources/src/main/kotlin/allin/routing/BetDetailRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/BetDetailRouter.kt @@ -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) } } } diff --git a/Sources/src/main/kotlin/allin/routing/BetRouter.kt b/Sources/src/main/kotlin/allin/routing/BetRouter.kt index 26d2206..f0b70ed 100644 --- a/Sources/src/main/kotlin/allin/routing/BetRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/BetRouter.kt @@ -3,9 +3,7 @@ package allin.routing import allin.dataSource import allin.ext.hasToken import allin.ext.verifyUserFromToken -import allin.model.ApiMessage -import allin.model.Bet -import allin.model.UpdatedBetData +import allin.model.* import allin.utils.AppConfig import io.ktor.http.* import io.ktor.server.application.* @@ -32,7 +30,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 +42,13 @@ 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, _ -> + call.respond(HttpStatusCode.Accepted, betDataSource.getAllBets()) + } + } } } @@ -58,7 +57,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 +67,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 +78,46 @@ 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) + } + } + } + + 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") { hasToken { principal -> verifyUserFromToken(userDataSource, principal) { user, _ -> - val currentBets = betDataSource.getBetsNotFinished() - .filter { bet -> - val userParticipation = - participationDataSource.getParticipationFromUserId(user.username, bet.id) - userParticipation.isNotEmpty() - } - - call.respond(HttpStatusCode.OK, currentBets) + call.respond(HttpStatusCode.Accepted, betDataSource.getCurrent(user.username)) + } + } + } + } + + authenticate { + post("/bets/confirm/{id}") { + hasToken { principal -> + verifyUserFromToken(userDataSource, principal) { user, _ -> + val betId = call.parameters["id"] ?: "" + val result = call.receive() + + if (betDataSource.getBetById(betId)?.createdBy == user.username) { + betDataSource.confirmBet(betId, result) + call.respond(HttpStatusCode.OK) + } else { + call.respond(HttpStatusCode.Unauthorized) + } + } } } } + } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt b/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt index e96d19e..3995a88 100644 --- a/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt @@ -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) } } } diff --git a/Sources/src/main/kotlin/allin/routing/UserRouter.kt b/Sources/src/main/kotlin/allin/routing/UserRouter.kt index fd7172d..746d825 100644 --- a/Sources/src/main/kotlin/allin/routing/UserRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/UserRouter.kt @@ -28,10 +28,10 @@ fun Application.UserRouter() { post { val tempUser = call.receive() 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) } } } diff --git a/Sources/src/main/kotlin/allin/utils/RegexChecker.kt b/Sources/src/main/kotlin/allin/utils/RegexChecker.kt index 8a70a90..6ed7248 100644 --- a/Sources/src/main/kotlin/allin/utils/RegexChecker.kt +++ b/Sources/src/main/kotlin/allin/utils/RegexChecker.kt @@ -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) } - } \ 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 0034ad8..39053cc 100644 --- a/Sources/src/main/kotlin/allin/utils/TokenManager.kt +++ b/Sources/src/main/kotlin/allin/utils/TokenManager.kt @@ -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) { diff --git a/Sources/src/main/resources/application.conf b/Sources/src/main/resources/application.conf index 1b4a12c..1613b79 100644 --- a/Sources/src/main/resources/application.conf +++ b/Sources/src/main/resources/application.conf @@ -1,4 +1,4 @@ secret="secret" issuer="http://0.0.0.0:8080/" audience="http://0.0.0.0:8080/" -realm="Access to main page" \ No newline at end of file +realm="allin" \ No newline at end of file