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

@ -59,54 +59,67 @@ def ci(ctx):
return CI
def cd(ctx):
CD = {
"kind": "pipeline",
"name": "CD",
"steps": [
{
"name": "hadolint",
"image": "hadolint/hadolint:latest-alpine",
"commands": [
"hadolint Sources/Dockerfile"
]
},
{
"name": "docker-image",
"image": "plugins/docker",
"settings": {
"dockerfile": "Sources/Dockerfile",
"context": "Sources",
"registry": "hub.codefirst.iut.uca.fr",
"repo": "hub.codefirst.iut.uca.fr/lucas.evard/api",
"username": {"from_secret": "SECRET_REGISTRY_USERNAME"},
"password": {"from_secret": "SECRET_REGISTRY_PASSWORD"}
}
},
{
"name": "deploy-container",
"image": "hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dockerproxy-clientdrone:latest",
"environment": {
"CODEFIRST_CLIENTDRONE_ENV_DATA_SOURCE": "postgres",
"CODEFIRST_CLIENTDRONE_ENV_CODEFIRST_CONTAINER": {"from_secret": "CODEFIRST_CONTAINER"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_DB": {"from_secret": "db_database"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_USER": {"from_secret": "db_user"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_PASSWORD": {"from_secret": "db_password"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_HOST": {"from_secret": "db_host"},
"CODEFIRST_CLIENTDRONE_ENV_SALT": {"from_secret": "SALT"},
"ADMINS": "lucasevard,emrekartal,arthurvalin,lucasdelanier",
"IMAGENAME": "hub.codefirst.iut.uca.fr/lucas.evard/api:latest",
"CONTAINERNAME": "api",
"COMMAND": "create",
"OVERWRITE": "true",
},
"depends_on": [
"docker-image"
CD = {
"kind": "pipeline",
"name": "CD",
"volumes": [
{
"name": "images",
"temp": {}
}
],
"steps": [
{
"name": "hadolint",
"image": "hadolint/hadolint:latest-alpine",
"commands": [
"hadolint Sources/Dockerfile"
]
},
{
"name": "docker-image",
"image": "plugins/docker",
"settings": {
"dockerfile": "Sources/Dockerfile",
"context": "Sources",
"registry": "hub.codefirst.iut.uca.fr",
"repo": "hub.codefirst.iut.uca.fr/lucas.evard/api",
"username": {"from_secret": "SECRET_REGISTRY_USERNAME"},
"password": {"from_secret": "SECRET_REGISTRY_PASSWORD"}
}
},
{
"name": "deploy-container",
"image": "hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dockerproxy-clientdrone:latest",
"environment": {
"CODEFIRST_CLIENTDRONE_ENV_DATA_SOURCE": "postgres",
"CODEFIRST_CLIENTDRONE_ENV_CODEFIRST_CONTAINER": {"from_secret": "CODEFIRST_CONTAINER"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_DB": {"from_secret": "db_database"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_USER": {"from_secret": "db_user"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_PASSWORD": {"from_secret": "db_password"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_HOST": {"from_secret": "db_host"},
"CODEFIRST_CLIENTDRONE_ENV_SALT": {"from_secret": "SALT"},
"ADMINS": "lucasevard,emrekartal,arthurvalin,lucasdelanier",
"IMAGENAME": "hub.codefirst.iut.uca.fr/lucas.evard/api:latest",
"CONTAINERNAME": "api",
"COMMAND": "create",
"OVERWRITE": "true",
},
"depends_on": [
"docker-image"
],
"volumes": [
{
"name": "images",
"path": "/uploads"
}
]
}
]
}
]
}
}
return CD
return CD
def db(ctx):
DB = {

@ -10,7 +10,7 @@
<properties>
<kotlin.version>1.9.10</kotlin.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>
<logback_version>1.4.14</logback_version>
<slf4j_version>2.0.9</slf4j_version>
@ -34,7 +34,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.1</version>
<version>42.7.3</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
@ -44,7 +44,7 @@
<dependency>
<groupId>io.github.smiley4</groupId>
<artifactId>ktor-swagger-ui</artifactId>
<version>2.7.4</version>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
@ -142,12 +142,12 @@
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-auth-jwt-jvm</artifactId>
<version>2.3.4</version>
<version>2.3.11</version>
</dependency>
<dependency>
<groupId>io.swagger.codegen.v3</groupId>
<artifactId>swagger-codegen-generators</artifactId>
<version>1.0.38</version>
<version>1.0.50</version>
</dependency>
<dependency>
@ -155,6 +155,12 @@
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<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 isCodeFirstContainer = System.getenv()["CODEFIRST_CONTAINER"].orEmpty()
val hostIP = "0.0.0.0"
val hostPort = 8080
private val allInDataSource: AllInDataSource = when (data_source) {
"mock" -> MockDataSource()
@ -38,7 +40,7 @@ val Application.dataSource: AllInDataSource
get() = allInDataSource
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
embeddedServer(Netty, port = hostPort, host = hostIP) {
extracted()
}.start(wait = true)
}

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

@ -12,4 +12,7 @@ interface UserDataSource {
fun userExists(username: String): Boolean
fun emailExists(email: 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 }
}
}
}
override fun getBetById(id: String): Bet? =

@ -81,4 +81,8 @@ class MockParticipationDataSource(private val mockData: MockDataSource.MockData)
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?> =
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)
override fun addUser(user: User) {
@ -46,4 +59,23 @@ class MockUserDataSource(private val mockData: MockDataSource.MockData) : UserDa
lastGifts[username] = ZonedDateTime.now()
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()
)
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) }

@ -64,5 +64,10 @@ class PostgresParticipationDataSource(private val database: Database) : Particip
}
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
import allin.data.UserDataSource
import allin.data.postgres.entities.UserEntity
import allin.data.postgres.entities.UsersEntity
import allin.data.postgres.entities.users
import allin.data.postgres.entities.*
import allin.dto.UserDTO
import allin.ext.executeWithResult
import allin.model.User
@ -20,7 +18,7 @@ class PostgresUserDataSource(private val database: Database) : UserDataSource {
override fun getUserByUsername(username: String): Pair<UserDTO?, String?> =
database.users
.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)
override fun addUser(user: User) {
@ -83,4 +81,25 @@ class PostgresUserDataSource(private val database: Database) : UserDataSource {
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
import allin.data.postgres.PostgresBetDataSource
import allin.data.postgres.PostgresFriendDataSource
import allin.data.postgres.PostgresParticipationDataSource
import allin.dto.UserDTO
import allin.model.FriendStatus
import allin.routing.imageManagerUser
import allin.utils.AppConfig
import org.ktorm.database.Database
import org.ktorm.dsl.eq
import org.ktorm.entity.Entity
import org.ktorm.entity.find
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.Table
import org.ktorm.schema.int
@ -21,15 +27,27 @@ interface UserEntity : Entity<UserEntity> {
var nbCoins: Int
var lastGift: Instant
fun toUserDTO(friendStatus: FriendStatus? = null) =
fun toUserDTO(database: Database) =
UserDTO(
id = id,
username = username,
email = email,
nbCoins = nbCoins,
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") {

@ -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
import allin.model.FriendStatus
import kotlinx.serialization.Serializable
@Serializable
@ -10,5 +9,8 @@ data class UserDTO(
val email: String,
val nbCoins: Int,
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
import allin.dto.UserDTO
import kotlinx.serialization.Serializable
const val DEFAULT_COIN_AMOUNT = 500
@ -14,18 +13,9 @@ data class User(
val email: String,
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
)
}
var token: String? = null,
var image: String? = null,
)
@Serializable
data class UserRequest(
@ -38,4 +28,10 @@ data class UserRequest(
data class CheckUser(
val login: 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.routing.*
fun Application.basicRouter() {
routing {
get("/", {
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.response.*
import io.ktor.server.routing.*
import java.io.File
import java.util.*
val RegexCheckerUser = AppConfig.regexChecker
val CryptManagerUser = AppConfig.cryptManager
val tokenManagerUser = AppConfig.tokenManager
val imageManagerUser = AppConfig.imageManager
val urlManager = AppConfig.urlManager
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 {
post("/users/delete", {
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 regexChecker = RegexChecker()
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
class ApplicationTest {
}

Loading…
Cancel
Save