diff --git a/api/pom.xml b/api/pom.xml index 63c2e02..06de466 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -12,7 +12,7 @@ 1.3.70 1.2.1 5.6.2 - 2.7.2 + 3.0.0 2.6.0 6.5.4 6.3.3 diff --git a/api/resources/db/migration/V1__Create_user_table.sql b/api/resources/db/migration/V1__Create_user_table.sql index 14a6c98..622f670 100644 --- a/api/resources/db/migration/V1__Create_user_table.sql +++ b/api/resources/db/migration/V1__Create_user_table.sql @@ -1,13 +1,9 @@ create table Users ( - id int auto_increment primary key, - username varchar(50) not null, - email varchar(255) not null, - password varchar(255) not null, - created_at datetime not null, - last_login datetime null, + id int auto_increment primary key, + username varchar(50) not null, + password varchar(255) not null, - constraint email unique (email), constraint username unique (username) ); diff --git a/api/src/entities/User.kt b/api/src/entities/User.kt index 0400eb2..6f12fdb 100644 --- a/api/src/entities/User.kt +++ b/api/src/entities/User.kt @@ -1,15 +1,14 @@ package be.vandewalleh.entities +import com.fasterxml.jackson.annotation.JsonProperty import me.liuwj.ktorm.entity.* -import java.time.LocalDateTime interface User : Entity { companion object : Entity.Factory() val id: Int var username: String - var email: String + + @get:JsonProperty(access = JsonProperty.Access.WRITE_ONLY) var password: String - var createdAt: LocalDateTime - var lastLogin: LocalDateTime? } diff --git a/api/src/extensions/KtormExtensions.kt b/api/src/extensions/KtormExtensions.kt index 2d3c642..f17ba49 100644 --- a/api/src/extensions/KtormExtensions.kt +++ b/api/src/extensions/KtormExtensions.kt @@ -5,7 +5,6 @@ import java.nio.ByteBuffer import java.sql.PreparedStatement import java.sql.ResultSet import java.sql.Types -import java.util.* import java.util.UUID as JavaUUID class UuidBinarySqlType : SqlType(Types.BINARY, typeName = "uuidBinary") { @@ -23,6 +22,6 @@ class UuidBinarySqlType : SqlType(Types.BINARY, typeName = "uuidBinary } } -fun BaseTable.uuidBinary(name: String): BaseTable.ColumnRegistration { +fun BaseTable.uuidBinary(name: String): Column { return registerColumn(name, UuidBinarySqlType()) } diff --git a/api/src/routing/AuthController.kt b/api/src/routing/AuthController.kt index d9f3810..65f767c 100644 --- a/api/src/routing/AuthController.kt +++ b/api/src/routing/AuthController.kt @@ -27,7 +27,7 @@ fun Routing.auth(kodein: Kodein) { post("/user/login") { val credential = call.receive() - val user = userService.getFromUsername(credential.username) + val user = userService.find(credential.username) ?: return@post call.respondStatus(HttpStatusCode.Unauthorized) if (!BCrypt.checkpw(credential.password, user.password)) { @@ -51,7 +51,7 @@ fun Routing.auth(kodein: Kodein) { return@post call.respondStatus(HttpStatusCode.Unauthorized) } - if (!userService.userExists(id)) + if (!userService.exists(id)) return@post call.respondStatus(HttpStatusCode.Unauthorized) val response = DualToken( @@ -63,9 +63,8 @@ fun Routing.auth(kodein: Kodein) { authenticate { get("/user/me") { - // retrieve email from token val id = call.principal()!!.id - val info = userService.getUserInfo(id) + val info = userService.find(id) if (info != null) call.respond(mapOf("user" to info)) else call.respondStatus(HttpStatusCode.Unauthorized) } diff --git a/api/src/routing/UserController.kt b/api/src/routing/UserController.kt index 4205f85..d03eac9 100644 --- a/api/src/routing/UserController.kt +++ b/api/src/routing/UserController.kt @@ -1,6 +1,5 @@ package be.vandewalleh.routing -import be.vandewalleh.entities.User import be.vandewalleh.extensions.respondStatus import be.vandewalleh.extensions.userId import be.vandewalleh.services.UserService @@ -9,13 +8,11 @@ import be.vandewalleh.validation.user.registerValidator import io.ktor.application.* import io.ktor.auth.* import io.ktor.http.* -import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* import org.kodein.di.Kodein import org.kodein.di.generic.instance import org.mindrot.jbcrypt.BCrypt -import java.time.LocalDateTime fun Routing.user(kodein: Kodein) { val userService by kodein.instance() @@ -24,32 +21,20 @@ fun Routing.user(kodein: Kodein) { post { val user = call.receiveValidated(registerValidator) - if (userService.userExists(user.username, user.email)) + if (userService.exists(user.username)) return@post call.respondStatus(HttpStatusCode.Conflict) val hashedPassword = BCrypt.hashpw(user.password, BCrypt.gensalt()) - userService.createUser(user.username, user.email, hashedPassword) + val newUser = userService.create(user.username, hashedPassword) + ?: return@post call.respondStatus(HttpStatusCode.Conflict) - call.respondStatus(HttpStatusCode.Created) + call.respond(HttpStatusCode.Created, newUser) } authenticate { - put { - val user = call.receiveValidated(registerValidator) - - if (userService.userExists(user.username, user.email)) - return@put call.respond(HttpStatusCode.Conflict) - - val hashedPassword = BCrypt.hashpw(user.password, BCrypt.gensalt()) - - userService.updateUser(call.userId(), user.username, user.email, hashedPassword) - - call.respondStatus(HttpStatusCode.OK) - } - delete { - val status = if (userService.deleteUser(call.userId())) + val status = if (userService.delete(call.userId())) HttpStatusCode.OK else HttpStatusCode.NotFound diff --git a/api/src/services/NotesService.kt b/api/src/services/NotesService.kt index cd87a59..eecdca1 100644 --- a/api/src/services/NotesService.kt +++ b/api/src/services/NotesService.kt @@ -47,12 +47,11 @@ class NotesService(override val kodein: Kodein) : KodeinAware { } fun noteExists(userId: Int, uuid: UUID): Boolean { - return db.from(Notes) - .select(Notes.uuid) - .where { Notes.userId eq userId } - .where { Notes.uuid eq uuid } - .limit(0, 1) - .toList().size == 1 + return db.sequenceOf(Notes) + .filterColumns { listOf(it.uuid) } + .find { + it.userId eq userId and (it.uuid eq uuid) + } == null } fun createNote(userId: Int, note: FullNoteCreateDTO): UUID { diff --git a/api/src/services/UserService.kt b/api/src/services/UserService.kt index 3e00482..a5d7d26 100644 --- a/api/src/services/UserService.kt +++ b/api/src/services/UserService.kt @@ -20,62 +20,39 @@ import java.time.LocalDateTime class UserService(override val kodein: Kodein) : KodeinAware { private val db by instance() - /** - * returns a user ID if present or null - */ - suspend fun getUserId(userEmail: String): Int? = launchIo { - db.from(Users) - .select(Users.id) - .where { Users.email eq userEmail } - .map { it[Users.id] } - .firstOrNull() - } /** - * returns a user email and password from it's username if found or null + * returns a user from it's username if found or null */ - suspend fun getFromUsername(username: String): User? = launchIo { - db.from(Users) - .select(Users.email, Users.password, Users.id) - .where { Users.username eq username } - .map { row -> - Users.createEntity(row) - } - .firstOrNull() + suspend fun find(username: String): User? = launchIo { + db.sequenceOf(Users, withReferences = false) + .find { it.username eq username } } - suspend fun userExists(username: String, email: String): Boolean = launchIo { - db.from(Users) - .select(Users.id) - .where { (Users.username eq username) or (Users.email eq email) } - .firstOrNull() != null + suspend fun find(id: Int): User? = launchIo { + db.sequenceOf(Users, withReferences = false) + .find { it.id eq id } } - suspend fun userExists(userId: Int): Boolean = launchIo { - db.from(Users) - .select(Users.id) - .where { Users.id eq userId } - .firstOrNull() != null + + suspend fun exists(username: String) = launchIo { + db.sequenceOf(Users, withReferences = false) + .any { it.username eq username } } - suspend fun getUserInfo(id: Int): User? = launchIo { - db.from(Users) - .select(Users.email, Users.username) - .where { Users.id eq id } - .map { Users.createEntity(it) } - .firstOrNull() + suspend fun exists(userId: Int) = launchIo { + db.sequenceOf(Users).any { it.id eq userId } } + /** * create a new user * password should already be hashed */ - suspend fun createUser(username: String, email: String, hashedPassword: String): User? { + suspend fun create(username: String, hashedPassword: String): User? { val newUser = User { this.username = username - this.email = email password = hashedPassword - createdAt = LocalDateTime.now() } return try { @@ -90,25 +67,11 @@ class UserService(override val kodein: Kodein) : KodeinAware { } } - suspend fun updateUser(userId: Int, username: String, email: String, hashedPassword: String): Unit = launchIo { - db.useTransaction { - db.update(Users) { - it.username to username - it.email to email - it.password to hashedPassword - where { - it.id eq userId - } - } - } - } - - suspend fun deleteUser(userId: Int): Boolean = launchIo { - db.useTransaction { + suspend fun delete(userId: Int): Boolean = launchIo { + val updateCount = db.useTransaction { db.delete(Users) { it.id eq userId } } - }.let { - when (it) { + when (updateCount) { 1 -> true 0 -> false else -> error("??") diff --git a/api/src/tables/Chapters.kt b/api/src/tables/Chapters.kt index ab04c9d..8f4e841 100644 --- a/api/src/tables/Chapters.kt +++ b/api/src/tables/Chapters.kt @@ -5,10 +5,10 @@ import be.vandewalleh.extensions.uuidBinary import me.liuwj.ktorm.schema.* object Chapters : Table("Chapters") { - val id by int("id").primaryKey().bindTo { it.id } - val number by int("number").bindTo { it.number } - val content by text("content").bindTo { it.content } - val title by varchar("title").bindTo { it.title } - val noteUuid by uuidBinary("note_uuid").references(Notes) { it.note } + val id = int("id").primaryKey().bindTo { it.id } + val number = int("number").bindTo { it.number } + val content = text("content").bindTo { it.content } + val title = varchar("title").bindTo { it.title } + val noteUuid = uuidBinary("note_uuid").references(Notes) { it.note } val note get() = noteUuid.referenceTable as Notes -} \ No newline at end of file +} diff --git a/api/src/tables/Notes.kt b/api/src/tables/Notes.kt index 37ca204..3078fff 100644 --- a/api/src/tables/Notes.kt +++ b/api/src/tables/Notes.kt @@ -5,9 +5,9 @@ import be.vandewalleh.extensions.uuidBinary import me.liuwj.ktorm.schema.* object Notes : Table("Notes") { - val uuid by uuidBinary("uuid").primaryKey().bindTo { it.uuid } - val title by varchar("title").bindTo { it.title } - val userId by int("user_id").references(Users) { it.user } - val updatedAt by datetime("updated_at").bindTo { it.updatedAt } + val uuid = uuidBinary("uuid").primaryKey().bindTo { it.uuid } + val title = varchar("title").bindTo { it.title } + val userId = int("user_id").references(Users) { it.user } + val updatedAt = datetime("updated_at").bindTo { it.updatedAt } val user get() = userId.referenceTable as Users -} \ No newline at end of file +} diff --git a/api/src/tables/Tags.kt b/api/src/tables/Tags.kt index 0f26bd1..e173137 100644 --- a/api/src/tables/Tags.kt +++ b/api/src/tables/Tags.kt @@ -5,8 +5,8 @@ import be.vandewalleh.extensions.uuidBinary import me.liuwj.ktorm.schema.* object Tags : Table("Tags") { - val id by int("id").primaryKey().bindTo { it.id } - val name by varchar("name").bindTo { it.name } - val noteUuid by uuidBinary("note_uuid").references(Notes) { it.note } + val id = int("id").primaryKey().bindTo { it.id } + val name = varchar("name").bindTo { it.name } + val noteUuid = uuidBinary("note_uuid").references(Notes) { it.note } val note get() = noteUuid.referenceTable as Notes -} \ No newline at end of file +} diff --git a/api/src/tables/Users.kt b/api/src/tables/Users.kt index 2ae5061..1150785 100644 --- a/api/src/tables/Users.kt +++ b/api/src/tables/Users.kt @@ -4,10 +4,7 @@ import be.vandewalleh.entities.User import me.liuwj.ktorm.schema.* object Users : Table("Users") { - val id by int("id").primaryKey().bindTo { it.id } - val username by varchar("username").bindTo { it.username } - val email by varchar("email").bindTo { it.email } - val password by varchar("password").bindTo { it.password } - val createdAt by datetime("created_at").bindTo { it.createdAt } - val lastLogin by datetime("last_login").bindTo { it.lastLogin } -} \ No newline at end of file + val id = int("id").primaryKey().bindTo { it.id } + val username = varchar("username").bindTo { it.username } + val password = varchar("password").bindTo { it.password } +} diff --git a/api/src/validation/user/UserValidation.kt b/api/src/validation/user/UserValidation.kt index 6f2063e..fb1cb47 100644 --- a/api/src/validation/user/UserValidation.kt +++ b/api/src/validation/user/UserValidation.kt @@ -9,9 +9,6 @@ val registerValidator: Validator = ValidatorBuilder.of() .konstraint(User::username) { notNull().lessThanOrEqual(50).greaterThanOrEqual(3) } - .konstraint(User::email) { - notNull().notEmpty().lessThanOrEqual(255).email() - } .konstraint(User::password) { notNull().greaterThanOrEqual(6) } diff --git a/api/test/integration/routing/AuthControllerKtTest.kt b/api/test/integration/routing/AuthControllerKtTest.kt index 11eb503..b792897 100644 --- a/api/test/integration/routing/AuthControllerKtTest.kt +++ b/api/test/integration/routing/AuthControllerKtTest.kt @@ -13,7 +13,6 @@ import io.ktor.server.testing.* import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk -import io.mockk.verify import org.amshove.kluent.* import org.json.JSONObject import org.junit.jupiter.api.* @@ -37,11 +36,10 @@ class AuthControllerKtTest { } user["id"] = 1 - coEvery { userService.getFromUsername("existing") } returns user - coEvery { userService.userExists(1) } returns true - coEvery { userService.getUserInfo(1) } returns User { + coEvery { userService.find("existing") } returns user + coEvery { userService.exists(1) } returns true + coEvery { userService.find(1) } returns User { username = "existing" - email = "existing@mail.com" } val user2 = User { @@ -49,12 +47,12 @@ class AuthControllerKtTest { username = "wrong" } user["id"] = 2 - coEvery { userService.getFromUsername("wrong") } returns user2 + coEvery { userService.find("wrong") } returns user2 - coEvery { userService.getFromUsername("notExisting") } returns null + coEvery { userService.find("notExisting") } returns null - coEvery { userService.userExists(3) } returns false - coEvery { userService.getUserInfo(3) } returns null + coEvery { userService.exists(3) } returns false + coEvery { userService.find(3) } returns null } @@ -79,7 +77,7 @@ class AuthControllerKtTest { } } - coVerify { userService.getFromUsername("existing") } + coVerify { userService.find("existing") } res.status() `should be equal to` HttpStatusCode.OK val jsonObject = JSONObject(res.content) @@ -107,7 +105,7 @@ class AuthControllerKtTest { } } - coVerify { userService.getFromUsername("wrong") } + coVerify { userService.find("wrong") } res.status() `should be equal to` HttpStatusCode.Unauthorized res.content `should strictly be equal to json` """{msg: "Unauthorized"}""" @@ -122,7 +120,7 @@ class AuthControllerKtTest { } } - coVerify { userService.getFromUsername("notExisting") } + coVerify { userService.find("notExisting") } res.status() `should be equal to` HttpStatusCode.Unauthorized res.content `should strictly be equal to json` """{msg: "Unauthorized"}""" @@ -155,7 +153,7 @@ class AuthControllerKtTest { val jsonObject = JSONObject(res.content) jsonObject.keyList() `should be equal to` listOf("token", "refreshToken") - coVerify { userService.userExists(1) } + coVerify { userService.exists(1) } res.status() `should be equal to` HttpStatusCode.OK } @@ -170,7 +168,7 @@ class AuthControllerKtTest { } } - coVerify { userService.userExists(3) } + coVerify { userService.exists(3) } res.status() `should be equal to` HttpStatusCode.Unauthorized res.content `should strictly be equal to json` """{msg: "Unauthorized"}""" } @@ -207,7 +205,7 @@ class AuthControllerKtTest { val res = testEngine.get("/user/me") { setToken(token) } - res.content `should strictly be equal to json` """{user:{username:"existing", email: "existing@mail.com"}}""" + res.content `should strictly be equal to json` """{user:{username:"existing"}}""" res.status() `should be equal to` HttpStatusCode.OK } diff --git a/api/test/integration/routing/UserControllerKtTest.kt b/api/test/integration/routing/UserControllerKtTest.kt index b2e1955..9615dff 100644 --- a/api/test/integration/routing/UserControllerKtTest.kt +++ b/api/test/integration/routing/UserControllerKtTest.kt @@ -15,7 +15,6 @@ import org.kodein.di.Kodein import org.kodein.di.generic.bind import org.kodein.di.generic.instance import utils.* -import java.time.LocalDateTime @TestInstance(TestInstance.Lifecycle.PER_CLASS) class UserControllerKtTest { @@ -24,31 +23,21 @@ class UserControllerKtTest { init { // new user - coEvery { userService.userExists("new", "new@test.com") } returns false - coEvery { userService.createUser("new", "new@test.com", any()) } returns User { - this.createdAt = LocalDateTime.now() + coEvery { userService.exists("new") } returns false + coEvery { userService.create("new", any()) } returns User { this.username = "new" - this.email = "new@test.com" } // existing user - coEvery { userService.userExists("existing", "existing@test.com") } returns true - coEvery { userService.createUser("existing", "existing@test.com", any()) } returns null - coEvery { userService.getUserId("existing@test.com") } returns 1 - coEvery { userService.deleteUser(1) } returns true andThen false + coEvery { userService.exists("existing") } returns true + coEvery { userService.create("existing", any()) } returns null + coEvery { userService.delete(1) } returns true andThen false // modified user - coEvery { userService.userExists("modified", "modified@test.com") } returns true - coEvery { - userService.userExists( - and(not("modified"), not("existing")), - and(not("modified@test.com"), not("existing@test.com")) - ) - } returns false - coEvery { userService.userExists(1) } returns true - coEvery { userService.createUser("modified", "modified@test.com", any()) } returns null - coEvery { userService.getUserId("modified@test.com") } returns 1 - coEvery { userService.updateUser(1, "ThisIsMyNewName", "ThisIsMyNewName@mail.com", any()) } returns Unit + coEvery { userService.exists("modified") } returns true + coEvery { userService.exists(and(not("modified"), not("existing"))) } returns false + coEvery { userService.exists(1) } returns true + coEvery { userService.create("modified", any()) } returns null } @@ -71,11 +60,10 @@ class UserControllerKtTest { json { it["username"] = "new" it["password"] = "test123abc" - it["email"] = "new@test.com" } } res.status() `should be equal to` HttpStatusCode.Created - res.content `should be equal to json` """{msg:"Created"}""" + res.content `should strictly be equal to json` """{username:"new"}""" } @Test @@ -83,7 +71,6 @@ class UserControllerKtTest { val res = testEngine.post("/user") { json { it["username"] = "existing" - it["email"] = "existing@test.com" it["password"] = "test123abc" } } @@ -116,28 +103,4 @@ class UserControllerKtTest { } } - @Nested - inner class ModifyUser { - - @Test - fun `modify a user`() { - val authJwt by kodein.instance("auth") - val token = authJwt.sign(1) - - val res = testEngine.put("/user") { - setToken(token) - json { - it["username"] = "ThisIsMyNewName" - it["email"] = "ThisIsMyNewName@mail.com" - it["password"] = "ThisIsMyCurrentPassword" - } - } - - res.status() `should be equal to` HttpStatusCode.OK - res.content `should be equal to json` """{msg:"OK"}""" - - } - } - - } diff --git a/api/test/integration/services/UserServiceTest.kt b/api/test/integration/services/UserServiceTest.kt index c758707..0a4ac1a 100644 --- a/api/test/integration/services/UserServiceTest.kt +++ b/api/test/integration/services/UserServiceTest.kt @@ -39,17 +39,12 @@ class UserServiceTest { fun `test create user`() { runBlocking { val username = "hubert" - val email = "a@a" val password = "password" - userService.createUser(username, email, password) - val id = userService.getUserId(email) - id `should not be` null - - userService.getUserInfo(id!!)!!.let { - it.username `should be equal to` username - it.email `should be equal to` email - } + userService.create(username, password) + val user = userService.find(username) + user `should not be` null + user?.username `should be equal to` username } } @@ -57,7 +52,7 @@ class UserServiceTest { @Order(2) fun `test create same user`() { runBlocking { - userService.createUser(username = "hubert", hashedPassword = "password", email = "a@a") `should be` null + userService.create(username = "hubert", hashedPassword = "password") `should be` null } } @@ -65,12 +60,11 @@ class UserServiceTest { @Order(3) fun `test delete user`() { runBlocking { - val email = "a@a" - val id = userService.getUserId(email)!! - userService.deleteUser(id) + val id = userService.find("hubert")!!.id + userService.delete(id) - userService.getUserId(email) `should be` null - userService.getUserInfo(id) `should be` null + userService.find("hubert") `should be` null + userService.find(id) `should be` null } } diff --git a/api/test/unit/validation/RegisterValidationTest.kt b/api/test/unit/validation/RegisterValidationTest.kt index b2ea374..0fac7da 100644 --- a/api/test/unit/validation/RegisterValidationTest.kt +++ b/api/test/unit/validation/RegisterValidationTest.kt @@ -14,41 +14,17 @@ class RegisterValidationTest { val violations = registerValidator.validate(User { username = "hubert" password = "definitelyNotMyPassword" - email = "test@mail.com" }) violations.isValid `should be equal to` true } - @Test - fun `invalid email test`() { - val violations = registerValidator.validate(User { - username = "hubert" - password = "definitelyNotMyPassword" - email = "teom" - }) - - violations.isValid `should be equal to` false - violations.firstInvalid `should be equal to` "email" - } - - @Test - fun `missing email test`() { - val violations = registerValidator.validate(User { - username = "hubert" - password = "definitelyNotMyPassword" - }) - - violations.isValid `should be equal to` false - violations.firstInvalid `should be equal to` "email" - } @Test fun `username too long test`() { val violations = registerValidator.validate(User { username = "6X9iboWmEOWjVjkO328ReTJ1gGPTTmB/ZGgBLhB6EzAJoWkJht8" password = "definitelyNotMyPassword" - email = "test@mail.com" }) violations.isValid `should be equal to` false