fix/friends (#14) [db]
continuous-integration/drone/push Build is passing Details

Co-authored-by: avalin <a.valin@openium.fr>
Reviewed-on: #14
pull/15/head
Arthur VALIN 11 months ago
parent de80aecc63
commit 8d8bccc7a3

@ -1,8 +1,19 @@
package allin.data package allin.data
import allin.dto.UserDTO
import allin.model.FriendStatus
interface FriendDataSource { interface FriendDataSource {
fun addFriend(sender: String, receiver: String) fun addFriend(sender: String, receiver: String)
fun getFriendFromUserId(id: String): List<String> fun getFriendFromUserId(id: String): List<UserDTO>
fun deleteFriend(senderId: String, receiverId: String): Boolean fun deleteFriend(senderId: String, receiverId: String): Boolean
fun isFriend(firstUser: String, secondUser: String): Boolean fun isFriend(firstUser: String, secondUser: String): Boolean
fun filterUsersByUsername(fromUserId: String, search: String): List<UserDTO>
fun getFriendStatus(firstUser: String, secondUser: String) =
if (isFriend(firstUser, secondUser)) {
if (isFriend(secondUser, firstUser)) {
FriendStatus.FRIEND
} else FriendStatus.REQUESTED
} else FriendStatus.NOT_FRIEND
} }

@ -1,11 +1,14 @@
package allin.data.mock package allin.data.mock
import allin.data.FriendDataSource import allin.data.FriendDataSource
import allin.dto.UserDTO
import allin.model.Friend import allin.model.Friend
import allin.model.FriendStatus
class MockFriendDataSource(private val mockData: MockDataSource.MockData) : FriendDataSource { class MockFriendDataSource(private val mockData: MockDataSource.MockData) : FriendDataSource {
private val friends get() = mockData.friends private val friends get() = mockData.friends
private val users get() = mockData.users
override fun addFriend(sender: String, receiver: String) { override fun addFriend(sender: String, receiver: String) {
mockData.friends.add(Friend(sender, receiver)) mockData.friends.add(Friend(sender, receiver))
@ -14,15 +17,26 @@ class MockFriendDataSource(private val mockData: MockDataSource.MockData) : Frie
override fun getFriendFromUserId(id: String) = override fun getFriendFromUserId(id: String) =
friends.map { Friend(sender = it.sender, receiver = it.receiver) } friends.map { Friend(sender = it.sender, receiver = it.receiver) }
.filter { it.sender == id } .filter { it.sender == id }
.map { it.receiver } .mapNotNull {
users.find { usr -> it.receiver == usr.id }
?.toDto(
friendStatus = if (isFriend(it.receiver, id)) {
FriendStatus.FRIEND
} else FriendStatus.REQUESTED
)
}
override fun deleteFriend(senderId: String, receiverId: String) = override fun deleteFriend(senderId: String, receiverId: String) =
friends.removeIf { (it.sender == senderId) && (it.receiver == receiverId) } friends.removeIf { (it.sender == senderId) && (it.receiver == receiverId) }
override fun isFriend(firstUser: String, secondUser: String) = override fun isFriend(firstUser: String, secondUser: String) =
friends friends.any { (it.sender == firstUser) and (it.receiver == secondUser) }
.filter { (it.sender == firstUser) and (it.receiver == secondUser) }
.map { Friend(sender = it.sender, receiver = it.receiver) }
.isNotEmpty() override fun filterUsersByUsername(fromUserId: String, search: String): List<UserDTO> =
users.filter { (it.username.contains(search, ignoreCase = true)) }
.map { user ->
user.toDto(friendStatus = getFriendStatus(fromUserId, user.id))
}
} }

@ -3,8 +3,6 @@ package allin.data.mock
import allin.data.UserDataSource import allin.data.UserDataSource
import allin.dto.UserDTO import allin.dto.UserDTO
import allin.model.User import allin.model.User
import org.ktorm.dsl.eq
import org.ktorm.dsl.or
import java.time.ZonedDateTime import java.time.ZonedDateTime
class MockUserDataSource(private val mockData: MockDataSource.MockData) : UserDataSource { class MockUserDataSource(private val mockData: MockDataSource.MockData) : UserDataSource {
@ -13,16 +11,7 @@ class MockUserDataSource(private val mockData: MockDataSource.MockData) : UserDa
override fun getUserByUsername(username: String): Pair<UserDTO?, String?> = override fun getUserByUsername(username: String): Pair<UserDTO?, String?> =
users.find { (it.username == username) or (it.email == username) }?.let { users.find { (it.username == username) or (it.email == username) }?.let {
Pair( it.toDto() to it.password
UserDTO(
id = it.id,
username = it.username,
email = it.email,
nbCoins = it.nbCoins,
token = it.token
),
it.password
)
} ?: Pair(null, null) } ?: Pair(null, null)
override fun addUser(user: User) { override fun addUser(user: User) {

@ -3,6 +3,7 @@ package allin.data.postgres
import allin.data.* import allin.data.*
import allin.ext.execute import allin.ext.execute
import org.ktorm.database.Database import org.ktorm.database.Database
import org.ktorm.support.postgresql.PostgreSqlDialect
class PostgresDataSource : AllInDataSource() { class PostgresDataSource : AllInDataSource() {
@ -20,7 +21,8 @@ class PostgresDataSource : AllInDataSource() {
database = Database.connect( database = Database.connect(
url = url, url = url,
user = dbUser, user = dbUser,
password = dbPassword password = dbPassword,
dialect = PostgreSqlDialect()
) )
database.execute( database.execute(

@ -3,13 +3,16 @@ package allin.data.postgres
import allin.data.FriendDataSource import allin.data.FriendDataSource
import allin.data.postgres.entities.FriendEntity import allin.data.postgres.entities.FriendEntity
import allin.data.postgres.entities.friends import allin.data.postgres.entities.friends
import allin.data.postgres.entities.users
import allin.dto.UserDTO
import allin.ext.toLowerCase
import allin.model.FriendStatus
import org.ktorm.database.Database import org.ktorm.database.Database
import org.ktorm.dsl.and import org.ktorm.dsl.and
import org.ktorm.dsl.eq import org.ktorm.dsl.eq
import org.ktorm.entity.add import org.ktorm.dsl.like
import org.ktorm.entity.filter import org.ktorm.dsl.notEq
import org.ktorm.entity.map import org.ktorm.entity.*
import org.ktorm.entity.removeIf
class PostgresFriendDataSource(private val database: Database) : FriendDataSource { class PostgresFriendDataSource(private val database: Database) : FriendDataSource {
override fun addFriend(sender: String, receiver: String) { override fun addFriend(sender: String, receiver: String) {
@ -24,17 +27,27 @@ class PostgresFriendDataSource(private val database: Database) : FriendDataSourc
override fun getFriendFromUserId(id: String) = override fun getFriendFromUserId(id: String) =
database.friends.map { it.toFriend() } database.friends.map { it.toFriend() }
.filter { it.sender == id } .filter { it.sender == id }
.map { it.receiver } .mapNotNull {
database.users.find { usr ->
usr.id eq it.receiver
}?.toUserDTO(
friendStatus = if (isFriend(it.receiver, id)) {
FriendStatus.FRIEND
} else FriendStatus.REQUESTED
)
}
override fun deleteFriend(senderId: String, receiverId: String): Boolean { override fun deleteFriend(senderId: String, receiverId: String): Boolean {
database.friends.removeIf { (it.sender eq senderId) and (it.receiver eq receiverId) } database.friends.removeIf { (it.sender eq receiverId) and (it.receiver eq senderId) }
return database.friends.removeIf { (it.sender eq senderId) and (it.receiver eq receiverId) } > 0 return database.friends.removeIf { (it.sender eq senderId) and (it.receiver eq receiverId) } > 0
} }
override fun isFriend(firstUser: String, secondUser: String) = override fun isFriend(firstUser: String, secondUser: String) =
database.friends database.friends.any { (it.sender eq firstUser) and (it.receiver eq secondUser) }
.filter { (it.sender eq firstUser) and (it.receiver eq secondUser) }
.map { it.toFriend() } override fun filterUsersByUsername(fromUserId: String, search: String): List<UserDTO> =
.isNotEmpty() database.users
.filter { (it.username.toLowerCase() like "%$search%") and (it.id notEq fromUserId) }
.map { user -> user.toUserDTO(friendStatus = getFriendStatus(fromUserId, user.id)) }
} }

@ -18,7 +18,6 @@ interface FriendEntity : Entity<FriendEntity> {
sender = sender, sender = sender,
receiver = receiver, receiver = receiver,
) )
} }
object FriendsEntity : Table<FriendEntity>("friend") { object FriendsEntity : Table<FriendEntity>("friend") {

@ -1,6 +1,7 @@
package allin.data.postgres.entities package allin.data.postgres.entities
import allin.dto.UserDTO import allin.dto.UserDTO
import allin.model.FriendStatus
import org.ktorm.database.Database import org.ktorm.database.Database
import org.ktorm.entity.Entity import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf import org.ktorm.entity.sequenceOf
@ -20,13 +21,14 @@ interface UserEntity : Entity<UserEntity> {
var nbCoins: Int var nbCoins: Int
var lastGift: Instant var lastGift: Instant
fun toUserDTO() = fun toUserDTO(friendStatus: FriendStatus? = null) =
UserDTO( UserDTO(
id = id, id = id,
username = username, username = username,
email = email, email = email,
nbCoins = nbCoins, nbCoins = nbCoins,
token = null token = null,
friendStatus = friendStatus
) )
} }

@ -1,5 +1,6 @@
package allin.dto package allin.dto
import allin.model.FriendStatus
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -8,5 +9,6 @@ data class UserDTO(
val username: String, val username: String,
val email: String, val email: String,
val nbCoins: Int, val nbCoins: Int,
var token: String? var token: String?,
val friendStatus: FriendStatus?
) )

@ -1,6 +1,9 @@
package allin.ext package allin.ext
import org.ktorm.database.Database import org.ktorm.database.Database
import org.ktorm.expression.FunctionExpression
import org.ktorm.schema.ColumnDeclaring
import org.ktorm.schema.VarcharSqlType
import java.sql.ResultSet import java.sql.ResultSet
fun Database.executeWithResult(request: String): ResultSet? { fun Database.executeWithResult(request: String): ResultSet? {
@ -27,3 +30,19 @@ fun Database.execute(request: String) {
connection.commit() connection.commit()
} }
} }
fun ColumnDeclaring<String>.toLowerCase(): FunctionExpression<String> {
return FunctionExpression(
functionName = "LOWER",
arguments = listOf(this.asExpression()),
sqlType = VarcharSqlType
)
}
fun ColumnDeclaring<String>.toUpperCase(): FunctionExpression<String> {
return FunctionExpression(
functionName = "UPPER",
arguments = listOf(this.asExpression()),
sqlType = VarcharSqlType
)
}

@ -0,0 +1,7 @@
package allin.model
enum class FriendStatus {
FRIEND,
REQUESTED,
NOT_FRIEND
}

@ -1,5 +1,6 @@
package allin.model package allin.model
import allin.dto.UserDTO
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
const val DEFAULT_COIN_AMOUNT = 500 const val DEFAULT_COIN_AMOUNT = 500
@ -14,7 +15,17 @@ data class User(
var password: String, var password: String,
var nbCoins: Int = DEFAULT_COIN_AMOUNT, var nbCoins: Int = DEFAULT_COIN_AMOUNT,
var token: String? = null var token: String? = null
) ) {
fun toDto(friendStatus: FriendStatus? = null) =
UserDTO(
id = id,
username = username,
email = email,
nbCoins = nbCoins,
token = token,
friendStatus = friendStatus
)
}
@Serializable @Serializable
data class UserRequest( data class UserRequest(

@ -1,14 +1,13 @@
package allin.routing package allin.routing
import allin.dataSource import allin.dataSource
import allin.dto.UserDTO
import allin.ext.hasToken import allin.ext.hasToken
import allin.ext.verifyUserFromToken import allin.ext.verifyUserFromToken
import allin.model.ApiMessage import allin.model.ApiMessage
import allin.model.Bet
import io.github.smiley4.ktorswaggerui.dsl.get import io.github.smiley4.ktorswaggerui.dsl.get
import io.github.smiley4.ktorswaggerui.dsl.post import io.github.smiley4.ktorswaggerui.dsl.post
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.*
import io.ktor.server.auth.jwt.* import io.ktor.server.auth.jwt.*
@ -32,7 +31,7 @@ fun Application.friendRouter() {
response { response {
HttpStatusCode.Accepted to { HttpStatusCode.Accepted to {
description = "The list of friends is available" description = "The list of friends is available"
body<List<Bet>> { body<List<UserDTO>> {
description = "List of friends" description = "List of friends"
} }
} }
@ -58,7 +57,7 @@ fun Application.friendRouter() {
response { response {
HttpStatusCode.Created to { HttpStatusCode.Created to {
description = "the friend has been added" description = "the friend has been added"
body<String>() { body<String> {
description = "Friend with assigned id" description = "Friend with assigned id"
} }
} }
@ -70,7 +69,10 @@ fun Application.friendRouter() {
}) { }) {
hasToken { principal -> hasToken { principal ->
val requestMap = call.receive<Map<String, String>>() val requestMap = call.receive<Map<String, String>>()
val usernameFriend = requestMap["username"] ?: return@hasToken call.respond(HttpStatusCode.BadRequest, "Username is missing") val usernameFriend = requestMap["username"] ?: return@hasToken call.respond(
HttpStatusCode.BadRequest,
"Username is missing"
)
val username = tokenManagerBet.getUsernameFromToken(principal) val username = tokenManagerBet.getUsernameFromToken(principal)
val user = userDataSource.getUserByUsername(username).first val user = userDataSource.getUserByUsername(username).first
@ -78,21 +80,18 @@ fun Application.friendRouter() {
if (user == null || userFriend == null) { if (user == null || userFriend == null) {
call.respond(HttpStatusCode.Conflict, ApiMessage.USER_NOT_FOUND) call.respond(HttpStatusCode.Conflict, ApiMessage.USER_NOT_FOUND)
} } else if (userFriend == user) {
else if (userFriend == user) { call.respond(HttpStatusCode.Conflict, ApiMessage.FRIENDS_REQUEST_HIMSELF)
call.respond(HttpStatusCode.Conflict,ApiMessage.FRIENDS_REQUEST_HIMSELF) } else {
}
else {
val friendlist = friendDataSource.getFriendFromUserId(user.id) val friendlist = friendDataSource.getFriendFromUserId(user.id)
if (friendlist.contains(userFriend.id)) { if (friendlist.map { it.id }.contains(userFriend.id)) {
call.respond(HttpStatusCode.Conflict,ApiMessage.FRIENDS_ALREADY_EXISTS) call.respond(HttpStatusCode.Conflict, ApiMessage.FRIENDS_ALREADY_EXISTS)
} else { } else {
friendDataSource.addFriend(user.id, userFriend.id) friendDataSource.addFriend(user.id, userFriend.id)
call.respond(HttpStatusCode.Created, usernameFriend) call.respond(HttpStatusCode.Created, usernameFriend)
} }
} }
} }
} }
post("/friends/delete", { post("/friends/delete", {
description = "Allows a user to delete a friend" description = "Allows a user to delete a friend"
@ -105,7 +104,7 @@ fun Application.friendRouter() {
response { response {
HttpStatusCode.Created to { HttpStatusCode.Created to {
description = "the friend has been delete" description = "the friend has been delete"
body<String>() { body<String> {
description = "Friend with assigned id" description = "Friend with assigned id"
} }
} }
@ -117,7 +116,10 @@ fun Application.friendRouter() {
}) { }) {
hasToken { principal -> hasToken { principal ->
val requestMap = call.receive<Map<String, String>>() val requestMap = call.receive<Map<String, String>>()
val usernameFriend = requestMap["username"] ?: return@hasToken call.respond(HttpStatusCode.BadRequest, "Username is missing") val usernameFriend = requestMap["username"] ?: return@hasToken call.respond(
HttpStatusCode.BadRequest,
"Username is missing"
)
val username = tokenManagerBet.getUsernameFromToken(principal) val username = tokenManagerBet.getUsernameFromToken(principal)
val user = userDataSource.getUserByUsername(username).first val user = userDataSource.getUserByUsername(username).first
@ -127,8 +129,8 @@ fun Application.friendRouter() {
call.respond(HttpStatusCode.Conflict, ApiMessage.USER_NOT_FOUND) call.respond(HttpStatusCode.Conflict, ApiMessage.USER_NOT_FOUND)
} else { } else {
val friendlist = friendDataSource.getFriendFromUserId(user.id) val friendlist = friendDataSource.getFriendFromUserId(user.id)
if (!friendlist.contains(userFriend.id)) { if (!friendlist.map { it.id }.contains(userFriend.id)) {
call.respond(HttpStatusCode.Conflict,ApiMessage.FRIENDS_DOESNT_EXISTS) call.respond(HttpStatusCode.Conflict, ApiMessage.FRIENDS_DOESNT_EXISTS)
} else { } else {
friendDataSource.deleteFriend(user.id, userFriend.id) friendDataSource.deleteFriend(user.id, userFriend.id)
call.respond(HttpStatusCode.Created, usernameFriend) call.respond(HttpStatusCode.Created, usernameFriend)
@ -138,6 +140,33 @@ fun Application.friendRouter() {
} }
get("/friends/search/{search}", {
description = "Search for users based on username"
request {
headerParameter<JWTPrincipal>("JWT token of the logged user")
pathParameter<String>("Search string")
}
response {
HttpStatusCode.OK to {
body<List<UserDTO>> {
description = "Filtered users."
}
}
}
}) {
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { userDto, _ ->
val users = friendDataSource.filterUsersByUsername(
fromUserId = userDto.id,
search = call.parameters["search"] ?: ""
)
call.respond(HttpStatusCode.OK, users)
}
}
}
} }
} }
} }

@ -55,11 +55,9 @@ fun Application.userRouter() {
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.INVALID_MAIL) call.respond(HttpStatusCode.Forbidden, ApiMessage.INVALID_MAIL)
} } else if (userDataSource.userExists(tempUser.username)) {
else if (userDataSource.userExists(tempUser.username)) {
call.respond(HttpStatusCode.Conflict, ApiMessage.USER_ALREADY_EXISTS) call.respond(HttpStatusCode.Conflict, ApiMessage.USER_ALREADY_EXISTS)
} } else if (userDataSource.emailExists(tempUser.email)) {
else if (userDataSource.emailExists(tempUser.email)) {
call.respond(HttpStatusCode.Conflict, ApiMessage.MAIL_ALREADY_EXISTS) call.respond(HttpStatusCode.Conflict, ApiMessage.MAIL_ALREADY_EXISTS)
} else { } else {
val user = User( val user = User(

Loading…
Cancel
Save