diff --git a/.drone.star b/.drone.star index 5910419..37e215c 100644 --- a/.drone.star +++ b/.drone.star @@ -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 = { diff --git a/Sources/pom.xml b/Sources/pom.xml index 9536a23..897fde2 100644 --- a/Sources/pom.xml +++ b/Sources/pom.xml @@ -10,7 +10,7 @@ 1.9.10 1.5.0 - 2.3.4 + 2.3.11 official 1.4.14 2.0.9 @@ -34,7 +34,7 @@ org.postgresql postgresql - 42.7.1 + 42.7.3 io.ktor @@ -44,7 +44,7 @@ io.github.smiley4 ktor-swagger-ui - 2.7.4 + 2.10.0 io.ktor @@ -142,12 +142,12 @@ io.ktor ktor-server-auth-jwt-jvm - 2.3.4 + 2.3.11 io.swagger.codegen.v3 swagger-codegen-generators - 1.0.38 + 1.0.50 @@ -155,6 +155,12 @@ hamcrest-core 1.3 + + org.junit.jupiter + junit-jupiter + RELEASE + test + ${project.basedir}/src/main/kotlin diff --git a/Sources/src/main/kotlin/allin/Application.kt b/Sources/src/main/kotlin/allin/Application.kt index 7b5a97d..cbc89f6 100644 --- a/Sources/src/main/kotlin/allin/Application.kt +++ b/Sources/src/main/kotlin/allin/Application.kt @@ -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) } diff --git a/Sources/src/main/kotlin/allin/data/ParticipationDataSource.kt b/Sources/src/main/kotlin/allin/data/ParticipationDataSource.kt index 1e6b3d8..5490ff9 100644 --- a/Sources/src/main/kotlin/allin/data/ParticipationDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/ParticipationDataSource.kt @@ -7,4 +7,5 @@ interface ParticipationDataSource { fun getParticipationFromBetId(betid: String): List fun getParticipationFromUserId(username: String, betid: String): List fun deleteParticipation(id: String): Boolean + fun getBestWinFromUserid(userId: String): Int? } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/UserDataSource.kt b/Sources/src/main/kotlin/allin/data/UserDataSource.kt index ba4d54a..a116264 100644 --- a/Sources/src/main/kotlin/allin/data/UserDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/UserDataSource.kt @@ -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? } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/mock/MockBetDataSource.kt b/Sources/src/main/kotlin/allin/data/mock/MockBetDataSource.kt index 1a29763..3e017d3 100644 --- a/Sources/src/main/kotlin/allin/data/mock/MockBetDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/mock/MockBetDataSource.kt @@ -48,7 +48,6 @@ class MockBetDataSource(private val mockData: MockDataSource.MockData) : BetData }.map { it } } } - } override fun getBetById(id: String): Bet? = diff --git a/Sources/src/main/kotlin/allin/data/mock/MockParticipationDataSource.kt b/Sources/src/main/kotlin/allin/data/mock/MockParticipationDataSource.kt index 653191a..68701f3 100644 --- a/Sources/src/main/kotlin/allin/data/mock/MockParticipationDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/mock/MockParticipationDataSource.kt @@ -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 + + } \ 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 047aa06..004bb28 100644 --- a/Sources/src/main/kotlin/allin/data/mock/MockUserDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/mock/MockUserDataSource.kt @@ -11,7 +11,20 @@ class MockUserDataSource(private val mockData: MockDataSource.MockData) : UserDa override fun getUserByUsername(username: String): Pair = 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 + } + } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt index 12e2f11..9548fbb 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt @@ -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) } diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresParticipationDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresParticipationDataSource.kt index 4ba7fa8..f7d9a29 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresParticipationDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresParticipationDataSource.kt @@ -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 } + + } diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresUserDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresUserDataSource.kt index d6f9fb3..6794e5b 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresUserDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresUserDataSource.kt @@ -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 = 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 + } } \ 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 bf1defb..a86f6d2 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/entities/UserEntity.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/entities/UserEntity.kt @@ -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 { 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("users") { diff --git a/Sources/src/main/kotlin/allin/data/postgres/entities/UserImageEntity.kt b/Sources/src/main/kotlin/allin/data/postgres/entities/UserImageEntity.kt new file mode 100644 index 0000000..64a47eb --- /dev/null +++ b/Sources/src/main/kotlin/allin/data/postgres/entities/UserImageEntity.kt @@ -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 { + companion object : Entity.Factory() + + var id: String + var image: ByteArray + + fun toUserImage() = + UserImage( + id = id, + image = image, + ) +} + +object UsersImageEntity : Table("userimage") { + val id = varchar("user_id").primaryKey().bindTo { it.id } + val image = bytes("image").bindTo { it.image } +} + +val Database.usersimage get() = this.sequenceOf(UsersImageEntity) diff --git a/Sources/src/main/kotlin/allin/dto/UserDTO.kt b/Sources/src/main/kotlin/allin/dto/UserDTO.kt index b22ddca..1e46a6a 100644 --- a/Sources/src/main/kotlin/allin/dto/UserDTO.kt +++ b/Sources/src/main/kotlin/allin/dto/UserDTO.kt @@ -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 +) \ 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 89058d3..a1f76d8 100644 --- a/Sources/src/main/kotlin/allin/model/User.kt +++ b/Sources/src/main/kotlin/allin/model/User.kt @@ -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 -) \ No newline at end of file +) + +@Serializable +data class UserImage( + val id: String, + val image: ByteArray, +) diff --git a/Sources/src/main/kotlin/allin/routing/basicRouter.kt b/Sources/src/main/kotlin/allin/routing/basicRouter.kt index f4a16e4..92d4e2d 100644 --- a/Sources/src/main/kotlin/allin/routing/basicRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/basicRouter.kt @@ -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" diff --git a/Sources/src/main/kotlin/allin/routing/userRouter.kt b/Sources/src/main/kotlin/allin/routing/userRouter.kt index 2b93aef..06ec225 100644 --- a/Sources/src/main/kotlin/allin/routing/userRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/userRouter.kt @@ -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("JWT token of the logged user") + body { + 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) + } + } + } + } } } diff --git a/Sources/src/main/kotlin/allin/utils/AppConfig.kt b/Sources/src/main/kotlin/allin/utils/AppConfig.kt index 554e138..7aaddc8 100644 --- a/Sources/src/main/kotlin/allin/utils/AppConfig.kt +++ b/Sources/src/main/kotlin/allin/utils/AppConfig.kt @@ -8,4 +8,6 @@ object AppConfig { val tokenManager = TokenManager.getInstance(config) val regexChecker = RegexChecker() val cryptManager = CryptManager() + val imageManager = ImageManager() + val urlManager = URLManager() } diff --git a/Sources/src/main/kotlin/allin/utils/ImageManager.kt b/Sources/src/main/kotlin/allin/utils/ImageManager.kt new file mode 100644 index 0000000..18b977b --- /dev/null +++ b/Sources/src/main/kotlin/allin/utils/ImageManager.kt @@ -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", "") +} \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/utils/URLManager.kt b/Sources/src/main/kotlin/allin/utils/URLManager.kt new file mode 100644 index 0000000..15e44ae --- /dev/null +++ b/Sources/src/main/kotlin/allin/utils/URLManager.kt @@ -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}" + } +} \ No newline at end of file diff --git a/Sources/src/test/kotlin/allin/ApplicationTest.kt b/Sources/src/test/kotlin/allin/ApplicationTest.kt index 055425d..9dd77ad 100644 --- a/Sources/src/test/kotlin/allin/ApplicationTest.kt +++ b/Sources/src/test/kotlin/allin/ApplicationTest.kt @@ -1,4 +1,5 @@ package allin class ApplicationTest { + }