Merge remote-tracking branch 'origin/profilePicture'

# Conflicts:
#	Sources/src/main/kotlin/allin/data/mock/MockUserDataSource.kt
#	Sources/src/main/kotlin/allin/data/postgres/entities/UserEntity.kt
#	Sources/src/main/kotlin/allin/dto/UserDTO.kt
#	Sources/src/main/kotlin/allin/model/User.kt
pull/17/head
luevard 11 months ago
commit 110aab80a7

@ -62,6 +62,12 @@ def cd(ctx):
CD = { CD = {
"kind": "pipeline", "kind": "pipeline",
"name": "CD", "name": "CD",
"volumes": [
{
"name": "images",
"temp": {}
}
],
"steps": [ "steps": [
{ {
"name": "hadolint", "name": "hadolint",
@ -101,6 +107,12 @@ def cd(ctx):
}, },
"depends_on": [ "depends_on": [
"docker-image" "docker-image"
],
"volumes": [
{
"name": "images",
"path": "/uploads"
}
] ]
} }
] ]
@ -108,6 +120,7 @@ def cd(ctx):
return CD return CD
def db(ctx): def db(ctx):
DB = { DB = {
"kind": "pipeline", "kind": "pipeline",

@ -10,7 +10,7 @@
<properties> <properties>
<kotlin.version>1.9.10</kotlin.version> <kotlin.version>1.9.10</kotlin.version>
<serialization.version>1.5.0</serialization.version> <serialization.version>1.5.0</serialization.version>
<ktor_version>2.3.4</ktor_version> <ktor_version>2.3.11</ktor_version>
<kotlin.code.style>official</kotlin.code.style> <kotlin.code.style>official</kotlin.code.style>
<logback_version>1.4.14</logback_version> <logback_version>1.4.14</logback_version>
<slf4j_version>2.0.9</slf4j_version> <slf4j_version>2.0.9</slf4j_version>
@ -34,7 +34,7 @@
<dependency> <dependency>
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>
<version>42.7.1</version> <version>42.7.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.ktor</groupId> <groupId>io.ktor</groupId>
@ -44,7 +44,7 @@
<dependency> <dependency>
<groupId>io.github.smiley4</groupId> <groupId>io.github.smiley4</groupId>
<artifactId>ktor-swagger-ui</artifactId> <artifactId>ktor-swagger-ui</artifactId>
<version>2.7.4</version> <version>2.10.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.ktor</groupId> <groupId>io.ktor</groupId>
@ -142,12 +142,12 @@
<dependency> <dependency>
<groupId>io.ktor</groupId> <groupId>io.ktor</groupId>
<artifactId>ktor-server-auth-jwt-jvm</artifactId> <artifactId>ktor-server-auth-jwt-jvm</artifactId>
<version>2.3.4</version> <version>2.3.11</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.swagger.codegen.v3</groupId> <groupId>io.swagger.codegen.v3</groupId>
<artifactId>swagger-codegen-generators</artifactId> <artifactId>swagger-codegen-generators</artifactId>
<version>1.0.38</version> <version>1.0.50</version>
</dependency> </dependency>
<dependency> <dependency>
@ -155,6 +155,12 @@
<artifactId>hamcrest-core</artifactId> <artifactId>hamcrest-core</artifactId>
<version>1.3</version> <version>1.3</version>
</dependency> </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory> <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>

@ -27,6 +27,8 @@ val BET_VERIFY_DELAY = 1.minutes
val data_source = System.getenv()["DATA_SOURCE"] val data_source = System.getenv()["DATA_SOURCE"]
val isCodeFirstContainer = System.getenv()["CODEFIRST_CONTAINER"].orEmpty() val isCodeFirstContainer = System.getenv()["CODEFIRST_CONTAINER"].orEmpty()
val hostIP = "0.0.0.0"
val hostPort = 8080
private val allInDataSource: AllInDataSource = when (data_source) { private val allInDataSource: AllInDataSource = when (data_source) {
"mock" -> MockDataSource() "mock" -> MockDataSource()
@ -38,7 +40,7 @@ val Application.dataSource: AllInDataSource
get() = allInDataSource get() = allInDataSource
fun main() { fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") { embeddedServer(Netty, port = hostPort, host = hostIP) {
extracted() extracted()
}.start(wait = true) }.start(wait = true)
} }

@ -7,4 +7,5 @@ interface ParticipationDataSource {
fun getParticipationFromBetId(betid: String): List<Participation> fun getParticipationFromBetId(betid: String): List<Participation>
fun getParticipationFromUserId(username: String, betid: String): List<Participation> fun getParticipationFromUserId(username: String, betid: String): List<Participation>
fun deleteParticipation(id: String): Boolean fun deleteParticipation(id: String): Boolean
fun getBestWinFromUserid(userId: String): Int?
} }

@ -12,4 +12,7 @@ interface UserDataSource {
fun userExists(username: String): Boolean fun userExists(username: String): Boolean
fun emailExists(email: String): Boolean fun emailExists(email: String): Boolean
fun canHaveDailyGift(username: String): Boolean fun canHaveDailyGift(username: String): Boolean
fun addImage(userid: String, image: ByteArray)
fun removeImage(userid: String)
fun getImage(userid: String): String?
} }

@ -48,7 +48,6 @@ class MockBetDataSource(private val mockData: MockDataSource.MockData) : BetData
}.map { it } }.map { it }
} }
} }
} }
override fun getBetById(id: String): Bet? = override fun getBetById(id: String): Bet? =

@ -81,4 +81,8 @@ class MockParticipationDataSource(private val mockData: MockDataSource.MockData)
return result return result
} }
override fun getBestWinFromUserid(userId: String) =
mockData.participations.filter { it.id == userId }.maxBy { it.stake }.stake
} }

@ -11,7 +11,20 @@ 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 {
it.toDto() to it.password Pair(
UserDTO(
id = it.id,
username = it.username,
email = it.email,
nbCoins = it.nbCoins,
token = it.token,
image = null,
nbBets = MockBetDataSource(mockData).getHistory(it.username).count(),
nbFriends = MockFriendDataSource(mockData).getFriendFromUserId(it.id).count(),
bestWin = MockParticipationDataSource(mockData).getBestWinFromUserid(it.id)
),
it.password
)
} ?: Pair(null, null) } ?: Pair(null, null)
override fun addUser(user: User) { override fun addUser(user: User) {
@ -46,4 +59,23 @@ class MockUserDataSource(private val mockData: MockDataSource.MockData) : UserDa
lastGifts[username] = ZonedDateTime.now() lastGifts[username] = ZonedDateTime.now()
return value return value
} }
override fun addImage(userid: String, image: ByteArray) {
val user = users.find { it.id == userid }
if (user != null) {
user.image = image.toString()
}
}
override fun removeImage(userid: String) {
val user = users.find { it.id == userid }
if (user != null) {
user.image = null
}
}
override fun getImage(userid: String): String? {
return users.find { it.id == userid }?.image
}
} }

@ -133,6 +133,16 @@ class PostgresDataSource : AllInDataSource() {
) )
""".trimIndent() """.trimIndent()
) )
database.execute(
"""
CREATE TABLE IF NOT EXISTS userimage
(
user_id VARCHAR(255) PRIMARY KEY,
image bytea
)
""".trimIndent()
)
} }
override val userDataSource: UserDataSource by lazy { PostgresUserDataSource(database) } override val userDataSource: UserDataSource by lazy { PostgresUserDataSource(database) }

@ -64,5 +64,10 @@ class PostgresParticipationDataSource(private val database: Database) : Particip
} }
return participation.delete() > 0 return participation.delete() > 0
} }
override fun getBestWinFromUserid(userId: String) =
database.participations.filter { it.id eq userId }.maxBy { it.stake }
} }

@ -1,9 +1,7 @@
package allin.data.postgres package allin.data.postgres
import allin.data.UserDataSource import allin.data.UserDataSource
import allin.data.postgres.entities.UserEntity import allin.data.postgres.entities.*
import allin.data.postgres.entities.UsersEntity
import allin.data.postgres.entities.users
import allin.dto.UserDTO import allin.dto.UserDTO
import allin.ext.executeWithResult import allin.ext.executeWithResult
import allin.model.User import allin.model.User
@ -20,7 +18,7 @@ class PostgresUserDataSource(private val database: Database) : UserDataSource {
override fun getUserByUsername(username: String): Pair<UserDTO?, String?> = override fun getUserByUsername(username: String): Pair<UserDTO?, String?> =
database.users database.users
.find { (it.username eq username) or (it.email eq username) } .find { (it.username eq username) or (it.email eq username) }
?.let { it.toUserDTO() to it.password } ?.let { it.toUserDTO(database) to it.password }
?: (null to null) ?: (null to null)
override fun addUser(user: User) { override fun addUser(user: User) {
@ -83,4 +81,25 @@ class PostgresUserDataSource(private val database: Database) : UserDataSource {
return false return false
} }
override fun addImage(userid: String, image: ByteArray) {
database.usersimage.add(UserImageEntity {
id = userid
this.image = image
})
}
override fun removeImage(userid: String) {
database.usersimage.removeIf { it.id eq userid }
}
override fun getImage(userid: String): String? {
val resultSet = database.executeWithResult("SELECT encode(image, 'base64') AS image FROM userimage WHERE user_id = '${userid}'")?: return null
if (resultSet.next()) {
val base64Image: String? = resultSet.getString("image")
if (base64Image != null) {
return base64Image
}
}
return null
}
} }

@ -1,9 +1,15 @@
package allin.data.postgres.entities package allin.data.postgres.entities
import allin.data.postgres.PostgresBetDataSource
import allin.data.postgres.PostgresFriendDataSource
import allin.data.postgres.PostgresParticipationDataSource
import allin.dto.UserDTO import allin.dto.UserDTO
import allin.model.FriendStatus import allin.routing.imageManagerUser
import allin.utils.AppConfig
import org.ktorm.database.Database import org.ktorm.database.Database
import org.ktorm.dsl.eq
import org.ktorm.entity.Entity import org.ktorm.entity.Entity
import org.ktorm.entity.find
import org.ktorm.entity.sequenceOf import org.ktorm.entity.sequenceOf
import org.ktorm.schema.Table import org.ktorm.schema.Table
import org.ktorm.schema.int import org.ktorm.schema.int
@ -21,15 +27,27 @@ interface UserEntity : Entity<UserEntity> {
var nbCoins: Int var nbCoins: Int
var lastGift: Instant var lastGift: Instant
fun toUserDTO(friendStatus: FriendStatus? = null) = fun toUserDTO(database: Database) =
UserDTO( UserDTO(
id = id, id = id,
username = username, username = username,
email = email, email = email,
nbCoins = nbCoins, nbCoins = nbCoins,
token = null, token = null,
friendStatus = friendStatus image = getImage(id, database),
nbBets = PostgresBetDataSource(database).getHistory(username).count(),
nbFriends = PostgresFriendDataSource(database).getFriendFromUserId(id).count(),
bestWin = PostgresParticipationDataSource(database).getBestWinFromUserid(id)?: 0,
) )
fun getImage(userId: String, database: Database): String? {
val imageByte = database.usersimage.find { it.id eq id }?.image ?: return null
val urlfile = "images/$userId"
if (!imageManagerUser.imageAvailable(urlfile)) {
imageManagerUser.saveImage(urlfile, imageByte)
}
return "${AppConfig.urlManager.getURL()}users/${urlfile}"
}
} }
object UsersEntity : Table<UserEntity>("users") { object UsersEntity : Table<UserEntity>("users") {

@ -0,0 +1,29 @@
package allin.data.postgres.entities
import allin.model.UserImage
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.Table
import org.ktorm.schema.bytes
import org.ktorm.schema.varchar
interface UserImageEntity : Entity<UserImageEntity> {
companion object : Entity.Factory<UserImageEntity>()
var id: String
var image: ByteArray
fun toUserImage() =
UserImage(
id = id,
image = image,
)
}
object UsersImageEntity : Table<UserImageEntity>("userimage") {
val id = varchar("user_id").primaryKey().bindTo { it.id }
val image = bytes("image").bindTo { it.image }
}
val Database.usersimage get() = this.sequenceOf(UsersImageEntity)

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

@ -1,6 +1,5 @@
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,18 +13,9 @@ data class User(
val email: String, val email: String,
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,
) { var image: 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(
@ -39,3 +29,9 @@ data class CheckUser(
val login: String, val login: String,
val password: String val password: String
) )
@Serializable
data class UserImage(
val id: String,
val image: ByteArray,
)

@ -7,8 +7,8 @@ import io.ktor.server.application.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
fun Application.basicRouter() { fun Application.basicRouter() {
routing { routing {
get("/", { get("/", {
description = "Hello World of Allin API" description = "Hello World of Allin API"

@ -15,11 +15,15 @@ import io.ktor.server.auth.jwt.*
import io.ktor.server.request.* import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import java.io.File
import java.util.* import java.util.*
val RegexCheckerUser = AppConfig.regexChecker val RegexCheckerUser = AppConfig.regexChecker
val CryptManagerUser = AppConfig.cryptManager val CryptManagerUser = AppConfig.cryptManager
val tokenManagerUser = AppConfig.tokenManager val tokenManagerUser = AppConfig.tokenManager
val imageManagerUser = AppConfig.imageManager
val urlManager = AppConfig.urlManager
const val DEFAULT_COINS = 500 const val DEFAULT_COINS = 500
@ -105,6 +109,23 @@ fun Application.userRouter() {
} }
} }
get("/users/images/{fileName}") {
val fileName = call.parameters["fileName"]
val urlfile = "images/$fileName"
val file = File("$urlfile.png")
if (file.exists()) {
call.respondFile(file)
} else {
val imageBytes = userDataSource.getImage(fileName.toString())
if (imageBytes != null) {
imageManagerUser.saveImage(urlfile, imageBytes)
call.respondFile(file)
} else {
call.respond(HttpStatusCode.NotFound, "File not found")
}
}
}
authenticate { authenticate {
post("/users/delete", { post("/users/delete", {
description = "Allow you to delete your account" description = "Allow you to delete your account"
@ -196,6 +217,44 @@ fun Application.userRouter() {
} }
} }
} }
post("/users/image", {
description = "Allow you to add a profil image"
request {
headerParameter<JWTPrincipal>("JWT token of the logged user")
body<CheckUser> {
description = "User information"
}
}
response {
HttpStatusCode.Accepted to {
description = "Image added"
}
HttpStatusCode.NotFound to {
description = "User not found"
body(ApiMessage.INCORRECT_LOGIN_PASSWORD)
}
}
}) {
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
val base64Image = call.receiveText()
val urlfile = "images/${user.id}"
val imageByteArray = imageManagerUser.saveImage(urlfile, base64Image)
if (imageByteArray != null && imageByteArray.isNotEmpty()) {
userDataSource.removeImage(user.id)
userDataSource.addImage(user.id, imageByteArray)
call.respond(HttpStatusCode.OK, "${urlManager.getURL()}users/${urlfile}")
}
call.respond(HttpStatusCode.Conflict)
}
}
}
} }
} }
} }

@ -8,4 +8,6 @@ object AppConfig {
val tokenManager = TokenManager.getInstance(config) val tokenManager = TokenManager.getInstance(config)
val regexChecker = RegexChecker() val regexChecker = RegexChecker()
val cryptManager = CryptManager() val cryptManager = CryptManager()
val imageManager = ImageManager()
val urlManager = URLManager()
} }

@ -0,0 +1,26 @@
package allin.utils
import java.io.File
import java.util.*
class ImageManager {
fun saveImage(urlfile: String, base64Image: String): ByteArray? {
val cleanedBase64Image = cleanBase64(base64Image)
val imageBytes = Base64.getDecoder().decode(cleanedBase64Image)
val file = File("${urlfile}.png")
file.parentFile.mkdirs()
file.writeBytes(imageBytes)
return imageBytes
}
fun saveImage(urlfile: String, base64Image: ByteArray) {
val file = File("${urlfile}.png")
file.parentFile.mkdirs()
file.writeBytes(base64Image)
}
fun imageAvailable(urlfile: String) = File(urlfile).exists()
fun cleanBase64(base64Image: String) = base64Image.replace("\n", "").replace("\r", "")
}

@ -0,0 +1,13 @@
package allin.utils
import allin.hostIP
import allin.hostPort
import allin.isCodeFirstContainer
class URLManager {
fun getURL(): String {
return if (isCodeFirstContainer.isEmpty()) {
"http://$hostIP:$hostPort/"
} else "http://codefirst.iut.uca.fr${isCodeFirstContainer}"
}
}

@ -1,4 +1,5 @@
package allin package allin
class ApplicationTest { class ApplicationTest {
} }

Loading…
Cancel
Save