Skip to content

Commit

Permalink
Login and register use cases
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanfallet committed Dec 7, 2023
1 parent ace9019 commit 3d960f2
Show file tree
Hide file tree
Showing 24 changed files with 493 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class UsersController(

override suspend fun get(call: ApplicationCall, id: String): User {
val user = requireUserForCallUseCase(call) as User
return getUserUseCase(id, UserContext(user)) ?: throw ControllerException(
return getUserUseCase(id, UserContext(user.id)) ?: throw ControllerException(
HttpStatusCode.NotFound, "users_not_found"
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,25 @@ class DatabaseUsersRepository(
override suspend fun get(id: String, context: IContext?): User? {
if (context !is UserContext) return null
return database.dbQuery {
customJoin(context.user.id)
customJoin(context.userId)
.select { Users.id eq id }
.groupBy(Users.id)
.map(Users::toUser)
.singleOrNull()
}
}

override suspend fun getForEmail(email: String, includePassword: Boolean): User? {
return database.dbQuery {
Users
.select { Users.email eq email }
.map {
Users.toUser(it, includePassword)
}
.singleOrNull()
}
}

override suspend fun create(payload: CreateUserPayload, context: IContext?): User? {
return database.dbQuery {
Users.insert {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,13 @@ import me.nathanfallet.extopy.models.users.CreateUserPayload
import me.nathanfallet.extopy.models.users.UpdateUserPayload
import me.nathanfallet.extopy.models.users.User
import me.nathanfallet.extopy.repositories.users.IUsersRepository
import me.nathanfallet.extopy.usecases.auth.CreateCodeRegisterUseCase
import me.nathanfallet.extopy.usecases.auth.GetCodeRegisterUseCase
import me.nathanfallet.extopy.usecases.auth.RegisterUseCase
import me.nathanfallet.extopy.usecases.auth.*
import me.nathanfallet.extopy.usecases.users.GetUserForCallUseCase
import me.nathanfallet.i18n.usecases.localization.TranslateUseCase
import me.nathanfallet.ktorx.controllers.IModelController
import me.nathanfallet.ktorx.controllers.auth.AuthWithCodeController
import me.nathanfallet.ktorx.controllers.auth.IAuthWithCodeController
import me.nathanfallet.ktorx.usecases.auth.ICreateCodeRegisterUseCase
import me.nathanfallet.ktorx.usecases.auth.IGetCodeRegisterUseCase
import me.nathanfallet.ktorx.usecases.auth.IRegisterUseCase
import me.nathanfallet.ktorx.usecases.auth.*
import me.nathanfallet.ktorx.usecases.localization.GetLocaleForCallUseCase
import me.nathanfallet.ktorx.usecases.localization.IGetLocaleForCallUseCase
import me.nathanfallet.ktorx.usecases.users.IGetUserForCallUseCase
Expand Down Expand Up @@ -60,6 +56,11 @@ fun Application.configureKoin() {
single<IGetLocaleForCallUseCase> { GetLocaleForCallUseCase() }

// Auth
single<IHashPasswordUseCase> { HashPasswordUseCase() }
single<IVerifyPasswordUseCase> { VerifyPasswordUseCase() }
single<IGetJWTPrincipalForCallUseCase> { GetJWTPrincipalForCallUseCase() }
single<IGetSessionForCallUseCase> { GetSessionForCallUseCase() }
single<ILoginUseCase<LoginPayload>> { LoginUseCase(get(), get()) }
single<IRegisterUseCase<RegisterCodePayload>> { RegisterUseCase(get(), get()) }
single<IGetCodeRegisterUseCase<RegisterPayload>> { GetCodeRegisterUseCase() }
single<ICreateCodeRegisterUseCase<RegisterPayload>> {
Expand All @@ -72,7 +73,7 @@ fun Application.configureKoin() {

// Users
single<IRequireUserForCallUseCase> { RequireUserForCallUseCase(get()) }
single<IGetUserForCallUseCase> { GetUserForCallUseCase() }
single<IGetUserForCallUseCase> { GetUserForCallUseCase(get(), get(), get(named<User>())) }
single<IGetModelWithContextSuspendUseCase<User, String>>(named<User>()) {
GetModelWithContextFromRepositorySuspendUseCase(get<IUsersRepository>())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ import me.nathanfallet.extopy.models.users.User
import me.nathanfallet.usecases.models.repositories.IModelSuspendRepository

interface IUsersRepository : IModelSuspendRepository<User, String, CreateUserPayload, UpdateUserPayload> {

suspend fun getForEmail(email: String, includePassword: Boolean): User?

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package me.nathanfallet.extopy.usecases.auth

import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*

class GetJWTPrincipalForCallUseCase : IGetJWTPrincipalForCallUseCase {

override fun invoke(input: ApplicationCall): JWTPrincipal? {
return input.principal()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package me.nathanfallet.extopy.usecases.auth

import io.ktor.server.application.*
import io.ktor.server.sessions.*
import me.nathanfallet.extopy.models.auth.SessionPayload
import me.nathanfallet.ktorx.usecases.auth.IGetSessionForCallUseCase
import me.nathanfallet.usecases.users.ISessionPayload

class GetSessionForCallUseCase : IGetSessionForCallUseCase {

override fun invoke(input: ApplicationCall): ISessionPayload? {
return input.sessions.get<SessionPayload>()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package me.nathanfallet.extopy.usecases.auth

import at.favre.lib.crypto.bcrypt.BCrypt

class HashPasswordUseCase : IHashPasswordUseCase {

override fun invoke(input: String): String {
return BCrypt.withDefaults().hashToString(12, input.toCharArray())
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.nathanfallet.extopy.usecases.auth

import io.ktor.server.application.*
import io.ktor.server.auth.jwt.*
import me.nathanfallet.usecases.base.IUseCase

interface IGetJWTPrincipalForCallUseCase : IUseCase<ApplicationCall, JWTPrincipal?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package me.nathanfallet.extopy.usecases.auth

import me.nathanfallet.usecases.base.IUseCase

interface IHashPasswordUseCase : IUseCase<String, String>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package me.nathanfallet.extopy.usecases.auth

import me.nathanfallet.usecases.base.IPairUseCase

interface IVerifyPasswordUseCase : IPairUseCase<String, String, Boolean>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package me.nathanfallet.extopy.usecases.auth

import me.nathanfallet.extopy.models.auth.LoginPayload
import me.nathanfallet.extopy.repositories.users.IUsersRepository
import me.nathanfallet.ktorx.usecases.auth.ILoginUseCase
import me.nathanfallet.usecases.users.IUser

class LoginUseCase(
private val repository: IUsersRepository,
private val verifyPasswordUseCase: IVerifyPasswordUseCase,
) : ILoginUseCase<LoginPayload> {

override suspend fun invoke(input: LoginPayload): IUser? {
return repository.getForEmail(input.email, true)?.takeIf {
verifyPasswordUseCase(input.password, it.password ?: "")
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package me.nathanfallet.extopy.usecases.auth

import at.favre.lib.crypto.bcrypt.BCrypt

class VerifyPasswordUseCase : IVerifyPasswordUseCase {

override fun invoke(input1: String, input2: String): Boolean {
return BCrypt.verifyer().verify(input1.toCharArray(), input2).verified
}

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
package me.nathanfallet.extopy.usecases.users

import io.ktor.server.application.*
import io.ktor.util.*
import me.nathanfallet.extopy.models.auth.SessionPayload
import me.nathanfallet.extopy.models.users.User
import me.nathanfallet.extopy.models.users.UserContext
import me.nathanfallet.extopy.usecases.auth.IGetJWTPrincipalForCallUseCase
import me.nathanfallet.ktorx.usecases.auth.IGetSessionForCallUseCase
import me.nathanfallet.ktorx.usecases.users.IGetUserForCallUseCase
import me.nathanfallet.usecases.models.get.context.IGetModelWithContextSuspendUseCase
import me.nathanfallet.usecases.users.IUser

class GetUserForCallUseCase : IGetUserForCallUseCase {
class GetUserForCallUseCase(
private val getJWTPrincipalForCall: IGetJWTPrincipalForCallUseCase,
private val getSessionForCallUseCase: IGetSessionForCallUseCase,
private val getUserUseCase: IGetModelWithContextSuspendUseCase<User, String>,
) : IGetUserForCallUseCase {

private data class UserForCall(
val user: User?,
)

private val userKey = AttributeKey<UserForCall>("extopy-user")

override suspend fun invoke(input: ApplicationCall): IUser? {
return null
// Note: we cannot use `computeIfAbsent` because it does not support suspending functions
return input.attributes.getOrNull(userKey)?.user ?: run {
val id =
getJWTPrincipalForCall(input)?.subject ?: (getSessionForCallUseCase(input) as? SessionPayload)?.userId
val computed = UserForCall(id?.let { getUserUseCase(it, UserContext(it)) })
input.attributes.put(userKey, computed)
computed.user
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ import kotlin.test.assertEquals

class UsersRouterTest {

private val user = User(
"id", "displayName", "username"
)
private val user = User("id", "displayName", "username")

private fun installApp(application: ApplicationTestBuilder): HttpClient {
application.environment {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class DatabaseUsersRepositoryTest {
LocalDate(2002, 12, 24)
)
) ?: fail("Unable to create user")
val result = repository.get(user.id, UserContext(user))
val result = repository.get(user.id, UserContext(user.id))
assertEquals(user.id, result?.id)
assertEquals(user.username, result?.username)
assertEquals(user.displayName, result?.displayName)
Expand All @@ -73,7 +73,7 @@ class DatabaseUsersRepositoryTest {
LocalDate(2002, 12, 24)
)
) ?: fail("Unable to create user")
assertEquals(null, repository.get("userId", UserContext(user)))
assertEquals(null, repository.get("userId", UserContext(user.id)))
}

@Test
Expand All @@ -89,6 +89,44 @@ class DatabaseUsersRepositoryTest {
assertEquals(null, repository.get(user.id))
}

@Test
fun getUserForEmail() = runBlocking {
val database = Database(protocol = "h2", name = "getUserForEmail")
val repository = DatabaseUsersRepository(database)
val user = repository.create(
CreateUserPayload(
"username", "displayName", "email", "password",
LocalDate(2002, 12, 24)
)
) ?: fail("Unable to create user")
val result = repository.getForEmail(user.email!!, false)
assertEquals(user.id, result?.id)
assertEquals(user.username, result?.username)
assertEquals(user.displayName, result?.displayName)
assertEquals(user.email, result?.email)
assertEquals(null, result?.password)
assertEquals(user.birthdate, result?.birthdate)
}

@Test
fun getUserForEmailWithPassword() = runBlocking {
val database = Database(protocol = "h2", name = "getUserForEmailWithPassword")
val repository = DatabaseUsersRepository(database)
val user = repository.create(
CreateUserPayload(
"username", "displayName", "email", "password",
LocalDate(2002, 12, 24)
)
) ?: fail("Unable to create user")
val result = repository.getForEmail(user.email!!, true)
assertEquals(user.id, result?.id)
assertEquals(user.username, result?.username)
assertEquals(user.displayName, result?.displayName)
assertEquals(user.email, result?.email)
assertEquals("password", result?.password)
assertEquals(user.birthdate, result?.birthdate)
}

@Test
fun updateUser() = runBlocking {
val database = Database(protocol = "h2", name = "updateUser")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package me.nathanfallet.extopy.usecases.auth

import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.mockk.every
import io.mockk.mockk
import kotlin.test.Test
import kotlin.test.assertEquals

class GetJWTPrincipalForCallUseCaseTest {

@Test
fun invoke() {
val useCase = GetJWTPrincipalForCallUseCase()
val call = mockk<ApplicationCall>()
val principal = mockk<JWTPrincipal>()
every { call.principal<JWTPrincipal>() } returns principal
assertEquals(principal, useCase(call))
}

@Test
fun invokeNull() {
val useCase = GetJWTPrincipalForCallUseCase()
val call = mockk<ApplicationCall>()
every { call.principal<JWTPrincipal>() } returns null
assertEquals(null, useCase(call))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package me.nathanfallet.extopy.usecases.auth

import io.ktor.client.request.*
import io.ktor.server.application.*
import io.ktor.server.config.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import io.ktor.server.testing.*
import me.nathanfallet.extopy.models.auth.SessionPayload
import me.nathanfallet.extopy.plugins.configureSessions
import kotlin.test.Test
import kotlin.test.assertEquals

class GetSessionForCallUseCaseTest {

@Test
fun invoke() = testApplication {
environment {
config = ApplicationConfig("application.test.conf")
}
application {
configureSessions()
}
routing {
get {
val useCase = GetSessionForCallUseCase()
assertEquals(null, useCase(call))
call.sessions.set(SessionPayload("id"))
assertEquals(SessionPayload("id"), useCase(call))
}
}
client.get("/")
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package me.nathanfallet.extopy.usecases.auth

import at.favre.lib.crypto.bcrypt.BCrypt
import kotlin.test.Test
import kotlin.test.assertTrue

class HashPasswordUseCaseTest {

@Test
fun invoke() {
val useCase = HashPasswordUseCase()
val hash = useCase("password")
assertTrue(BCrypt.verifyer().verify("password".toCharArray(), hash).verified)
}

}
Loading

0 comments on commit 3d960f2

Please sign in to comment.