diff --git a/Sources/src/main/kotlin/allin/data/FriendDataSource.kt b/Sources/src/main/kotlin/allin/data/FriendDataSource.kt index bb72df0..5436116 100644 --- a/Sources/src/main/kotlin/allin/data/FriendDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/FriendDataSource.kt @@ -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 + fun getFriendFromUserId(id: String): List fun deleteFriend(senderId: String, receiverId: String): Boolean fun isFriend(firstUser: String, secondUser: String): Boolean + fun filterUsersByUsername(fromUserId: String, search: String): List + + fun getFriendStatus(firstUser: String, secondUser: String) = + if (isFriend(firstUser, secondUser)) { + if (isFriend(secondUser, firstUser)) { + FriendStatus.FRIEND + } else FriendStatus.REQUESTED + } else FriendStatus.NOT_FRIEND } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/mock/MockFriendDataSource.kt b/Sources/src/main/kotlin/allin/data/mock/MockFriendDataSource.kt index f8f0bac..40c2122 100644 --- a/Sources/src/main/kotlin/allin/data/mock/MockFriendDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/mock/MockFriendDataSource.kt @@ -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 = + users.filter { (it.username.contains(search, ignoreCase = true)) } + .map { user -> + user.toDto(friendStatus = getFriendStatus(fromUserId, user.id)) + } } \ 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 454357f..047aa06 100644 --- a/Sources/src/main/kotlin/allin/data/mock/MockUserDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/mock/MockUserDataSource.kt @@ -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 = 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) { diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt index 41b075b..705750d 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt @@ -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( diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresFriendDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresFriendDataSource.kt index 130205a..ce78345 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresFriendDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresFriendDataSource.kt @@ -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 = + database.users + .filter { (it.username.toLowerCase() like "%$search%") and (it.id notEq fromUserId) } + .map { user -> user.toUserDTO(friendStatus = getFriendStatus(fromUserId, user.id)) } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/postgres/entities/FriendEntity.kt b/Sources/src/main/kotlin/allin/data/postgres/entities/FriendEntity.kt index 5ef4920..6cb232c 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/entities/FriendEntity.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/entities/FriendEntity.kt @@ -18,7 +18,6 @@ interface FriendEntity : Entity { sender = sender, receiver = receiver, ) - } object FriendsEntity : Table("friend") { @@ -26,4 +25,4 @@ object FriendsEntity : Table("friend") { val receiver = varchar("receiver").primaryKey().bindTo { it.receiver } } -val Database.friends get() = this.sequenceOf(FriendsEntity) +val Database.friends get() = this.sequenceOf(FriendsEntity) \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/postgres/entities/UserEntity.kt b/Sources/src/main/kotlin/allin/data/postgres/entities/UserEntity.kt index 463b91e..bf1defb 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/entities/UserEntity.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/entities/UserEntity.kt @@ -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 { 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 ) } diff --git a/Sources/src/main/kotlin/allin/dto/UserDTO.kt b/Sources/src/main/kotlin/allin/dto/UserDTO.kt index c34686f..b22ddca 100644 --- a/Sources/src/main/kotlin/allin/dto/UserDTO.kt +++ b/Sources/src/main/kotlin/allin/dto/UserDTO.kt @@ -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? ) diff --git a/Sources/src/main/kotlin/allin/ext/DatabaseExt.kt b/Sources/src/main/kotlin/allin/ext/DatabaseExt.kt index 50fd89b..3a5bd61 100644 --- a/Sources/src/main/kotlin/allin/ext/DatabaseExt.kt +++ b/Sources/src/main/kotlin/allin/ext/DatabaseExt.kt @@ -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.toLowerCase(): FunctionExpression { + return FunctionExpression( + functionName = "LOWER", + arguments = listOf(this.asExpression()), + sqlType = VarcharSqlType + ) +} + +fun ColumnDeclaring.toUpperCase(): FunctionExpression { + return FunctionExpression( + functionName = "UPPER", + arguments = listOf(this.asExpression()), + sqlType = VarcharSqlType + ) } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/model/FriendStatus.kt b/Sources/src/main/kotlin/allin/model/FriendStatus.kt new file mode 100644 index 0000000..a5d8763 --- /dev/null +++ b/Sources/src/main/kotlin/allin/model/FriendStatus.kt @@ -0,0 +1,7 @@ +package allin.model + +enum class FriendStatus { + FRIEND, + REQUESTED, + NOT_FRIEND +} \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/model/User.kt b/Sources/src/main/kotlin/allin/model/User.kt index 423986e..89058d3 100644 --- a/Sources/src/main/kotlin/allin/model/User.kt +++ b/Sources/src/main/kotlin/allin/model/User.kt @@ -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( diff --git a/Sources/src/main/kotlin/allin/routing/friendRouter.kt b/Sources/src/main/kotlin/allin/routing/friendRouter.kt index f7d857c..6b747a9 100644 --- a/Sources/src/main/kotlin/allin/routing/friendRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/friendRouter.kt @@ -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> { + body> { description = "List of friends" } } @@ -47,53 +46,53 @@ fun Application.friendRouter() { } } - post("/friends/add", { - description = "Allows a user to add a friend" - request { - headerParameter("JWT token of the logged user") - body { - description = "User to add in the friends list" - } - } - response { - HttpStatusCode.Created to { - description = "the friend has been added" - body() { - description = "Friend with assigned id" + post("/friends/add", { + description = "Allows a user to add a friend" + request { + headerParameter("JWT token of the logged user") + body { + 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 { + 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>() - val usernameFriend = requestMap["username"] ?: return@hasToken call.respond(HttpStatusCode.BadRequest, "Username is missing") - val username = tokenManagerBet.getUsernameFromToken(principal) + }) { + hasToken { principal -> + val requestMap = call.receive>() + 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() { + body { description = "Friend with assigned id" } } @@ -117,7 +116,10 @@ fun Application.friendRouter() { }) { hasToken { principal -> val requestMap = call.receive>() - 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("JWT token of the logged user") + pathParameter("Search string") + } + response { + HttpStatusCode.OK to { + body> { + 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) + } + } + } + } } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/routing/userRouter.kt b/Sources/src/main/kotlin/allin/routing/userRouter.kt index ace7d8b..2b93aef 100644 --- a/Sources/src/main/kotlin/allin/routing/userRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/userRouter.kt @@ -55,11 +55,9 @@ fun Application.userRouter() { val tempUser = call.receive() 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(