fix/friends #14

Merged
arthur.valin merged 3 commits from fix/friends into master 11 months ago

@ -1,8 +1,19 @@
package allin.data
import allin.dto.UserDTO
import allin.model.FriendStatus
interface FriendDataSource {
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 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
import allin.data.FriendDataSource
import allin.dto.UserDTO
import allin.model.Friend
import allin.model.FriendStatus
class MockFriendDataSource(private val mockData: MockDataSource.MockData) : FriendDataSource {
private val friends get() = mockData.friends
private val users get() = mockData.users
override fun addFriend(sender: String, receiver: String) {
mockData.friends.add(Friend(sender, receiver))
@ -14,15 +17,26 @@ class MockFriendDataSource(private val mockData: MockDataSource.MockData) : Frie
override fun getFriendFromUserId(id: String) =
friends.map { Friend(sender = it.sender, receiver = it.receiver) }
.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) =
friends.removeIf { (it.sender == senderId) && (it.receiver == receiverId) }
override fun isFriend(firstUser: String, secondUser: String) =
friends
.filter { (it.sender == firstUser) and (it.receiver == secondUser) }
.map { Friend(sender = it.sender, receiver = it.receiver) }
.isNotEmpty()
friends.any { (it.sender == firstUser) and (it.receiver == secondUser) }
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.dto.UserDTO
import allin.model.User
import org.ktorm.dsl.eq
import org.ktorm.dsl.or
import java.time.ZonedDateTime
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?> =
users.find { (it.username == username) or (it.email == username) }?.let {
Pair(
UserDTO(
id = it.id,
username = it.username,
email = it.email,
nbCoins = it.nbCoins,
token = it.token
),
it.password
)
it.toDto() to it.password
} ?: Pair(null, null)
override fun addUser(user: User) {

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

@ -3,13 +3,16 @@ package allin.data.postgres
import allin.data.FriendDataSource
import allin.data.postgres.entities.FriendEntity
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.dsl.and
import org.ktorm.dsl.eq
import org.ktorm.entity.add
import org.ktorm.entity.filter
import org.ktorm.entity.map
import org.ktorm.entity.removeIf
import org.ktorm.dsl.like
import org.ktorm.dsl.notEq
import org.ktorm.entity.*
class PostgresFriendDataSource(private val database: Database) : FriendDataSource {
override fun addFriend(sender: String, receiver: String) {
@ -24,17 +27,27 @@ class PostgresFriendDataSource(private val database: Database) : FriendDataSourc
override fun getFriendFromUserId(id: String) =
database.friends.map { it.toFriend() }
.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 {
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
}
override fun isFriend(firstUser: String, secondUser: String) =
database.friends
.filter { (it.sender eq firstUser) and (it.receiver eq secondUser) }
.map { it.toFriend() }
.isNotEmpty()
database.friends.any { (it.sender eq firstUser) and (it.receiver eq secondUser) }
override fun filterUsersByUsername(fromUserId: String, search: String): List<UserDTO> =
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,
receiver = receiver,
)
}
object FriendsEntity : Table<FriendEntity>("friend") {
@ -26,4 +25,4 @@ object FriendsEntity : Table<FriendEntity>("friend") {
val receiver = varchar("receiver").primaryKey().bindTo { it.receiver }
}
val Database.friends get() = this.sequenceOf(FriendsEntity)
val Database.friends get() = this.sequenceOf(FriendsEntity)

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

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

@ -1,6 +1,9 @@
package allin.ext
import org.ktorm.database.Database
import org.ktorm.expression.FunctionExpression
import org.ktorm.schema.ColumnDeclaring
import org.ktorm.schema.VarcharSqlType
import java.sql.ResultSet
fun Database.executeWithResult(request: String): ResultSet? {
@ -26,4 +29,20 @@ fun Database.execute(request: String) {
connection.prepareStatement(request).execute()
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
import allin.dto.UserDTO
import kotlinx.serialization.Serializable
const val DEFAULT_COIN_AMOUNT = 500
@ -14,7 +15,17 @@ data class User(
var password: String,
var nbCoins: Int = DEFAULT_COIN_AMOUNT,
var token: String? = null
)
) {
fun toDto(friendStatus: FriendStatus? = null) =
UserDTO(
id = id,
username = username,
email = email,
nbCoins = nbCoins,
token = token,
friendStatus = friendStatus
)
}
@Serializable
data class UserRequest(

@ -1,14 +1,13 @@
package allin.routing
import allin.dataSource
import allin.dto.UserDTO
import allin.ext.hasToken
import allin.ext.verifyUserFromToken
import allin.model.ApiMessage
import allin.model.Bet
import io.github.smiley4.ktorswaggerui.dsl.get
import io.github.smiley4.ktorswaggerui.dsl.post
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
@ -32,7 +31,7 @@ fun Application.friendRouter() {
response {
HttpStatusCode.Accepted to {
description = "The list of friends is available"
body<List<Bet>> {
body<List<UserDTO>> {
description = "List of friends"
}
}
@ -47,53 +46,53 @@ fun Application.friendRouter() {
}
}
post("/friends/add", {
description = "Allows a user to add a friend"
request {
headerParameter<JWTPrincipal>("JWT token of the logged user")
body<String> {
description = "User to add in the friends list"
}
}
response {
HttpStatusCode.Created to {
description = "the friend has been added"
body<String>() {
description = "Friend with assigned id"
post("/friends/add", {
description = "Allows a user to add a friend"
request {
headerParameter<JWTPrincipal>("JWT token of the logged user")
body<String> {
description = "User to add in the friends list"
}
}
HttpStatusCode.Conflict to {
description = "Friend already exist in the friends list"
body(ApiMessage.FRIENDS_ALREADY_EXISTS)
response {
HttpStatusCode.Created to {
description = "the friend has been added"
body<String> {
description = "Friend with assigned id"
}
}
HttpStatusCode.Conflict to {
description = "Friend already exist in the friends list"
body(ApiMessage.FRIENDS_ALREADY_EXISTS)
}
}
}
}) {
hasToken { principal ->
val requestMap = call.receive<Map<String, String>>()
val usernameFriend = requestMap["username"] ?: return@hasToken call.respond(HttpStatusCode.BadRequest, "Username is missing")
val username = tokenManagerBet.getUsernameFromToken(principal)
}) {
hasToken { principal ->
val requestMap = call.receive<Map<String, String>>()
val usernameFriend = requestMap["username"] ?: return@hasToken call.respond(
HttpStatusCode.BadRequest,
"Username is missing"
)
val username = tokenManagerBet.getUsernameFromToken(principal)
val user = userDataSource.getUserByUsername(username).first
val userFriend = userDataSource.getUserByUsername(usernameFriend).first
val user = userDataSource.getUserByUsername(username).first
val userFriend = userDataSource.getUserByUsername(usernameFriend).first
if (user == null || userFriend == null) {
call.respond(HttpStatusCode.Conflict, ApiMessage.USER_NOT_FOUND)
}
else if (userFriend == user) {
call.respond(HttpStatusCode.Conflict,ApiMessage.FRIENDS_REQUEST_HIMSELF)
}
else {
val friendlist = friendDataSource.getFriendFromUserId(user.id)
if (friendlist.contains(userFriend.id)) {
call.respond(HttpStatusCode.Conflict,ApiMessage.FRIENDS_ALREADY_EXISTS)
if (user == null || userFriend == null) {
call.respond(HttpStatusCode.Conflict, ApiMessage.USER_NOT_FOUND)
} else if (userFriend == user) {
call.respond(HttpStatusCode.Conflict, ApiMessage.FRIENDS_REQUEST_HIMSELF)
} else {
friendDataSource.addFriend(user.id, userFriend.id)
call.respond(HttpStatusCode.Created, usernameFriend)
val friendlist = friendDataSource.getFriendFromUserId(user.id)
if (friendlist.map { it.id }.contains(userFriend.id)) {
call.respond(HttpStatusCode.Conflict, ApiMessage.FRIENDS_ALREADY_EXISTS)
} else {
friendDataSource.addFriend(user.id, userFriend.id)
call.respond(HttpStatusCode.Created, usernameFriend)
}
}
}
}
}
post("/friends/delete", {
description = "Allows a user to delete a friend"
request {
@ -105,7 +104,7 @@ fun Application.friendRouter() {
response {
HttpStatusCode.Created to {
description = "the friend has been delete"
body<String>() {
body<String> {
description = "Friend with assigned id"
}
}
@ -117,7 +116,10 @@ fun Application.friendRouter() {
}) {
hasToken { principal ->
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 user = userDataSource.getUserByUsername(username).first
@ -127,8 +129,8 @@ fun Application.friendRouter() {
call.respond(HttpStatusCode.Conflict, ApiMessage.USER_NOT_FOUND)
} else {
val friendlist = friendDataSource.getFriendFromUserId(user.id)
if (!friendlist.contains(userFriend.id)) {
call.respond(HttpStatusCode.Conflict,ApiMessage.FRIENDS_DOESNT_EXISTS)
if (!friendlist.map { it.id }.contains(userFriend.id)) {
call.respond(HttpStatusCode.Conflict, ApiMessage.FRIENDS_DOESNT_EXISTS)
} else {
friendDataSource.deleteFriend(user.id, userFriend.id)
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>()
if (RegexCheckerUser.isEmailInvalid(tempUser.email)) {
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)
}
else if (userDataSource.emailExists(tempUser.email)) {
} else if (userDataSource.emailExists(tempUser.email)) {
call.respond(HttpStatusCode.Conflict, ApiMessage.MAIL_ALREADY_EXISTS)
} else {
val user = User(

Loading…
Cancel
Save