Skip to content

Commit

Permalink
The provided code changes implement user activation functionality wit…
Browse files Browse the repository at this point in the history
…hin a Kotlin Spring application using R2DBC for database interactions. Let's break down the modifications:

**1. `UserActivationDao.kt`:**

* The `activateUser` function is renamed to `activate` for conciseness.
* A commented-out code block suggests a previous implementation directly handling success/failure checks based on the database update result. This logic is moved to the service layer.

**2. `SignupService.kt`:**

* The `signup` function is streamlined, directly returning the result of the signup process.
* New functions `activateRequest` and `activate` are introduced.  `activateRequest` takes the activation key and `ServerWebExchange` to perform the activation.  `activate` encapsulates the database interaction and error handling. It calls the `activate` method from the DAO, checks the result, and throws exceptions if activation fails. The constant `ONE_ROW_UPDATED` is added to check if exactly one row was updated during activation, ensuring only the intended user is activated.
* A commented-out code block outlines a previous, more complex activation approach likely involving direct database queries and checks within the controller. This is replaced by the cleaner service layer approach.

**3. `DaoTests.kt`:**

* The test code is updated to use the renamed `activate` function. The assertion checking the number of updated rows is removed as this validation is now in the `SignupService`.

**4. `UserController.kt`:**

* A new `activate` endpoint is added, mapped to `GET /activate`.  It takes the activation key as a request parameter and delegates the activation process to the `activateRequest` function in the service.

**Key improvements:**

* **Centralized logic:** Moving the activation logic to the `SignupService` improves code organization and maintainability.
* **Error handling:** The `activate` function in the service provides robust error handling and clear exception messages.
* **Simplified controller:** The `UserController` is now more concise, focusing on routing requests and handling responses.
* **Testability:** The refactored code is easier to test, with clear separation of concerns between the controller, service, and DAO layers.

This refined implementation provides a more structured, maintainable, and testable approach to user activation, leveraging the power of Spring's service layer and clear error handling.  The change also illustrates the idea of keeping controllers lean, delegating business logic to services.
  • Loading branch information
cheroliv committed Dec 12, 2024
1 parent a3e75fd commit b3e56c6
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 16 deletions.
34 changes: 27 additions & 7 deletions api/src/main/kotlin/users/UserController.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package users

import org.springframework.http.MediaType.APPLICATION_PROBLEM_JSON_VALUE
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*
import org.springframework.web.server.ServerWebExchange
import users.UserController.UserRestApiRoutes.API_ACTIVATE
import users.UserController.UserRestApiRoutes.API_ACTIVATE_KEY
import users.UserController.UserRestApiRoutes.API_SIGNUP
import users.UserController.UserRestApiRoutes.API_USERS
import users.signup.Signup
Expand Down Expand Up @@ -33,16 +32,37 @@ class UserController(private val service: SignupService) {
}

/**
* {@code POST /signup} : Signup the user.
* Handles user signup requests. This method processes the incoming signup data and
* initiates the signup flow using the provided service layer.
*
* @param signup the managed signup View Model.
* @param signup The signup object containing user details such as login, email,
* password, and confirmation password.
* @param exchange The server web exchange instance, which provides access to the
* request and response context during the signup process.
*/
@PostMapping(
API_SIGNUP,
produces = [APPLICATION_PROBLEM_JSON_VALUE]
)
suspend fun signup(
@RequestBody signup: Signup,
@RequestBody
signup: Signup,
exchange: ServerWebExchange
) = service.signupRequest(signup, exchange)

/**
* Activates a user account using a provided activation key. This method processes
* the activation key and ensures the associated account is marked as activated.
*
* @param key The activation key used to verify and activate the user account.
* @param exchange The server web exchange instance, which provides access to the
* request and response context during the activation process.
*/
@GetMapping(API_ACTIVATE)
// @Throws(SignupException::class)
suspend fun activate(
@RequestParam(API_ACTIVATE_KEY)
key: String,
exchange: ServerWebExchange
) = service.activateRequest(key, exchange)
}
46 changes: 41 additions & 5 deletions api/src/main/kotlin/users/signup/SignupService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import app.http.HttpUtils.validator
import app.http.ProblemsModel
import app.utils.Constants.defaultProblems
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left
import arrow.core.right
import jakarta.validation.ConstraintViolation
Expand All @@ -20,17 +21,18 @@ import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.server.ServerWebExchange
import users.User
import users.UserController.UserRestApiRoutes.API_SIGNUP
import users.UserController.UserRestApiRoutes.API_USERS
import users.UserDao
import users.UserDao.Dao.signup
import users.UserDao.Dao.signupAvailability
import users.UserDao.Dao.signupToUser
import users.UserController.UserRestApiRoutes.API_SIGNUP
import users.UserController.UserRestApiRoutes.API_USERS
import users.signup.UserActivationDao.Dao.activate
import workspace.Log.i

@Service
class SignupService(private val context: ApplicationContext) {
suspend fun signup(signup: Signup): Either<Throwable, User> = try {
suspend fun signup(signup: Signup): Either<Throwable, User> = try {
context.signupToUser(signup).run {
(this to context).signup()
.mapLeft { return Exception("Unable to save user with id").left() }
Expand Down Expand Up @@ -63,15 +65,49 @@ class SignupService(private val context: ApplicationContext) {
SIGNUP_LOGIN_NOT_AVAILABLE -> return signupProblems.badResponseLoginIsNotAvailable
SIGNUP_EMAIL_NOT_AVAILABLE -> return signupProblems.badResponseEmailIsNotAvailable
else -> {
signup(signup)
return CREATED.run(::ResponseEntity)
return signup(signup).run { CREATED.run(::ResponseEntity) }
}
}
}
SERVICE_UNAVAILABLE.run(::ResponseEntity)
}

suspend fun activateRequest(
key: String,
exchange: ServerWebExchange
) = key.run { activate(this) }

suspend fun activate(key: String): Long = context.activate(key)
.getOrElse { throw IllegalStateException("Error activating user with key: $key", it) }
.takeIf { it == ONE_ROW_UPADTED }
?: throw IllegalArgumentException("Activation failed: No user was activated for key: $key")

/*
@GetMapping(ACTIVATE_API)
@Throws(SignupException::class)
suspend fun activateAccount(@RequestParam(ACTIVATE_API_KEY) key: String) {
if (!signupService.accountByActivationKey(key).run no@{
return@no when {
this == null -> false.apply { i("no activation for key: $key") }
else -> signupService
.saveAccount(copy(activated = true, activationKey = null))
.run yes@{
return@yes when {
this != null -> true.apply { i("activation: $login") }
else -> false
}
}
}
})
//TODO: remplacer un ResponseEntity<ProblemDetail>
throw SignupException(MSG_WRONG_ACTIVATION_KEY)
}*/


companion object {
const val ONE_ROW_UPADTED = 1L

@JvmStatic
val SIGNUP_AVAILABLE = Triple(true, true, true)

Expand Down
7 changes: 5 additions & 2 deletions api/src/main/kotlin/users/signup/UserActivationDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ object UserActivationDao {
* If the Right value (the result of the database operation) is not equal to 1,
* then either the key doesn't exist, or the user is already activated.
*/
suspend fun ApplicationContext.activateUser(key: String): Either<Throwable, Long> = try {
suspend fun ApplicationContext.activate(key: String): Either<Throwable, Long> = try {
UPDATE_ACTIVATION_BY_KEY
.trimIndent()
.run(getBean<R2dbcEntityTemplate>().databaseClient::sql)
Expand All @@ -136,5 +136,8 @@ object UserActivationDao {
} catch (e: Throwable) {
e.left()
}
}
}/* context.activateUser(this)
.getOrElse { throw IllegalStateException("Error activating user with key: $key", it) }
.takeIf { it == ONE_ROW_UPADTED }
?: throw IllegalArgumentException("Activation failed: No user was activated for key: $key")*/
}
4 changes: 2 additions & 2 deletions api/src/test/kotlin/users/DaoTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ import users.signup.SignupService.Companion.SIGNUP_LOGIN_NOT_AVAILABLE
import users.signup.UserActivation
import users.signup.UserActivationDao
import users.signup.UserActivationDao.Attributes.ACTIVATION_KEY_ATTR
import users.signup.UserActivationDao.Dao.activateUser
import users.signup.UserActivationDao.Dao.activate
import users.signup.UserActivationDao.Dao.countUserActivation
import users.signup.UserActivationDao.Fields.ACTIVATION_DATE_FIELD
import users.signup.UserActivationDao.Fields.ACTIVATION_KEY_FIELD
Expand Down Expand Up @@ -730,7 +730,7 @@ class DaoTests {
"activation key : $second".run(::i)
// assertEquals(
// 1,
context.activateUser(second).getOrNull()!!
context.activate(second).getOrNull()!!
// )
assertEquals(this@counts.first + 1, context.countUsers())
assertEquals(this@counts.second + 1, context.countUserAuthority())
Expand Down

0 comments on commit b3e56c6

Please sign in to comment.