added basic edit account

- add link in to edit a user in users.html
- change to AccountController
  - update getCreateAccount for change to AccountForm
  - add getEditAccount
- add edit.html
- change to Mapping.kt
  - update AccountForm toAccountRequest to throw exception if password null
  - add Account Response toAccountForm
- change password and passwordConfirm to be null in AccountForm
- add PasswordException
- add getAccount to AccountRegistryService
- add accountRegistryAccount to WebClientCalls.kt
This commit is contained in:
2025-01-27 13:26:31 +01:00
parent 8e65de0350
commit 14e7971f73
8 changed files with 167 additions and 4 deletions

View File

@@ -1,9 +1,12 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.dto.Pagination import ltd.hlaeja.dto.Pagination
import ltd.hlaeja.exception.PasswordException
import ltd.hlaeja.exception.UsernameDuplicateException import ltd.hlaeja.exception.UsernameDuplicateException
import ltd.hlaeja.form.AccountForm import ltd.hlaeja.form.AccountForm
import ltd.hlaeja.service.AccountRegistryService import ltd.hlaeja.service.AccountRegistryService
import ltd.hlaeja.util.toAccountForm
import ltd.hlaeja.util.toAccountRequest 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
@@ -24,11 +27,22 @@ class AccountController(
const val DEFAULT_SIZE: Int = 25 const val DEFAULT_SIZE: Int = 25
} }
@GetMapping("/edit-{account}")
fun getEditAccount(
@PathVariable account: UUID,
model: Model
): Mono<String> = accountRegistryService.getAccount(account)
.doOnNext {
model.addAttribute("account", account)
model.addAttribute("accountForm", it.toAccountForm())
}
.then(Mono.just("account/edit"))
@GetMapping("/create") @GetMapping("/create")
fun getCreateAccount( fun getCreateAccount(
model: Model, model: Model,
): Mono<String> = Mono.just("account/create") ): Mono<String> = Mono.just("account/create")
.doOnNext { model.addAttribute("accountForm", AccountForm("", "", "", "", true)) } .doOnNext { model.addAttribute("accountForm", AccountForm("", "")) }
@PostMapping("/create") @PostMapping("/create")
fun postCreateAccount( fun postCreateAccount(
@@ -43,6 +57,7 @@ class AccountController(
.onErrorResume { error -> .onErrorResume { error ->
val errorMessage = when (error) { val errorMessage = when (error) {
is UsernameDuplicateException -> "Username already exists. Please choose another." is UsernameDuplicateException -> "Username already exists. Please choose another."
is PasswordException -> error.message
else -> "An unexpected error occurred. Please try again later." else -> "An unexpected error occurred. Please try again later."
} }
model.addAttribute("errorMessage", errorMessage) model.addAttribute("errorMessage", errorMessage)

View File

@@ -0,0 +1,23 @@
package ltd.hlaeja.exception
@Suppress("unused")
open class PasswordException : 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

@@ -2,8 +2,8 @@ package ltd.hlaeja.form
data class AccountForm( data class AccountForm(
val username: String, val username: String,
val password: CharSequence,
val passwordConfirm: CharSequence,
val role: String, val role: String,
val enabled: Boolean = false, val enabled: Boolean = false,
val password: CharSequence? = null,
val passwordConfirm: CharSequence? = null,
) )

View File

@@ -1,10 +1,12 @@
package ltd.hlaeja.service package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.UUID
import ltd.hlaeja.exception.AccountRegistryException 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.accountRegistryAccount
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 ltd.hlaeja.util.accountRegistryCreate
@@ -66,4 +68,14 @@ class AccountRegistryService(
else -> Mono.error(ResponseStatusException(BAD_REQUEST, error.message)) else -> Mono.error(ResponseStatusException(BAD_REQUEST, error.message))
} }
} }
fun getAccount(
account: UUID,
): Mono<Account.Response> = webClient.accountRegistryAccount(account, property)
.onErrorResume { error ->
when (error) {
is ResponseStatusException -> Mono.error(error)
else -> Mono.error(ResponseStatusException(BAD_REQUEST, error.message))
}
}
} }

View File

@@ -1,5 +1,6 @@
package ltd.hlaeja.util package ltd.hlaeja.util
import ltd.hlaeja.exception.PasswordException
import ltd.hlaeja.form.AccountForm import ltd.hlaeja.form.AccountForm
import ltd.hlaeja.library.accountRegistry.Account import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.library.accountRegistry.Authentication import ltd.hlaeja.library.accountRegistry.Authentication
@@ -12,7 +13,13 @@ fun SpringAuthentication.toAuthenticationRequest(): Authentication.Request = Aut
fun AccountForm.toAccountRequest(): Account.Request = Account.Request( fun AccountForm.toAccountRequest(): Account.Request = Account.Request(
username = username, username = username,
password = password, password = password ?: throw PasswordException("Password requirements failed"),
enabled = enabled, enabled = enabled,
roles = listOf("ROLE_${role.uppercase()}"), roles = listOf("ROLE_${role.uppercase()}"),
) )
fun Account.Response.toAccountForm(): AccountForm = AccountForm(
username = username,
enabled = enabled,
role = roles.first().removePrefix("ROLE_").lowercase()
)

View File

@@ -1,5 +1,6 @@
package ltd.hlaeja.util package ltd.hlaeja.util
import java.util.UUID
import ltd.hlaeja.exception.AccountRegistryException import ltd.hlaeja.exception.AccountRegistryException
import ltd.hlaeja.exception.UsernameDuplicateException import ltd.hlaeja.exception.UsernameDuplicateException
import ltd.hlaeja.library.accountRegistry.Account import ltd.hlaeja.library.accountRegistry.Account
@@ -14,6 +15,7 @@ import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.LockedException import org.springframework.security.authentication.LockedException
import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.server.ResponseStatusException
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -48,3 +50,12 @@ fun WebClient.accountRegistryCreate(
.onStatus(CONFLICT::equals) { throw UsernameDuplicateException() } .onStatus(CONFLICT::equals) { throw UsernameDuplicateException() }
.onStatus(BAD_REQUEST::equals) { throw AccountRegistryException("Remote service returned 400") } .onStatus(BAD_REQUEST::equals) { throw AccountRegistryException("Remote service returned 400") }
.bodyToMono(Account.Response::class.java) .bodyToMono(Account.Response::class.java)
fun WebClient.accountRegistryAccount(
account: UUID,
property: AccountRegistryProperty,
): Mono<Account.Response> = get()
.uri("${property.url}/account-$account".also(::logCall))
.retrieve()
.onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NOT_FOUND) }
.bodyToMono(Account.Response::class.java)

View File

@@ -0,0 +1,93 @@
<!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 edit user <strong th:text="${accountForm.username}">username</strong></h1>
<hr>
<form th:action="@{/account/edit-{account}(account = ${account})}" 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>
<!-- 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>
<hr>
<!-- Password -->
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password"/>
</div>
<!-- Re-enter Password -->
<div>
<label for="passwordConfirm">Re-enter Password:</label>
<input type="password" id="passwordConfirm" name="passwordConfirm"/>
<span id="passwordMatchMessage"></span>
</div>
<hr>
<!-- Submit Button -->
<button type="submit">Update 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

@@ -14,11 +14,13 @@
<th>Id</th> <th>Id</th>
<th>Name</th> <th>Name</th>
<th>Description</th> <th>Description</th>
<th>Actions</th>
</tr> </tr>
<tr th:each="item : ${items}"> <tr th:each="item : ${items}">
<td th:text="${item.id}">ID</td> <td th:text="${item.id}">ID</td>
<td th:text="${item.timestamp}">timestamp</td> <td th:text="${item.timestamp}">timestamp</td>
<td th:text="${item.username}">username</td> <td th:text="${item.username}">username</td>
<td><a th:href="@{/account/edit-{id}(id = ${item.id})}">Edit</a></td>
</tr> </tr>
</table> </table>
<div th:if="${pagination.showSize}"> <div th:if="${pagination.showSize}">