diff --git a/application/app/test_cli.py b/application/app/test_cli.py index 29f39d5..700fe26 100644 --- a/application/app/test_cli.py +++ b/application/app/test_cli.py @@ -12,7 +12,6 @@ INSTALL_PATH = "" - class TestCLI(unittest.TestCase): def test_greetings(self): print(f"test cli : greetings!") diff --git a/springboot/api/src/main/kotlin/webapp/users/User.kt b/springboot/api/src/main/kotlin/webapp/users/User.kt index 66fdd94..6e10355 100755 --- a/springboot/api/src/main/kotlin/webapp/users/User.kt +++ b/springboot/api/src/main/kotlin/webapp/users/User.kt @@ -131,9 +131,9 @@ data class User( .trimMargin() + //TODO: signup, findByEmailOrLogin, } - object Dao { val Pair.toJson: String get() = second.getBean().writeValueAsString(first) @@ -156,4 +156,19 @@ data class User( } } } + /** Account REST API URIs */ + object UserRestApis { + const val API_AUTHORITY = "/api/authorities" + const val API_USERS = "/api/users" + const val API_SIGNUP = "/signup" + const val API_SIGNUP_PATH = "$API_USERS$API_SIGNUP" + const val API_ACTIVATE = "/activate" + const val API_ACTIVATE_PATH = "$API_USERS$API_ACTIVATE?key=" + const val API_ACTIVATE_PARAM = "{activationKey}" + const val API_ACTIVATE_KEY = "key" + const val API_RESET_INIT = "/reset-password/init" + const val API_RESET_FINISH = "/reset-password/finish" + const val API_CHANGE = "/change-password" + const val API_CHANGE_PATH = "$API_USERS$API_CHANGE" + } } diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/Account.kt b/springboot/api/src/main/kotlin/webapp/users/signup/Account.kt new file mode 100755 index 0000000..d88d125 --- /dev/null +++ b/springboot/api/src/main/kotlin/webapp/users/signup/Account.kt @@ -0,0 +1,208 @@ +//@file:Suppress("unused") +// +//package webapp.users.signup +// +//import jakarta.validation.constraints.Email +//import jakarta.validation.constraints.NotBlank +//import jakarta.validation.constraints.Pattern +//import jakarta.validation.constraints.Size +//import org.springframework.web.server.ServerWebExchange +//import webapp.core.http.validator +//import java.time.Instant +//import java.util.* +// +///** +// * Représente l'account domain model sans le password +// */ +////TODO: add field enabled=false +//data class Account( +// val id: UUID? = null, +// @field:NotBlank +// @field:Pattern(regexp = LOGIN_REGEX) +// @field:Size(min = 1, max = 50) +// val login: String? = null, +// @field:Size(max = 50) +// val firstName: String? = null, +// @field:Size(max = 50) +// val lastName: String? = null, +// @field:Email +// @field:Size(min = 5, max = 254) +// val email: String? = null, +// @field:Size(max = 256) +// val imageUrl: String? = IMAGE_URL_DEFAULT, +// val activated: Boolean = false, +// @field:Size(min = 2, max = 10) +// val langKey: String? = null, +// val createdBy: String? = null, +// val createdDate: Instant? = null, +// val lastModifiedBy: String? = null, +// val lastModifiedDate: Instant? = null, +// val authorities: Set? = null +//){ +// companion object { +// const val IMAGE_URL_DEFAULT = "http://placehold.it/50x50" +// // Regex for acceptable logins +// const val LOGIN_REGEX = +// "^(?>[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*)|(?>[_.@A-Za-z0-9-]+)$" +// const val PASSWORD_MIN: Int = 4 +// const val PASSWORD_MAX: Int = 16 +// @JvmStatic +// val objectName: String = Account::class +// .java +// .simpleName +// .run { +// replaceFirst( +// first(), +// first().lowercaseChar() +// ) +// } +// const val LOGIN_FIELD = "login" +// const val PASSWORD_FIELD = "password" +// const val EMAIL_FIELD = "email" +// const val ACCOUNT_AUTH_USER_ID_FIELD = "userId" +// const val ACTIVATION_KEY_FIELD = "activationKey" +// const val RESET_KEY_FIELD = "resetKey" +// const val FIRST_NAME_FIELD = "firstName" +// const val LAST_NAME_FIELD = "lastName" +// } +//} +// +//fun Account.isActivated(): Boolean = activated +// +//fun Account.validate( +// fields: Set, +// //si le validateur est null alors injecter celui du context +// // mais faire cela a l'appel de la methode validate +// //faire une extension function getValidator(exchange: ServerWebExchange?=null) +// exchange: ServerWebExchange?, //Pair +// //si les deux sont null contruire soit meme le validator par defaut +//) = exchange.apply { +// if (this == null) return emptySet>() +//}!!.validator.run { +// fields.map { field -> +// field to validateProperty(this@validate, field) +// }.flatMap { violatedField -> +// violatedField.second.map { +// mapOf( +// "objectName" to Account.objectName, +// "field" to violatedField.first, +// "message" to it.message +// ) +// } +// }.toSet() +//} +// +////fun Account.validate( +//// fields: Set, +//// //si le validateur est null alors injecter celui du context +//// // mais faire cela a l'appel de la methode validate +//// //faire une extension function getValidator(exchange: ServerWebExchange?=null) +//// exchange: ServerWebExchange?, //Pair +//// //si les deux sont null contruire soit meme le validator par defaut +////) = exchange.apply { +//// if (this == null) return emptySet>() +////}!!.validator.run { +//// fields.map { field -> +//// field to validateProperty(this@validate, field) +//// }.flatMap { violatedField -> +//// violatedField.second.map { +//// mapOf( +//// "objectName" to Account.objectName, +//// "field" to violatedField.first, +//// "message" to it.message +//// ) +//// } +//// }.toSet() +////} +// +// +////@file:Suppress("unused") +//// +////package community.accounts +//// +////import community.core.http.validator +////import jakarta.validation.constraints.Email +////import jakarta.validation.constraints.NotBlank +////import jakarta.validation.constraints.Pattern +////import jakarta.validation.constraints.Size +////import org.springframework.web.server.ServerWebExchange +////import java.time.Instant +////import java.util.* +//// +/////** +//// * Représente l'account domain model avec le password et l'activationKey +//// * pour la vue +//// */ +////data class Account( +//// val id: UUID? = null, +//// @field:NotBlank +//// @field:Pattern(regexp = LOGIN_REGEX) +//// @field:Size(min = 1, max = 50) +//// val login: String? = null, +//// @field:Email +//// @field:Size(min = 5, max = 254) +//// val email: String? = null, +//// @field:Size(max = 50) +//// val firstName: String? = null, +//// @field:Size(max = 50) +//// val lastName: String? = null, +//// @field:Size(min = 2, max = 10) +//// val langKey: String? = null, +//// @field:Size(max = 256) +//// val imageUrl: String? = IMAGE_URL_DEFAULT, +//// val createdBy: String? = null, +//// val createdDate: Instant? = null, +//// val lastModifiedBy: String? = null, +//// val lastModifiedDate: Instant? = null, +//// val authorities: Set? = null +////) { +//// companion object { +//// const val IMAGE_URL_DEFAULT = "http://placehold.it/50x50" +//// // Regex for acceptable logins +//// const val LOGIN_REGEX = +//// "^(?>[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*)|(?>[_.@A-Za-z0-9-]+)$" +//// const val PASSWORD_MIN: Int = 4 +//// const val PASSWORD_MAX: Int = 16 +//// @JvmStatic +//// val objectName: String = Account::class +//// .java +//// .simpleName +//// .run { +//// replaceFirst( +//// first(), +//// first().lowercaseChar() +//// ) +//// } +//// const val LOGIN_FIELD = "login" +//// const val PASSWORD_FIELD = "password" +//// const val EMAIL_FIELD = "email" +//// const val ACCOUNT_AUTH_USER_ID_FIELD = "userId" +//// const val ACTIVATION_KEY_FIELD = "activationKey" +//// const val RESET_KEY_FIELD = "resetKey" +//// const val FIRST_NAME_FIELD = "firstName" +//// const val LAST_NAME_FIELD = "lastName" +//// } +////} +//// +////fun Account.validate( +//// fields: Set, +//// //si le validateur est null alors injecter celui du context +//// // mais faire cela a l'appel de la methode validate +//// //faire une extension function getValidator(exchange: ServerWebExchange?=null) +//// exchange: ServerWebExchange?, //Pair +//// //si les deux sont null contruire soit meme le validator par defaut +////) = exchange.apply { +//// if (this == null) return emptySet>() +////}!!.validator.run { +//// fields.map { field -> +//// field to validateProperty(this@validate, field) +//// }.flatMap { violatedField -> +//// violatedField.second.map { +//// mapOf( +//// "objectName" to Account.objectName, +//// "field" to violatedField.first, +//// "message" to it.message +//// ) +//// } +//// }.toSet() +////} \ No newline at end of file diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/AccountCredentials.kt b/springboot/api/src/main/kotlin/webapp/users/signup/AccountCredentials.kt new file mode 100755 index 0000000..55e7135 --- /dev/null +++ b/springboot/api/src/main/kotlin/webapp/users/signup/AccountCredentials.kt @@ -0,0 +1,85 @@ +//@file:Suppress("unused") +// +//package webapp.users.signup +// +//import jakarta.validation.constraints.* +//import webapp.core.property.IMAGE_URL_DEFAULT +//import webapp.core.property.LOGIN_REGEX +//import webapp.core.property.PASSWORD_MAX +//import webapp.core.property.PASSWORD_MIN +//import webapp.accounts.models.AccountUtils.objectName +//import java.time.Instant +//import java.util.* +// +///** +// * Représente l'account domain model avec le password et l'activationKey +// * pour la vue +// */ +//data class AccountCredentials( +// val id: UUID? = null, +// @field:Size(min = PASSWORD_MIN, max = PASSWORD_MAX) +// @field:NotNull +// val password: String? = null, +// @field:NotBlank +// @field:Pattern(regexp = LOGIN_REGEX) +// @field:Size(min = 1, max = 50) +// val login: String? = null, +// @field:Email +// @field:Size(min = 5, max = 254) +// val email: String? = null, +// @field:Size(max = 50) +// val firstName: String? = null, +// @field:Size(max = 50) +// val lastName: String? = null, +// @field:Size(min = 2, max = 10) +// val langKey: String? = null, +// @field:Size(max = 256) +// val imageUrl: String? = IMAGE_URL_DEFAULT, +// val activationKey: String? = null, +// val resetKey: String? = null, +// val activated: Boolean = false, +// val createdBy: String? = null, +// val createdDate: Instant? = null, +// val lastModifiedBy: String? = null, +// val lastModifiedDate: Instant? = null, +// val authorities: Set? = null +//) { +// +// constructor(account: Account) : this( +// id = account.id, +// login = account.login, +// email = account.email, +// firstName = account.firstName, +// lastName = account.lastName, +// langKey = account.langKey, +// activated = account.activated, +// createdBy = account.createdBy, +// createdDate = account.createdDate, +// lastModifiedBy = account.lastModifiedBy, +// lastModifiedDate = account.lastModifiedDate, +// imageUrl = account.imageUrl, +// authorities = account.authorities?.map { it }?.toMutableSet() +// ) +// +// companion object { +// @JvmStatic +// val objectName = AccountCredentials::class.java.simpleName.objectName +// } +//} +// +// +//fun AccountCredentials.toAccount(): Account = Account( +// id = id, +// login = login, +// firstName = firstName, +// lastName = lastName, +// email = email, +// activated = activated, +// langKey = langKey, +// createdBy = createdBy, +// createdDate = createdDate, +// lastModifiedBy = lastModifiedBy, +// lastModifiedDate = lastModifiedDate, +// authorities = authorities +//) +// diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/AccountExtra.kt b/springboot/api/src/main/kotlin/webapp/users/signup/AccountExtra.kt new file mode 100755 index 0000000..f0d1a9c --- /dev/null +++ b/springboot/api/src/main/kotlin/webapp/users/signup/AccountExtra.kt @@ -0,0 +1,17 @@ +//package webapp.users.signup +// +//import java.time.Instant +//import java.util.* +// +//data class AccountExtra( +// val password: String, +// val enabled: Boolean = false, +// val activationKey: String?, +// val resetKey: String? = null, +// val resetDate: Instant? = null, +// val createdBy: UUID, +// val lastModifiedBy: UUID, +//) +// +// +// diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/AccountFull.kt b/springboot/api/src/main/kotlin/webapp/users/signup/AccountFull.kt new file mode 100755 index 0000000..30be993 --- /dev/null +++ b/springboot/api/src/main/kotlin/webapp/users/signup/AccountFull.kt @@ -0,0 +1,7 @@ +//package webapp.users.signup +// +//data class AccountFull( +// val account: Account, +// val accountExtra: AccountExtra, +//) +// \ No newline at end of file diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/AccountRestApis.kt b/springboot/api/src/main/kotlin/webapp/users/signup/AccountRestApis.kt deleted file mode 100755 index 29f5917..0000000 --- a/springboot/api/src/main/kotlin/webapp/users/signup/AccountRestApis.kt +++ /dev/null @@ -1,17 +0,0 @@ -package webapp.users.signup - -/** Account REST API URIs */ -object AccountRestApis { - const val API_AUTHORITY = "/api/authorities" - const val API_ACCOUNT = "/api/accounts" - const val API_SIGNUP = "/signup" - const val API_SIGNUP_PATH = "$API_ACCOUNT$API_SIGNUP" - const val API_ACTIVATE = "/activate" - const val API_ACTIVATE_PATH = "$API_ACCOUNT$API_ACTIVATE?key=" - const val API_ACTIVATE_PARAM = "{activationKey}" - const val API_ACTIVATE_KEY = "key" - const val API_RESET_INIT = "/reset-password/init" - const val API_RESET_FINISH = "/reset-password/finish" - const val API_CHANGE = "/change-password" - const val API_CHANGE_PATH = "$API_ACCOUNT$API_CHANGE" -} \ No newline at end of file diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/AccountUtils.kt b/springboot/api/src/main/kotlin/webapp/users/signup/AccountUtils.kt new file mode 100755 index 0000000..a5e72bc --- /dev/null +++ b/springboot/api/src/main/kotlin/webapp/users/signup/AccountUtils.kt @@ -0,0 +1,35 @@ +//package webapp.users.signup +// +//import org.apache.commons.lang3.RandomStringUtils +//import java.security.SecureRandom +// +//object AccountUtils { +// private const val DEF_COUNT = 20 +// private val SECURE_RANDOM: SecureRandom by lazy { +// SecureRandom().apply { nextBytes(ByteArray(size = 64)) } +// } +// +// private val generateRandomAlphanumericString: String +// get() = RandomStringUtils.random( +// DEF_COUNT, +// 0, +// 0, +// true, +// true, +// null, +// SECURE_RANDOM +// ) +// +// @Suppress("unused") +// val generatePassword: String +// get() = generateRandomAlphanumericString +// +// val generateActivationKey: String +// get() = generateRandomAlphanumericString +// +// val generateResetKey: String +// get() = generateRandomAlphanumericString +// +// val String.objectName get() = replaceFirst(first(), first().lowercaseChar()) +// +//} \ No newline at end of file diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/Avatar.kt b/springboot/api/src/main/kotlin/webapp/users/signup/Avatar.kt new file mode 100755 index 0000000..e012012 --- /dev/null +++ b/springboot/api/src/main/kotlin/webapp/users/signup/Avatar.kt @@ -0,0 +1,11 @@ +//package webapp.users.signup +// +//import java.util.* +// +///** +// * Représente l'account domain model minimaliste pour la view +// */ +//data class Avatar( +// val id: UUID? = null, +// val login: String? = null +//) \ No newline at end of file diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/KeyAndPassword.kt b/springboot/api/src/main/kotlin/webapp/users/signup/KeyAndPassword.kt new file mode 100755 index 0000000..2b737cb --- /dev/null +++ b/springboot/api/src/main/kotlin/webapp/users/signup/KeyAndPassword.kt @@ -0,0 +1,6 @@ +//package webapp.users.signup +// +//data class KeyAndPassword( +// val key: String? = null, +// val newPassword: String? = null +//) \ No newline at end of file diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/Login.kt b/springboot/api/src/main/kotlin/webapp/users/signup/Login.kt new file mode 100755 index 0000000..fc29790 --- /dev/null +++ b/springboot/api/src/main/kotlin/webapp/users/signup/Login.kt @@ -0,0 +1,17 @@ +//package webapp.users.signup +// +//import jakarta.validation.constraints.NotNull +//import jakarta.validation.constraints.Size +// +///*=================================================================================*/ +//data class Login( +// @field:NotNull +// val username: +// @Size(min = 1, max = 50) +// String? = null, +// @field:NotNull +// @field:Size(min = 4, max = 100) +// val password: +// String? = null, +// val rememberMe: Boolean? = null +//) \ No newline at end of file diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/PasswordChange.kt b/springboot/api/src/main/kotlin/webapp/users/signup/PasswordChange.kt new file mode 100755 index 0000000..00bfb9f --- /dev/null +++ b/springboot/api/src/main/kotlin/webapp/users/signup/PasswordChange.kt @@ -0,0 +1,7 @@ +//package webapp.users.signup +// +///*=================================================================================*/ +//data class PasswordChange( +// val currentPassword: String? = null, +// val newPassword: String? = null +//) \ No newline at end of file diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/SignupController.kt b/springboot/api/src/main/kotlin/webapp/users/signup/SignupController.kt index bcd5b3e..7be30fb 100755 --- a/springboot/api/src/main/kotlin/webapp/users/signup/SignupController.kt +++ b/springboot/api/src/main/kotlin/webapp/users/signup/SignupController.kt @@ -1,16 +1,1033 @@ package webapp.users.signup +import org.springframework.http.HttpStatus +import org.springframework.http.HttpStatus.CREATED +import org.springframework.http.MediaType +import org.springframework.http.MediaType.APPLICATION_PROBLEM_JSON_VALUE +import org.springframework.http.ProblemDetail +import org.springframework.http.ResponseEntity +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 webapp.users.signup.AccountRestApis.API_ACCOUNT +import org.springframework.web.server.ServerWebExchange +import webapp.core.utils.i +import webapp.users.User.UserRestApis.API_SIGNUP +import webapp.users.User.UserRestApis.API_USERS @RestController -@RequestMapping(API_ACCOUNT) +@RequestMapping(API_USERS) class SignupController { internal class SignupException(message: String) : RuntimeException(message) - object Signup { - const val API_ACCOUNT = "/api/account" - const val SIGNUP_API_PATH = "$API_ACCOUNT/signup" + +// /** +// * {@code POST /signup} : register the user. +// * +// * @param account the managed user View Model. +// */ +// @PostMapping( +// API_SIGNUP, +// produces = [APPLICATION_PROBLEM_JSON_VALUE] +// ) +// suspend fun signup( +// @RequestBody account: AccountCredentials, +// exchange: ServerWebExchange +// ): ResponseEntity = account.validate(exchange).run { +// i("signup attempt: ${this@run} ${account.login} ${account.email}") +// if (isNotEmpty()) return signupProblems.badResponse(this) +// }.run { +// when { +// account.loginIsNotAvailable(signupService) -> signupProblems.badResponseLoginIsNotAvailable +// account.emailIsNotAvailable(signupService) -> signupProblems.badResponseEmailIsNotAvailable +// else -> { +// signupService.signup(account) +// ResponseEntity(CREATED) +// } +// } +// } +} + + +/* +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ProblemDetail +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import org.springframework.web.server.ServerWebExchange +import webapp.* +import webapp.accounts.models.AccountCredentials +import webapp.core.http.badResponse +import webapp.core.http.validate +import webapp.core.logging.i +import webapp.core.property.* + + +@RestController +@RequestMapping(ACCOUNT_API) +class SignupController(private val signupService: SignupService) { + + internal class SignupException(message: String) : RuntimeException(message) + + /** + * {@code POST /signup} : register the user. + * + * @param account the managed user View Model. + */ + @PostMapping( + SIGNUP_API, + produces = [MediaType.APPLICATION_PROBLEM_JSON_VALUE] + ) + suspend fun signup( + @RequestBody account: AccountCredentials, + exchange: ServerWebExchange + ): ResponseEntity = account.validate(exchange).run { + i("signup attempt: ${this@run} ${account.login} ${account.email}") + if (isNotEmpty()) return signupProblems.badResponse(this) + }.run { + when { + account.loginIsNotAvailable(signupService) -> signupProblems.badResponseLoginIsNotAvailable + account.emailIsNotAvailable(signupService) -> signupProblems.badResponseEmailIsNotAvailable + else -> { + signupService.signup(account) + ResponseEntity(HttpStatus.CREATED) + } + } + } + + + /** + * `GET /activate` : activate the signed-up user. + * + * @param key the activation key. + * @throws RuntimeException `500 (Internal Application Error)` if the user couldn't be activated. + */ + @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 + throw SignupException(MSG_WRONG_ACTIVATION_KEY) + } + + +} + +//@file:Suppress("unused") +// +//package community.accounts +// +// +//import org.springframework.context.ApplicationContext +//import org.springframework.stereotype.Service +//import org.springframework.web.bind.annotation.* +//import java.util.* +// +// +///*=================================================================================*/ +////TODO: renommer management en accounts +// +////@RestController +////@RequestMapping("api") +////class AccountController( +//// private val accountService: AccountService +////) { +//// internal class AccountException(message: String) : RuntimeException(message) +//// +////// +////// /** +////// * `GET /authenticate` : check if the user is authenticated, and return its login. +////// * +////// * @param request the HTTP request. +////// * @return the login if the user is authenticated. +////// */ +////// @GetMapping("/authenticate") +////// suspend fun isAuthenticated(request: ServerWebExchange): String? = +////// request.getPrincipal().map(Principal::getName).awaitFirstOrNull().also { +////// d("REST request to check if the current user is authenticated") +////// } +////// +////// +////// /** +////// * {@code GET /account} : get the current user. +////// * +////// * @return the current user. +////// * @throws RuntimeException {@code 500 (Internal Application Error)} if the user couldn't be returned. +////// */ +////// @GetMapping("account") +////// suspend fun getAccount(): Account = i("controller getAccount").run { +////// userService.getUserWithAuthorities().run { +////// if (this == null) throw AccountException("User could not be found") +////// else return Account(user = this) +////// } +////// } +////// +////// /** +////// * {@code POST /account} : update the current user information. +////// * +////// * @param account the current user information. +////// * @throws EmailAlreadyUsedProblem {@code 400 (Bad Request)} if the email is already used. +////// * @throws RuntimeException {@code 500 (Internal Application Error)} if the user login wasn't found. +////// */ +////// @PostMapping("account") +////// suspend fun saveAccount(@Valid @RequestBody account: Account): Unit { +////// getCurrentUserLogin().apply principal@{ +////// if (isBlank()) throw AccountException("Current user login not found") +////// else { +////// userService.findAccountByEmail(account.email!!).apply { +////// if (!this?.login?.equals(this@principal, true)!!) +////// throw EmailAlreadyUsedException() +////// } +////// userService.findAccountByLogin(account.login!!).apply { +////// if (this == null) +////// throw AccountException("User could not be found") +////// } +////// userService.updateUser( +////// account.firstName, +////// account.lastName, +////// account.email, +////// account.langKey, +////// account.imageUrl +////// ) +////// } +////// } +////// } +////// +////// /** +////// * {@code POST /account/change-password} : changes the current user's password. +////// * +////// * @param passwordChange current and new password. +////// * @throws InvalidPasswordProblem {@code 400 (Bad Request)} if the new password is incorrect. +////// */ +////// @PostMapping("account/change-password") +////// suspend fun changePassword(@RequestBody passwordChange: PasswordChange): Unit = +////// passwordChange.run { +////// InvalidPasswordException().apply { if (isPasswordLengthInvalid(newPassword)) throw this } +////// if (currentPassword != null && newPassword != null) +////// userService.changePassword(currentPassword, newPassword) +////// } +////// +////// /** +////// * {@code POST /account/reset-password/init} : Send an email to reset the password of the user. +////// * +////// * @param mail the mail of the user. +////// */ +////// @PostMapping("account/reset-password/init") +////// suspend fun requestPasswordReset(@RequestBody mail: String): Unit = +////// userService.requestPasswordReset(mail).run { +////// if (this == null) log.warn("Password reset requested for non existing mail") +////// else mailService.sendPasswordResetMail(this) +////// } +////// +////// /** +////// * {@code POST /account/reset-password/finish} : Finish to reset the password of the user. +////// * +////// * @param keyAndPassword the generated key and the new password. +////// * @throws InvalidPasswordProblem {@code 400 (Bad Request)} if the password is incorrect. +////// * @throws RuntimeException {@code 500 (Internal Application Error)} if the password could not be reset. +////// */ +////// @PostMapping("account/reset-password/finish") +////// suspend fun finishPasswordReset(@RequestBody keyAndPassword: PasswordReset): Unit { +////// keyAndPassword.run { +////// InvalidPasswordException().apply { if (isPasswordLengthInvalid(newPassword)) throw this } +////// if (newPassword != null && key != null) +////// if (userService.completePasswordReset(newPassword, key) == null) +////// throw AccountException("No user was found for this reset key") +////// } +////// } +////} +// +///*=================================================================================*/ +// +// +///** +// * REST controller for managing users. +// *

+// * This class accesses the {@link User} entity, and needs to fetch its collection of authorities. +// *

+// * For a normal use-case, it would be better to have an eager relationship between User and Authority, +// * and send everything to the client side: there would be no View Model and DTO, a lot less code, and an outer-join +// * which would be good for performance. +// *

+// * We use a View Model and a DTO for 3 reasons: +// *

    +// *
  • We want to keep a lazy association between the user and the authorities, because people will +// * quite often do relationships with the user, and we don't want them to get the authorities all +// * the time for nothing (for performance reasons). This is the #1 goal: we should not impact our users' +// * application because of this use-case.
  • +// *
  • Not having an outer join causes n+1 requests to the database. This is not a real issue as +// * we have by default a second-level cache. This means on the first HTTP call we do the n+1 requests, +// * but then all authorities come from the cache, so in fact it's much better than doing an outer join +// * (which will get lots of data from the database, for each HTTP call).
  • +// *
  • As this manages users, for security reasons, we'd rather have a DTO layer.
  • +// *
+// *

+// * Another option would be to have a specific JPA entity graph to handle this case. +// */ +////@RestController +////@RequestMapping("api/admin") +////class AccountController( +//// private val userService: UserService, +//// private val mailService: MailService, +//// private val properties: Properties +////) { +//// companion object { +//// private val ALLOWED_ORDERED_PROPERTIES = +//// arrayOf( +//// "id", +//// "login", +//// "firstName", +//// "lastName", +//// "email", +//// "activated", +//// "langKey" +//// ) +//// } +//// +//// /** +//// * {@code POST /admin/users} : Creates a new user. +//// *

+//// * Creates a new user if the login and email are not already used, and sends an +//// * mail with an activation link. +//// * The user needs to be activated on creation. +//// * +//// * @param account the user to create. +//// * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new user, +//// * or with status {@code 400 (Bad Request)} if the login or email is already in use. +//// * @throws AlertProblem {@code 400 (Bad Request)} if the login or email is already in use. +//// */ +//// @PostMapping("users") +//// @org.springframework.security.access.prepost.PreAuthorize("hasAuthority(\"$ROLE_ADMIN\")") +//// suspend fun createUser(@Valid @RequestBody account: Account): ResponseEntity { +//// account.apply requestAccount@{ +//// d("REST request to save User : {}", account) +//// if (id != null) throw AlertProblem( +//// defaultMessage = "A new user cannot already have an ID", +//// entityName = "userManagement", +//// errorKey = "idexists" +//// ) +//// userService.findAccountByLogin(login!!).apply retrieved@{ +//// if (this@retrieved?.login?.equals( +//// this@requestAccount.login, +//// true +//// ) == true +//// ) throw LoginAlreadyUsedProblem() +//// } +//// userService.findAccountByEmail(email!!).apply retrieved@{ +//// if (this@retrieved?.email?.equals( +//// this@requestAccount.email, +//// true +//// ) == true +//// ) throw EmailAlreadyUsedProblem() +//// } +//// userService.createUser(this).apply { +//// mailService.sendActivationEmail(this) +//// try { +//// return created(URI("/api/admin/users/$login")) +//// .headers( +//// createAlert( +//// properties.clientApp.name, +//// "userManagement.created", +//// login +//// ) +//// ).body(this) +//// } catch (e: URISyntaxException) { +//// throw RuntimeException(e) +//// } +//// } +//// } +//// } +//// +//// /** +//// * {@code PUT /admin/users} : Updates an existing User. +//// * +//// * @param account the user to update. +//// * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated user. +//// * @throws EmailAlreadyUsedProblem {@code 400 (Bad Request)} if the email is already in use. +//// * @throws LoginAlreadyUsedProblem {@code 400 (Bad Request)} if the login is already in use. +//// */ +//// @PutMapping("/users") +//// @org.springframework.security.access.prepost.PreAuthorize("hasAuthority(\"$ROLE_ADMIN\")") +//// suspend fun updateUser(@Valid @RequestBody account: Account): ResponseEntity { +//// d("REST request to update User : {}", account) +//// userService.findAccountByEmail(account.email!!).apply { +//// if (this == null) throw ResponseStatusException(NOT_FOUND) +//// if (id != account.id) throw EmailAlreadyUsedProblem() +//// } +//// userService.findAccountByLogin(account.login!!).apply { +//// if (this == null) throw ResponseStatusException(NOT_FOUND) +//// if (id != account.id) throw LoginAlreadyUsedProblem() +//// } +//// return ok() +//// .headers( +//// createAlert( +//// properties.clientApp.name, +//// "userManagement.updated", +//// account.login +//// ) +//// ).body(userService.updateUser(account)) +//// } +//// +//// /** +//// * {@code GET /admin/users} : get all users with all the details - +//// * calling this are only allowed for the administrators. +//// * +//// * @param request a {@link ServerHttpRequest} request. +//// * @param pageable the pagination information. +//// * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body all users. +//// */ +//// @GetMapping("/users") +//// @org.springframework.security.access.prepost.PreAuthorize("hasAuthority(\"$ROLE_ADMIN\")") +//// suspend fun getAllUsers(request: ServerHttpRequest, pageable: Pageable): ResponseEntity> = +//// d("REST request to get all User for an admin").run { +//// return if (!onlyContainsAllowedProperties(pageable)) { +//// badRequest().build() +//// } else ok() +//// .headers( +//// generatePaginationHttpHeaders( +//// fromHttpRequest(request), +//// PageImpl( +//// mutableListOf(), +//// pageable, +//// userService.countUsers() +//// ) +//// ) +//// ).body(userService.getAllManagedUsers(pageable)) +//// } +//// +//// +//// private fun onlyContainsAllowedProperties(pageable: Pageable): Boolean = pageable +//// .sort +//// .stream() +//// .map(Order::getProperty) +//// .allMatch(ALLOWED_ORDERED_PROPERTIES::contains) +//// +//// +//// /** +//// * {@code GET /admin/users/:login} : get the "login" user. +//// * +//// * @param login the login of the user to find. +//// * @return the {@link ResponseEntity} with status {@code 200 (OK)} +//// * and with body the "login" user, or with status {@code 404 (Not Found)}. +//// */ +//// @GetMapping("/users/{login}") +//// @org.springframework.security.access.prepost.PreAuthorize("hasAuthority(\"$ROLE_ADMIN\")") +//// suspend fun getUser(@PathVariable login: String): Account = +//// d("REST request to get User : {}", login).run { +//// return Account(userService.getUserWithAuthoritiesByLogin(login).apply { +//// if (this == null) throw ResponseStatusException(NOT_FOUND) +//// }!!) +//// } +//// +//// /** +//// * {@code DELETE /admin/users/:login} : delete the "login" User. +//// * +//// * @param login the login of the user to delete. +//// * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. +//// */ +//// @DeleteMapping("/users/{login}") +//// @org.springframework.security.access.prepost.PreAuthorize("hasAuthority(\"$ROLE_ADMIN\")") +//// @ResponseStatus(code = NO_CONTENT) +//// suspend fun deleteUser( +//// @PathVariable @Pattern(regexp = LOGIN_REGEX) login: String +//// ): ResponseEntity { +//// d("REST request to delete User: {}", login).run { +//// userService.deleteUser(login).run { +//// return noContent().headers( +//// createAlert( +//// properties.clientApp.name, +//// "userManagement.deleted", +//// login +//// ) +//// ).build() +//// } +//// } +//// } +////} +// +///*=================================================================================*/ +// +// +///** +// * Controller to authenticate users. +// */ +// +////@RestController +////@RequestMapping("/api") +////@Suppress("unused") +////class AuthenticationController( +//// private val tokenProvider: TokenProvider, +//// private val authenticationManager: ReactiveAuthenticationManager +////) { +//// /** +//// * Object to return as body in Jwt Authentication. +//// */ +//// class JwtToken(@JsonProperty(AUTHORIZATION_ID_TOKEN) val idToken: String) +//// +//// @PostMapping("/authenticate") +//// suspend fun authorize(@Valid @RequestBody loginVm: Login) +//// : ResponseEntity = tokenProvider.createToken( +//// authenticationManager.authenticate( +//// UsernamePasswordAuthenticationToken( +//// loginVm.username, +//// loginVm.password +//// ) +//// ).awaitSingle(), loginVm.rememberMe!! +//// ).run { +//// return ResponseEntity( +//// JwtToken(idToken = this), +//// HttpHeaders().apply { +//// add( +//// AUTHORIZATION_HEADER, +//// "$BEARER_START_WITH$this" +//// ) +//// }, +//// OK +//// ) +//// } +////} +// +// +///*=================================================================================*/ +// +// +////import community.Application.Log.log +////import common.domain.Avatar +////import community.core.http.util.paginationUtils.generatePaginationHttpHeaders +//////import community.management.UserService +////import kotlinx.coroutines.flow.Flow +////import kotlinx.coroutines.flow.toCollection +////import org.springframework.data.domain.PageImpl +////import org.springframework.data.domain.Pageable +////import org.springframework.data.domain.Sort.Order +////import org.springframework.http.ResponseEntity +////import org.springframework.http.ResponseEntity.badRequest +////import org.springframework.http.ResponseEntity.ok +////import org.springframework.http.server.reactive.ServerHttpRequest +////import org.springframework.web.bind.annotation.GetMapping +////import org.springframework.web.bind.annotation.RequestMapping +////import org.springframework.web.bind.annotation.RestController +////import org.springframework.web.util.UriComponentsBuilder.fromHttpRequest +// +////@RestController +////@RequestMapping("/api") +////@Suppress("unused") +////class AvatarController( +//// private val userService: UserService +////) { +//// companion object { +//// private val ALLOWED_ORDERED_PROPERTIES = +//// arrayOf( +//// "id", +//// "login", +//// "firstName", +//// "lastName", +//// "email", +//// "activated", +//// "langKey" +//// ) +//// } +//// +//// /** +//// * {@code GET /users} : get all users with only the public informations - calling this are allowed for anyone. +//// * +//// * @param request a {@link ServerHttpRequest} request. +//// * @param pageable the pagination information. +//// * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body all users. +//// */ +//// @GetMapping("/users") +//// suspend fun getAllAvatars( +//// request: ServerHttpRequest, +//// pageable: Pageable +//// ): ResponseEntity> = log +//// .debug("REST request to get all public User names").run { +//// return if (!onlyContainsAllowedProperties(pageable)) badRequest().build() +//// else { +//// ok().headers( +//// generatePaginationHttpHeaders( +//// fromHttpRequest(request), +//// PageImpl( +//// mutableListOf(), +//// pageable, +//// userService.countUsers() +//// ) +//// ) +//// ).body(userService.getAvatars(pageable)) +//// } +//// } +//// +//// private fun onlyContainsAllowedProperties( +//// pageable: Pageable +//// ): Boolean = pageable +//// .sort +//// .stream() +//// .map(Order::getProperty) +//// .allMatch(ALLOWED_ORDERED_PROPERTIES::contains) +//// +//// /** +//// * Gets a list of all roles. +//// * @return a string list of all roles. +//// */ +//// @GetMapping("/authorities") +//// suspend fun getAuthorities(): List = userService +//// .getAuthorities() +//// .toCollection(mutableListOf()) +////} +///*=================================================================================*/ +// +// +// +// +// +//@Service("userService") +////@Suppress("unused") +//class UserService +// ( +//// private val passwordEncoder: PasswordEncoder, +//// private val userRepository: UserRepository, +//// private val iUserRepository: IUserRepository, +//// private val userRepositoryPageable: UserRepositoryPageable, +//// private val userAuthRepository: UserAuthRepository, +//// private val authorityRepository: AuthorityRepository +// private val context: ApplicationContext, +//) { +//// @PostConstruct +//// private fun init() = checkProfileLog(context) +//// +//// @Transactional +//// suspend fun activateRegistration(key: String): User? = +//// d("Activating user for activation key {}", key).run { +//// return@run iUserRepository.findOneByActivationKey(key).apply { +//// if (this != null) { +//// activated = true +//// activationKey = null +//// saveUser(user = this).run { +//// d("Activated user: {}", this) +//// } +//// } else d("No user found with activation key {}", key) +//// } +//// } +//// +//// +//// suspend fun completePasswordReset(newPassword: String, key: String): User? = +//// d("Reset user password for reset key {}", key).run { +//// userRepository.findOneByResetKey(key).apply { +//// return if (this != null && +//// resetDate?.isAfter(now().minusSeconds(86400)) == true +//// ) saveUser( +//// apply { +//// password = passwordEncoder.encode(newPassword) +//// resetKey = null +//// resetDate = null +//// }) +//// else null +//// } +//// } +//// +//// +//// @Transactional +//// suspend fun requestPasswordReset(mail: String): User? { +//// return userRepository +//// .findOneByEmail(mail) +//// .apply { +//// if (this != null && this.activated) { +//// resetKey = generateResetKey +//// resetDate = now() +//// saveUser(this) +//// } else return null +//// } +//// } +//// +//// @Transactional +//// suspend fun register(account: Account, password: String): User? = userRepository +//// .findOneByLogin(account.login!!) +//// ?.apply isActivatedOnCheckLogin@{ +//// if (!activated) return@isActivatedOnCheckLogin userRepository.delete(user = this) +//// else throw UsernameAlreadyUsedException() +//// } +//// .also { +//// userRepository.findOneByEmail(account.email!!) +//// ?.apply isActivatedOnCheckEmail@{ +//// if (!activated) return@isActivatedOnCheckEmail userRepository.delete(user = this) +//// else throw EmailAlreadyUsedException() +//// } +//// } +//// .apply { +//// return@register userRepository.save( +//// User( +//// login = account.login, +//// password = passwordEncoder.encode(password), +//// firstName = account.firstName, +//// lastName = account.lastName, +//// email = account.email, +//// imageUrl = account.imageUrl, +//// langKey = account.langKey, +//// activated = USER_INITIAL_ACTIVATED_VALUE, +//// activationKey = generateActivationKey, +//// authorities = mutableSetOf().apply { +//// add(AuthorityEntity(role = ROLE_USER)) +//// }) +//// ) +//// } +//// +//// @Transactional +//// suspend fun createUser(account: Account): User = +//// saveUser(account.toUser().apply { +//// password = passwordEncoder.encode(generatePassword) +//// resetKey = generateResetKey +//// resetDate = now() +//// activated = true +//// account.authorities?.map { +//// authorities?.remove(AuthorityEntity(it)) +//// authorityRepository.findById(it).apply auth@{ +//// if (this@auth != null) authorities!!.add(this@auth) +//// } +//// } +//// }).also { +//// d("Created Information for User: {}", it) +//// } +//// +//// +//// /** +//// * Update all information for a specific user, and return the modified user. +//// * +//// * @param account user to update. +//// * @return updated user. +//// */ +//// @Transactional +//// suspend fun updateUser(account: Account): Account = +//// if (account.id != null) account +//// else { +//// val user = iUserRepository.findById(account.id!!) +//// if (user == null) account +//// else Account(saveUser(user.apply { +//// login = account.login +//// firstName = account.firstName +//// lastName = account.lastName +//// email = account.email +//// imageUrl = account.imageUrl +//// activated = account.activated +//// langKey = account.langKey +//// if (!authorities.isNullOrEmpty()) { +//// account.authorities!!.forEach { +//// authorities?.remove(AuthorityEntity(it)) +//// authorityRepository.findById(it).apply auth@{ +//// if (this@auth != null) authorities!!.add(this@auth) +//// } +//// } +//// authorities!!.clear() +//// userAuthRepository.deleteAllUserAuthoritiesByUser(account.id!!) +//// } +//// }).also { +//// d("Changed Information for User: {}", it) +//// }) +//// } +//// +//// +//// @Transactional +//// suspend fun deleteUser(login: String): Unit = +//// userRepository.findOneByLogin(login).apply { +//// userRepository.delete(this!!) +//// }.run { d("Changed Information for User: $this") } +//// +//// /** +//// * Update basic information (first name, last name, email, language) for the current user. +//// * +//// * @param firstName first name of user. +//// * @param lastName last name of user. +//// * @param email email id of user. +//// * @param langKey language key. +//// * @param imageUrl image URL of user. +//// */ +//// @Transactional +//// suspend fun updateUser( +//// firstName: String?, +//// lastName: String?, +//// email: String?, +//// langKey: String?, +//// imageUrl: String? +//// ): Unit = securityUtils.getCurrentUserLogin().run { +//// userRepository.findOneByLogin(login = this)?.apply { +//// this.firstName = firstName +//// this.lastName = lastName +//// this.email = email +//// this.langKey = langKey +//// this.imageUrl = imageUrl +//// saveUser(user = this).also { +//// d("Changed Information for User: {}", it) +//// } +//// } +//// } +//// +//// +//// @Transactional +//// suspend fun saveUser(user: User): User = securityUtils.getCurrentUserLogin() +//// .run currentUserLogin@{ +//// user.apply user@{ +//// SYSTEM_USER.apply systemUser@{ +//// if (createdBy.isNullOrBlank()) { +//// createdBy = this@systemUser +//// lastModifiedBy = this@systemUser +//// } else lastModifiedBy = this@currentUserLogin +//// } +//// userRepository.save(this@user) +//// } +//// } +//// +//// @Transactional +//// suspend fun changePassword(currentClearTextPassword: String, newPassword: String) { +//// securityUtils.getCurrentUserLogin().apply { +//// if (!isNullOrBlank()) { +//// userRepository.findOneByLogin(this).apply { +//// if (this != null) { +//// if (!passwordEncoder.matches( +//// currentClearTextPassword, +//// password +//// ) +//// ) throw InvalidPasswordException() +//// else saveUser(this.apply { +//// password = passwordEncoder.encode(newPassword) +//// }).run { +//// d("Changed password for User: {}", this) +//// } +//// } +//// } +//// } +//// } +//// } +//// +//// +//// @Transactional(readOnly = true) +//// suspend fun getAllManagedUsers(pageable: Pageable): Flow = +//// userRepositoryPageable +//// .findAllByIdNotNull(pageable) +//// .asFlow() +//// .map { +//// Account( +//// userRepository.findOneWithAuthoritiesByLogin(it.login!!)!! +//// ) +//// } +//// +//// +//// @Transactional(readOnly = true) +//// suspend fun getAvatars(pageable: Pageable) +//// : Flow = userRepositoryPageable +//// .findAllByActivatedIsTrue(pageable) +//// .filter { it != null } +//// .map { Avatar(it) } +//// .asFlow() +//// +//// @Transactional(readOnly = true) +//// suspend fun countUsers(): Long = userRepository.count() +//// +//// @Transactional(readOnly = true) +//// suspend fun getUserWithAuthoritiesByLogin(login: String): User? = +//// userRepository.findOneByLogin(login) +//// +//// suspend fun findAccountByEmail(email: String): Account? = +//// Account(userRepository.findOneByEmail(email).apply { +//// if (this == null) return null +//// }!!) +//// +//// suspend fun findAccountByLogin(login: String): Account? = +//// Account(userRepository.findOneWithAuthoritiesByLogin(login).apply { +//// if (this == null) return null +//// }!!) +//// +//// /** +//// * Gets a list of all the authorities. +//// * @return a list of all the authorities. +//// */ +//// @Transactional(readOnly = true) +//// suspend fun getAuthorities(): Flow = +//// authorityRepository +//// .findAll() +//// .map { it.role } +//// +//// @Transactional(readOnly = true) +//// suspend fun getUserWithAuthorities(): User? = +//// securityUtils.getCurrentUserLogin().run { +//// return@run if (isNullOrBlank()) null +//// else userRepository +//// .findOneWithAuthoritiesByLogin(this) +//// } +//// +//// /** +//// * Not activated users should be automatically deleted after 3 days. +//// * +//// * +//// * This is scheduled to get fired everyday, at 01:00 (am). +//// */ +//// @Scheduled(cron = "0 0 1 * * ?") +//// fun removeNotActivatedUsers() { +//// runBlocking { +//// removeNotActivatedUsersReactively() +//// .collect() +//// } +//// } +//// +//// @Transactional +//// suspend fun removeNotActivatedUsersReactively(): Flow = userRepository +//// .findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore( +//// ofInstant( +//// now().minus(3, DAYS), +//// UTC +//// ) +//// ).map { +//// it.apply { +//// userRepository.delete(this).also { +//// d("Deleted User: {}", this) +//// } +//// } +//// } +//} +// +///*=================================================================================*/ + +/* +package community.accounts.signup + +import community.* +import community.accounts.Account +import community.accounts.Account.Companion.EMAIL_FIELD +import community.accounts.Account.Companion.FIRST_NAME_FIELD +import community.accounts.Account.Companion.LAST_NAME_FIELD +import community.accounts.Account.Companion.LOGIN_FIELD +import community.accounts.Account.Companion.PASSWORD_FIELD +import community.accounts.validate +import community.core.http.ProblemsModel +import community.core.http.badResponse +import community.core.http.serverErrorResponse +import community.core.logging.d +import org.springframework.http.HttpStatus.CREATED +import org.springframework.http.HttpStatus.OK +import org.springframework.http.ProblemDetail +import org.springframework.http.ResponseEntity +import org.springframework.web.server.ServerWebExchange + +data class Signup( + val account: Account, + val password: String? = null, + val repass: String? = null, + val activationKey: String? = null, +) + +val signupFields by lazy { + setOf( + PASSWORD_FIELD, + EMAIL_FIELD, + LOGIN_FIELD, + FIRST_NAME_FIELD, + LAST_NAME_FIELD + ) +} +val Signup.logSignupComplete get() = d("activation link: $BASE_URL_DEV$API_ACTIVATE_PATH$activationKey") +val Account.logSignupAttempt get() = d("signup attempt: $login $email") +val Account.logResetAttempt get() = d("reset attempt: $email") + +suspend fun signup( + signup: Signup, + exchange: ServerWebExchange, + service: SignupService +): ResponseEntity = (signup + .account.apply { logSignupAttempt } + .validate(signupFields, exchange) to + validationProblems.copy(path = "$API_ACCOUNT$API_SIGNUP")).run { + when { + first.isNotEmpty() -> second.badResponse(first) + signup.account.loginIsNotAvailable(service) -> second.badResponseLoginIsNotAvailable + signup.account.emailIsNotAvailable(service) -> second.badResponseEmailIsNotAvailable + else -> try { + service.signup(signup)?.logSignupComplete + ResponseEntity(CREATED) + } catch (e: Exception) { + serverErrorProblems + .copy(path = "$API_ACCOUNT$API_SIGNUP") + .serverErrorResponse(e.message!!) + } + } +} + +suspend fun Pair.idsIsNotAvailable( + service: SignupService +): Boolean = @Suppress("KotlinConstantConditions") +when { + !second -> service.deleteAccount(first).run { false } + + else -> true +} + +val ProblemsModel.badResponseLoginIsNotAvailable + : ResponseEntity + get() = badResponse( + setOf( + mapOf( + "objectName" to Account.objectName, + "field" to LOGIN_FIELD, + "message" to "Login name already used!" + ) + ) + ) +val ProblemsModel.badResponseEmailIsNotAvailable + : ResponseEntity + get() = badResponse( + setOf( + mapOf( + "objectName" to Account.objectName, + "field" to EMAIL_FIELD, + "message" to "Email is already in use!" + ) + ) + ) + +suspend fun Account.emailIsNotAvailable( + service: SignupService +) = service + .pairAccountActivatedById(email!!) + ?.idsIsNotAvailable(service) + ?: false + +suspend fun activate(key: String, service: SignupService): ResponseEntity { + serverErrorProblems.copy(path = "$API_ACCOUNT$API_ACTIVATE").also { + return when (val account = service.accountByActivationKey(key)) { + null -> { + d("no activation for key: $key") + it.serverErrorResponse(MSG_WRONG_ACTIVATION_KEY) + } + + else -> try { + service.activate(account to key).run { + d("activation: ${account.login}") + ResponseEntity(OK) + } + } catch (e: Exception) { + it.serverErrorResponse(e.message!!) + } + } } } +suspend fun Account.loginIsNotAvailable( + service: SignupService +) = service + .pairAccountActivatedById(login!!) + ?.idsIsNotAvailable(service) + ?: false + + +/*=================================================================================*/ + + */ +*/ diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/SignupService.kt b/springboot/api/src/main/kotlin/webapp/users/signup/SignupService.kt index 5e54c21..42c206d 100755 --- a/springboot/api/src/main/kotlin/webapp/users/signup/SignupService.kt +++ b/springboot/api/src/main/kotlin/webapp/users/signup/SignupService.kt @@ -4,4 +4,139 @@ import org.springframework.context.ApplicationContext import org.springframework.stereotype.Service @Service -class SignupService(private val context: ApplicationContext) \ No newline at end of file +class SignupService(private val context: ApplicationContext) + +/* +package webapp.accounts.signup + +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import webapp.core.property.ACTIVATE_API_PATH +import webapp.core.property.BASE_URL_DEV +import webapp.core.property.ROLE_USER +import webapp.core.property.SYSTEM_USER +import webapp.accounts.models.Account +import webapp.accounts.models.AccountCredentials +import webapp.accounts.models.AccountUtils +import webapp.accounts.repository.AccountRepository +import webapp.core.logging.i +import webapp.core.mail.MailService +import java.time.Instant +import java.util.* + +@Service +class SignupService( + private val accountRepository: AccountRepository, + private val mailService: MailService, + private val passwordEncoder: PasswordEncoder +) { + @Transactional + suspend fun signup(account: AccountCredentials) = Instant.now().run { + accountRepository.signup( + account.copy( + password = passwordEncoder.encode(account.password), + activationKey = AccountUtils.generateActivationKey, + authorities = setOf(ROLE_USER), + langKey = when { + account.langKey.isNullOrBlank() -> Locale.ENGLISH.language + else -> account.langKey + }, + activated = false, + createdBy = SYSTEM_USER, + createdDate = this, + lastModifiedBy = SYSTEM_USER, + lastModifiedDate = this + ).apply { i("activation link: $BASE_URL_DEV$ACTIVATE_API_PATH$activationKey") } + ) + mailService.sendActivationEmail(account) + } + + + @Transactional(readOnly = true) + suspend fun accountByActivationKey(key: String) = accountRepository.findOneByActivationKey(key) + + @Transactional(readOnly = true) + suspend fun accountById(emailOrLogin: String) = accountRepository.findOne(emailOrLogin) + + @Transactional + suspend fun saveAccount(account: AccountCredentials) = accountRepository.save(account) + + @Transactional + suspend fun deleteAccount(account: Account) = accountRepository.delete(account) +} + +/* +package community.accounts.signup + +import community.accounts.Account + +interface SignupService { + suspend fun signup(signup: Signup): Signup? + suspend fun accountByActivationKey(key: String): Account? + suspend fun pairAccountActivatedById(emailOrLogin: String): Pair? + suspend fun saveAccount(account: Account): Account? + suspend fun deleteAccount(account: Account) + suspend fun activate(accountActivationKey: Pair) + suspend fun checkSystemAvailable() +} + */ + +/* +@file:Suppress("unused") + +package community.accounts.signup + +import community.accounts.Account +import community.accounts.AccountRepository +import community.accounts.mail.MailService +import jakarta.annotation.PostConstruct +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.Instant.now + +@Service +class SignupServiceImpl( + private val accountRepository: AccountRepository, + private val mailService: MailService, + private val passwordEncoder: PasswordEncoder +) : SignupService { + @Transactional + override suspend fun signup(signup: Signup): Signup? = now().run { + accountRepository.save(signup)?.also { + mailService.sendActivationEmail(it.account) + } + } + + @PostConstruct + override suspend fun checkSystemAvailable() { + //TODO: check if system account if available if not create it + // if profile is not dev check if admin and user is created then create it + } + + override suspend fun activate(accountActivationKey: Pair) { + + TODO("Not yet implemented") + } + + @Transactional(readOnly = true) + override suspend fun accountByActivationKey(key: String): Account? = + accountRepository.findOneByActivationKey(key) + + @Transactional(readOnly = true) + override suspend fun pairAccountActivatedById(emailOrLogin: String): Pair? = + accountRepository.findOne(emailOrLogin) + ?.let { + it.first to (it.second.activationKey?.isNotBlank() ?: false) + } + + @Transactional + override suspend fun saveAccount(account: Account): Account? = accountRepository.save(account)?.first + + @Transactional + override suspend fun deleteAccount(account: Account) = accountRepository.delete(account) + +} + */ +*/ \ No newline at end of file diff --git a/springboot/api/src/main/kotlin/webapp/users/signup/SignupUtils.kt b/springboot/api/src/main/kotlin/webapp/users/signup/SignupUtils.kt new file mode 100755 index 0000000..94bbc9f --- /dev/null +++ b/springboot/api/src/main/kotlin/webapp/users/signup/SignupUtils.kt @@ -0,0 +1,59 @@ +//package webapp.users.signup +// +//import webapp.* +//import webapp.accounts.models.AccountCredentials +//import webapp.accounts.models.toAccount +//import webapp.core.http.ProblemsModel +//import webapp.core.http.badResponse +//import webapp.core.property.* +// +//@JvmField +//val signupProblems = defaultProblems.copy(path = "$ACCOUNT_API$SIGNUP_API") +// +//val ProblemsModel.badResponseLoginIsNotAvailable +// get() = badResponse( +// setOf( +// mapOf( +// "objectName" to AccountCredentials.objectName, +// "field" to LOGIN_FIELD, +// "message" to "Login name already used!" +// ) +// ) +// ) +//val ProblemsModel.badResponseEmailIsNotAvailable +// get() = badResponse( +// setOf( +// mapOf( +// "objectName" to AccountCredentials.objectName, +// "field" to EMAIL_FIELD, +// "message" to "Email is already in use!" +// ) +// ) +// ) +// +// +//suspend fun AccountCredentials.loginIsNotAvailable(signupService: SignupService) = +// signupService.accountById(login!!).run { +// if (this == null) return@run false +// return when { +// !activated -> { +// signupService.deleteAccount(toAccount()) +// false +// } +// +// else -> true +// } +// } +// +//suspend fun AccountCredentials.emailIsNotAvailable(signupService: SignupService) = +// signupService.accountById(email!!).run { +// if (this == null) return@run false +// return when { +// !activated -> { +// signupService.deleteAccount(toAccount()) +// false +// } +// +// else -> true +// } +// } diff --git a/springboot/api/src/test/kotlin/webapp/signup/SignupIntegrationTests.kt b/springboot/api/src/test/kotlin/webapp/signup/SignupIntegrationTests.kt index 126ce6e..cbe87dd 100755 --- a/springboot/api/src/test/kotlin/webapp/signup/SignupIntegrationTests.kt +++ b/springboot/api/src/test/kotlin/webapp/signup/SignupIntegrationTests.kt @@ -59,7 +59,7 @@ import webapp.tests.TestUtils.deleteAllUsersOnly import webapp.users.User.UserDao.Fields.EMAIL_FIELD import webapp.users.User.UserDao.Fields.LOGIN_FIELD import webapp.users.User.UserDao.Fields.PASSWORD_FIELD -import webapp.users.signup.SignupController.Signup.SIGNUP_API_PATH +import webapp.users.User.UserRestApis.API_SIGNUP_PATH import kotlin.test.* @SpringBootTest(properties = ["spring.main.web-application-type=reactive"]) @@ -120,7 +120,7 @@ class SignupIntegrationTests { } } - + @Ignore @Test //TODO: mock sendmail fun `SignupController - test signup avec un account valide`(): Unit = runBlocking { val countUserBefore = context.countUsers() @@ -129,7 +129,7 @@ class SignupIntegrationTests { assertEquals(0, countUserAuthBefore) client .post() - .uri(SIGNUP_API_PATH) + .uri(API_SIGNUP_PATH) .contentType(APPLICATION_JSON) .bodyValue(user) .exchange() @@ -137,9 +137,9 @@ class SignupIntegrationTests { .isCreated .returnResult() .responseBodyContent!! + .logBody() .isEmpty() .let(::assertTrue) -// .run { assertTrue(this) } // assertEquals(countUserBefore + 1, countAccount(dao)) // assertEquals(countUserAuthBefore + 1, countAccountAuthority(dao)) // findOneByEmail(defaultAccount.email!!, dao).run {