added basic create account

- add link in to creat in users.html
- AccountController
  - add getCreateAccount
  - add postCreateAccount
- add create.html
- add AccountForm to Account Request in Mapping.kt
- add AccountForm
- add addAccount to AccountRegistryService
- add accountRegistryCreate to WebClientCalls.kt
- add UsernameDuplicateException
- add AccountRegistryException
- add HlaejaException
This commit is contained in:
2025-01-27 11:39:04 +01:00
parent 34349653db
commit 8e65de0350
11 changed files with 240 additions and 6 deletions

View File

@@ -35,7 +35,7 @@ class SecurityConfiguration {
.anyExchange().authenticated() .anyExchange().authenticated()
private fun AuthorizeExchangeSpec.adminPaths(): AuthorizeExchangeSpec.Access = pathMatchers( private fun AuthorizeExchangeSpec.adminPaths(): AuthorizeExchangeSpec.Access = pathMatchers(
"/account/**" "/account/**",
) )
private fun AuthorizeExchangeSpec.publicPaths(): AuthorizeExchangeSpec.Access = pathMatchers( private fun AuthorizeExchangeSpec.publicPaths(): AuthorizeExchangeSpec.Access = pathMatchers(

View File

@@ -1,11 +1,16 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import ltd.hlaeja.dto.Pagination import ltd.hlaeja.dto.Pagination
import ltd.hlaeja.exception.UsernameDuplicateException
import ltd.hlaeja.form.AccountForm
import ltd.hlaeja.service.AccountRegistryService import ltd.hlaeja.service.AccountRegistryService
import ltd.hlaeja.util.toAccountRequest
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.ui.Model import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -19,6 +24,32 @@ class AccountController(
const val DEFAULT_SIZE: Int = 25 const val DEFAULT_SIZE: Int = 25
} }
@GetMapping("/create")
fun getCreateAccount(
model: Model,
): Mono<String> = Mono.just("account/create")
.doOnNext { model.addAttribute("accountForm", AccountForm("", "", "", "", true)) }
@PostMapping("/create")
fun postCreateAccount(
@ModelAttribute("accountForm") accountForm: AccountForm,
model: Model,
): Mono<String> {
return accountRegistryService.addAccount(accountForm.toAccountRequest())
.map {
model.addAttribute("success", true)
"redirect:/account"
}
.onErrorResume { error ->
val errorMessage = when (error) {
is UsernameDuplicateException -> "Username already exists. Please choose another."
else -> "An unexpected error occurred. Please try again later."
}
model.addAttribute("errorMessage", errorMessage)
Mono.just("account/create")
}
}
@GetMapping @GetMapping
fun getDefaultAccounts( fun getDefaultAccounts(
model: Model, model: Model,
@@ -49,4 +80,3 @@ class AccountController(
} }
.then(Mono.just("account/users")) .then(Mono.just("account/users"))
} }

View File

@@ -0,0 +1,23 @@
package ltd.hlaeja.exception
@Suppress("unused")
open class AccountRegistryException : HlaejaException {
constructor() : super()
constructor(message: String) : super(message)
constructor(cause: Throwable) : super(cause)
constructor(
message: String,
cause: Throwable,
) : super(message, cause)
constructor(
message: String,
cause: Throwable,
enableSuppression: Boolean,
writableStackTrace: Boolean,
) : super(message, cause, enableSuppression, writableStackTrace)
}

View File

@@ -0,0 +1,23 @@
package ltd.hlaeja.exception
@Suppress("unused")
open class HlaejaException : RuntimeException {
constructor() : super()
constructor(message: String) : super(message)
constructor(cause: Throwable) : super(cause)
constructor(
message: String,
cause: Throwable,
) : super(message, cause)
constructor(
message: String,
cause: Throwable,
enableSuppression: Boolean,
writableStackTrace: Boolean,
) : super(message, cause, enableSuppression, writableStackTrace)
}

View File

@@ -0,0 +1,23 @@
package ltd.hlaeja.exception
@Suppress("unused")
open class UsernameDuplicateException : AccountRegistryException {
constructor() : super()
constructor(message: String) : super(message)
constructor(cause: Throwable) : super(cause)
constructor(
message: String,
cause: Throwable,
) : super(message, cause)
constructor(
message: String,
cause: Throwable,
enableSuppression: Boolean,
writableStackTrace: Boolean,
) : super(message, cause, enableSuppression, writableStackTrace)
}

View File

@@ -0,0 +1,9 @@
package ltd.hlaeja.form
data class AccountForm(
val username: String,
val password: CharSequence,
val passwordConfirm: CharSequence,
val role: String,
val enabled: Boolean = false,
)

View File

@@ -1,11 +1,13 @@
package ltd.hlaeja.service package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import ltd.hlaeja.exception.AccountRegistryException
import ltd.hlaeja.library.accountRegistry.Account import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.library.accountRegistry.Authentication import ltd.hlaeja.library.accountRegistry.Authentication
import ltd.hlaeja.property.AccountRegistryProperty import ltd.hlaeja.property.AccountRegistryProperty
import ltd.hlaeja.util.accountRegistryAccounts import ltd.hlaeja.util.accountRegistryAccounts
import ltd.hlaeja.util.accountRegistryAuthenticate import ltd.hlaeja.util.accountRegistryAuthenticate
import ltd.hlaeja.util.accountRegistryCreate
import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.BAD_REQUEST
import org.springframework.security.authentication.AuthenticationServiceException import org.springframework.security.authentication.AuthenticationServiceException
import org.springframework.security.core.AuthenticationException import org.springframework.security.core.AuthenticationException
@@ -54,4 +56,14 @@ class AccountRegistryService(
size: Int, size: Int,
): Flux<Account.Response> = webClient.accountRegistryAccounts(page, size, property) ): Flux<Account.Response> = webClient.accountRegistryAccounts(page, size, property)
.onErrorResume { error -> Flux.error(ResponseStatusException(BAD_REQUEST, error.message, error)) } .onErrorResume { error -> Flux.error(ResponseStatusException(BAD_REQUEST, error.message, error)) }
fun addAccount(
request: Account.Request,
): Mono<Account.Response> = webClient.accountRegistryCreate(request, property)
.onErrorResume { error ->
when (error) {
is AccountRegistryException -> Mono.error(error)
else -> Mono.error(ResponseStatusException(BAD_REQUEST, error.message))
}
}
} }

View File

@@ -1,10 +1,18 @@
package ltd.hlaeja.util package ltd.hlaeja.util
import org.springframework.security.core.Authentication as SpringAuthentication import ltd.hlaeja.form.AccountForm
import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.library.accountRegistry.Authentication import ltd.hlaeja.library.accountRegistry.Authentication
import org.springframework.security.core.Authentication as SpringAuthentication
fun SpringAuthentication.toAuthenticationRequest(): Authentication.Request = Authentication.Request( fun SpringAuthentication.toAuthenticationRequest(): Authentication.Request = Authentication.Request(
principal as String, principal as String,
credentials as String, credentials as String,
) )
fun AccountForm.toAccountRequest(): Account.Request = Account.Request(
username = username,
password = password,
enabled = enabled,
roles = listOf("ROLE_${role.uppercase()}"),
)

View File

@@ -1,8 +1,12 @@
package ltd.hlaeja.util package ltd.hlaeja.util
import ltd.hlaeja.exception.AccountRegistryException
import ltd.hlaeja.exception.UsernameDuplicateException
import ltd.hlaeja.library.accountRegistry.Account import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.library.accountRegistry.Authentication import ltd.hlaeja.library.accountRegistry.Authentication
import ltd.hlaeja.property.AccountRegistryProperty import ltd.hlaeja.property.AccountRegistryProperty
import org.springframework.http.HttpStatus.BAD_REQUEST
import org.springframework.http.HttpStatus.CONFLICT
import org.springframework.http.HttpStatus.LOCKED import org.springframework.http.HttpStatus.LOCKED
import org.springframework.http.HttpStatus.NOT_FOUND import org.springframework.http.HttpStatus.NOT_FOUND
import org.springframework.http.HttpStatus.UNAUTHORIZED import org.springframework.http.HttpStatus.UNAUTHORIZED
@@ -28,8 +32,19 @@ fun WebClient.accountRegistryAuthenticate(
fun WebClient.accountRegistryAccounts( fun WebClient.accountRegistryAccounts(
page: Int, page: Int,
size: Int, size: Int,
property: AccountRegistryProperty property: AccountRegistryProperty,
): Flux<Account.Response> = get() ): Flux<Account.Response> = get()
.uri("${property.url}/accounts?page=$page&size=$size".also(::logCall)) .uri("${property.url}/accounts?page=$page&size=$size".also(::logCall))
.retrieve() .retrieve()
.bodyToFlux(Account.Response::class.java) .bodyToFlux(Account.Response::class.java)
fun WebClient.accountRegistryCreate(
request: Account.Request,
property: AccountRegistryProperty,
): Mono<Account.Response> = post()
.uri("${property.url}/account".also(::logCall))
.bodyValue(request)
.retrieve()
.onStatus(CONFLICT::equals) { throw UsernameDuplicateException() }
.onStatus(BAD_REQUEST::equals) { throw AccountRegistryException("Remote service returned 400") }
.bodyToMono(Account.Response::class.java)

View File

@@ -0,0 +1,90 @@
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Home Pages</title>
<!--/*/<th:block th:insert="~{layout.html :: documentHead}"/>/*/-->
</head>
<body>
<main>
<h1>Test create user</h1>
<hr>
<form th:action="@{/account/create}" th:method="post">
<!-- Display error message if present -->
<div th:if="${errorMessage != null}" style="color: red; margin-bottom: 10px;">
<span th:text="${errorMessage}">Error Message</span>
</div>
<!-- Username -->
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username" th:field="*{accountForm.username}" required/>
</div>
<!-- Password -->
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required/>
</div>
<!-- Re-enter Password -->
<div>
<label for="passwordConfirm">Re-enter Password:</label>
<input type="password" id="passwordConfirm" name="passwordConfirm" required/>
<span id="passwordMatchMessage"></span>
</div>
<!-- Role -->
<div>
<label for="role">Role:</label>
<select id="role" name="role" th:field="*{accountForm.role}" required>
<option value="user">User</option>
<option value="registered">Registered</option>
<option value="admin">Admin</option>
</select>
</div>
<!-- Enabled -->
<div>
<label for="enabled">Enabled:</label>
<input type="checkbox" id="enabled" name="enabled" th:field="*{accountForm.enabled}"/>
</div>
<!-- Submit Button -->
<button type="submit">Create User</button>
</form>
<br>
<a href="/account">Account</a>
<a href="/logout">Logout</a><br>
</main>
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
<script>
// Get password fields
const password = document.getElementById('password');
const passwordConfirm = document.getElementById('passwordConfirm');
const passwordMatchMessage = document.getElementById('passwordMatchMessage');
// Function to check if passwords match
function checkPasswordMatch() {
if (password.value === passwordConfirm.value) {
passwordMatchMessage.textContent = 'Passwords match!';
passwordMatchMessage.style.color = 'green';
} else {
passwordMatchMessage.textContent = 'Passwords do not match!';
passwordMatchMessage.style.color = 'red';
}
}
// Add event listeners to both password fields
password.addEventListener('input', checkPasswordMatch);
passwordConfirm.addEventListener('input', checkPasswordMatch);
// Form submit validation
document.querySelector('form').addEventListener('submit', function(e) {
if (password.value !== passwordConfirm.value) {
alert('Passwords do not match!');
e.preventDefault();
}
});
</script>
</body>
</html>

View File

@@ -34,7 +34,8 @@
<span th:unless="${pagination.hasMore}">Next</span> <span th:unless="${pagination.hasMore}">Next</span>
</div> </div>
<br> <br>
<a href="/logout">Logout</a> <a href="/account/create">Create Account</a><br>
<a href="/logout">Logout</a><br>
</main> </main>
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/--> <!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
</body> </body>