Update html layout

- add messages fragment
  - extract error and success message list from edit.html
  - extract error message list from create.html
  - add messages.html

- update edit account
  - update AccountController
    - update postEditAccount for validation
    - update getEditAccount for roleGroups
  - update validation for AccountForm for edit
  - add EditGroup
  - update Account.Response.toAccountForm()
  - update edit.html

- update create account
  - update AccountController
    - update postCreateAccount for validation
    - update getCreateAccount for role group
  - add validation to AccountForm
  - add PasswordMatchValidator
  - add annotation PasswordMatch
  - add CreateGroup
  - add temporary getRoles() in AccountRegistryService
  - update AccountForm.toAccountRequest() in Mapping
  - change management.css
    - add ::selection
    - add Selected Option Styling
  - add passwordMatchCheck to management.js
  - update create.html

- update user
  - add makeLocalTime in management.js
  - update users.html
  - update Pagination
    - add size of items
    - rename size to show

- update goodbye.html

- update logout.html

- update login.html

- update welcome.html

- update index.html

- update layout
  - update management.css
  - update management.js
  - update layout.html
This commit is contained in:
2025-03-01 16:04:56 +01:00
parent 4c6c7dd9d8
commit 4c4baa95dd
22 changed files with 807 additions and 344 deletions

View File

@@ -19,6 +19,7 @@ dependencies {
implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.security)
implementation(hlaeja.springboot.starter.thymeleaf)
implementation(hlaeja.springboot.starter.validation)
implementation(hlaeja.springboot.starter.webflux)
implementation(hlaeja.thymeleaf.spring.security)

View File

@@ -1,10 +1,11 @@
package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.controller.validation.CreateGroup
import ltd.hlaeja.controller.validation.EditGroup
import ltd.hlaeja.dto.Pagination
import ltd.hlaeja.exception.NoChangeException
import ltd.hlaeja.exception.NotFoundException
import ltd.hlaeja.exception.PasswordException
import ltd.hlaeja.exception.UsernameDuplicateException
import ltd.hlaeja.form.AccountForm
import ltd.hlaeja.service.AccountRegistryService
@@ -12,6 +13,8 @@ import ltd.hlaeja.util.toAccountForm
import ltd.hlaeja.util.toAccountRequest
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.validation.BindingResult
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PathVariable
@@ -36,6 +39,7 @@ class AccountController(
): Mono<String> = accountRegistryService.getAccount(account)
.doOnNext {
model.addAttribute("account", account)
model.addAttribute("roleGroups", accountRegistryService.getRoles())
model.addAttribute("accountForm", it.toAccountForm())
}
.then(Mono.just("account/edit"))
@@ -43,65 +47,93 @@ class AccountController(
@PostMapping("/edit-{account}")
fun postEditAccount(
@PathVariable account: UUID,
@ModelAttribute("accountForm") accountForm: AccountForm,
@Validated(EditGroup::class) @ModelAttribute("accountForm") accountForm: AccountForm,
bindingResult: BindingResult,
model: Model,
): Mono<String> = Mono.just(accountForm)
.flatMap {
accountRegistryService.updateAccount(
account,
it.toAccountRequest { password -> if (password.isNullOrEmpty()) null else password },
)
}
.doOnNext {
model.addAttribute("successMessage", "Saved changes!!!")
model.addAttribute("account", account)
model.addAttribute("accountForm", it.toAccountForm())
}
.then(Mono.just("account/edit"))
.onErrorResume { error ->
val errorMessage = when (error) {
is NoChangeException -> Pair("successMessage", "No change to save")
is NotFoundException -> Pair("errorMessage", "User dont exists. how did this happen?")
is UsernameDuplicateException -> Pair("errorMessage", "Username already exists. Please choose another.")
else -> Pair("errorMessage", "An unexpected error occurred. Please try again later.")
): Mono<String> {
val validationErrors = if (bindingResult.hasErrors()) {
bindingResult.allErrors.map { error ->
error.defaultMessage ?: "Unknown validation error"
}
model.addAttribute(errorMessage.first, errorMessage.second)
model.addAttribute("accountForm", accountForm)
model.addAttribute("account", account)
Mono.just("account/edit")
} else {
emptyList()
}
if (bindingResult.hasErrors()) {
model.addAttribute("accountForm", accountForm)
model.addAttribute("validationErrors", validationErrors)
model.addAttribute("roleGroups", accountRegistryService.getRoles())
return Mono.just("account/edit")
}
return Mono.just(accountForm)
.flatMap { accountRegistryService.updateAccount(account, it.toAccountRequest()) }
.doOnNext {
model.addAttribute("successMessage", listOf("Saved changes!!!"))
model.addAttribute("account", account)
model.addAttribute("accountForm", it.toAccountForm())
model.addAttribute("roleGroups", accountRegistryService.getRoles())
}
.then(Mono.just("account/edit"))
.onErrorResume { error ->
val errorMessage = when (error) {
is NoChangeException -> Pair("successMessage", "No change to save.")
is NotFoundException -> Pair("validationErrors", "User dont exists. how did this happen?")
is UsernameDuplicateException -> Pair(
"validationErrors",
"Username already exists. Please choose another.",
)
else -> Pair("validationErrors", "An unexpected error occurred. Please try again later.")
}
model.addAttribute(errorMessage.first, listOf(errorMessage.second))
model.addAttribute("account", account)
model.addAttribute("accountForm", accountForm)
model.addAttribute("roleGroups", accountRegistryService.getRoles())
Mono.just("account/edit")
}
}
@GetMapping("/create")
fun getCreateAccount(
model: Model,
): Mono<String> = Mono.just("account/create")
.doOnNext { model.addAttribute("accountForm", AccountForm("", "")) }
.doOnNext {
model.addAttribute("accountForm", AccountForm("", emptyList()))
model.addAttribute("roleGroups", accountRegistryService.getRoles())
}
@PostMapping("/create")
fun postCreateAccount(
@ModelAttribute("accountForm") accountForm: AccountForm,
@Validated(CreateGroup::class) @ModelAttribute("accountForm") accountForm: AccountForm,
bindingResult: BindingResult,
model: Model,
): Mono<String> = Mono.just(accountForm)
.flatMap {
accountRegistryService.addAccount(
it.toAccountRequest { password ->
when {
password.isNullOrEmpty() -> throw PasswordException("Password requirements failed")
else -> password
}
},
)
}
.map { "redirect:/account" }
.onErrorResume { error ->
val errorMessage = when (error) {
is UsernameDuplicateException -> "Username already exists. Please choose another."
is PasswordException -> error.message
else -> "An unexpected error occurred. Please try again later."
): Mono<String> {
val validationErrors = if (bindingResult.hasErrors()) {
bindingResult.allErrors.map { error ->
error.defaultMessage ?: "Unknown validation error"
}
model.addAttribute("errorMessage", errorMessage)
Mono.just("account/create")
} else {
emptyList()
}
if (bindingResult.hasErrors()) {
model.addAttribute("accountForm", accountForm)
model.addAttribute("validationErrors", validationErrors)
model.addAttribute("roleGroups", accountRegistryService.getRoles())
return Mono.just("account/create")
}
return Mono.just(accountForm)
.flatMap { accountRegistryService.addAccount(it.toAccountRequest()) }
.map { "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("validationErrors", listOf(errorMessage))
model.addAttribute("roleGroups", accountRegistryService.getRoles())
Mono.just("account/create")
}
}
@GetMapping
fun getDefaultAccounts(

View File

@@ -0,0 +1,3 @@
package ltd.hlaeja.controller.validation
interface CreateGroup

View File

@@ -0,0 +1,3 @@
package ltd.hlaeja.controller.validation
interface EditGroup

View File

@@ -0,0 +1,15 @@
package ltd.hlaeja.controller.validation
import jakarta.validation.Constraint
import jakarta.validation.Payload
import kotlin.reflect.KClass
@MustBeDocumented
@Constraint(validatedBy = [PasswordMatchValidator::class])
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class PasswordMatch(
val message: String = "Passwords must match",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = [],
)

View File

@@ -0,0 +1,17 @@
package ltd.hlaeja.controller.validation
import jakarta.validation.ConstraintValidator
import jakarta.validation.ConstraintValidatorContext
import ltd.hlaeja.form.AccountForm
class PasswordMatchValidator : ConstraintValidator<PasswordMatch, AccountForm> {
override fun isValid(form: AccountForm, context: ConstraintValidatorContext): Boolean {
val password = form.password?.toString()
val passwordConfirm = form.passwordConfirm?.toString()
return if (password.isNullOrEmpty() && passwordConfirm.isNullOrEmpty()) {
true
} else {
password == passwordConfirm
}
}
}

View File

@@ -3,15 +3,16 @@ package ltd.hlaeja.dto
@Suppress("unused")
data class Pagination(
val page: Int,
val size: Int,
val show: Int,
val items: Int,
val defaultSize: Int,
) {
val hasMore: Boolean = size == items
val showSize: Boolean = size != defaultSize
val hasMore: Boolean = show == items
val showSize: Boolean = show != defaultSize
val first: Boolean = page <= 1
val previous: Int = page - 1
val next: Int = page + 1
val start: Int = (page - 1) * size + 1
val end: Int = page * size
val start: Int = previous * show + 1
val end: Int = page * show
val size: Int = previous * show + items
}

View File

@@ -1,9 +1,19 @@
package ltd.hlaeja.form
import jakarta.validation.constraints.NotEmpty
import ltd.hlaeja.controller.validation.CreateGroup
import ltd.hlaeja.controller.validation.EditGroup
import ltd.hlaeja.controller.validation.PasswordMatch
@PasswordMatch(groups = [CreateGroup::class, EditGroup::class])
data class AccountForm(
val username: String,
val role: String,
val enabled: Boolean = false,
val password: CharSequence? = null,
val passwordConfirm: CharSequence? = null,
@field:NotEmpty(message = "Username cannot be empty", groups = [CreateGroup::class, EditGroup::class])
var username: String,
@field:NotEmpty(message = "At least one role must be selected", groups = [CreateGroup::class, EditGroup::class])
var roles: List<String> = emptyList(),
var enabled: Boolean = false,
@field:NotEmpty(message = "Password cannot be empty", groups = [CreateGroup::class])
var password: CharSequence? = null,
@field:NotEmpty(message = "Password confirmation cannot be empty", groups = [CreateGroup::class])
var passwordConfirm: CharSequence? = null,
)

View File

@@ -90,4 +90,11 @@ class AccountRegistryService(
else -> Mono.error(ResponseStatusException(BAD_REQUEST, error.message))
}
}
// TODO implement user gropes and access
fun getRoles(): Map<String, List<String>> = mapOf(
"Admin Group" to listOf("Admin"),
"Operations Group" to listOf("Registry"),
"User Group" to listOf("User"),
)
}

View File

@@ -10,15 +10,19 @@ fun SpringAuthentication.toAuthenticationRequest(): Authentication.Request = Aut
credentials as String,
)
fun AccountForm.toAccountRequest(operation: (CharSequence?) -> CharSequence?): Account.Request = Account.Request(
fun AccountForm.toAccountRequest(): Account.Request = Account.Request(
username = username,
password = operation(password),
password = if (password.isNullOrEmpty()) null else password,
enabled = enabled,
roles = listOf("ROLE_${role.uppercase()}"),
roles = roles.map { "ROLE_${it.uppercase()}" },
)
fun Account.Response.toAccountForm(): AccountForm = AccountForm(
username = username,
enabled = enabled,
role = roles.first().removePrefix("ROLE_").lowercase(),
roles = roles.map {
it.removePrefix("ROLE_")
.lowercase()
.replaceFirstChar { char -> char.uppercase() }
},
)

View File

@@ -1,4 +1,135 @@
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap');
body {
background-color: #000000;
color: #04931b;
font-family: 'JetBrains Mono', monospace;
}
::selection {
background-color: #065f46;
color: #ffffff;
}
.terminal-glow {
text-shadow: 0 0 5px rgba(0, 255, 0, 0.5);
}
/* Dropdown Divider Styling */
.dropdown-divider {
border: 0;
border-top: 1px solid #065f46;
margin: 0.5rem 0;
}
/* Custom Scrollbar Styling */
::-webkit-scrollbar {
width: 16px;
}
::-webkit-scrollbar-track {
background: #1f2937;
border-left: 1px solid #065f46;
border-radius: 0;
}
::-webkit-scrollbar-thumb {
background: #047857;
border-radius: 0;
}
::-webkit-scrollbar-thumb:hover {
background: #10b981;
}
* {
scrollbar-width: medium;
scrollbar-color: #047857 #1f2937;
}
.scrollable-no-radius-right {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
overflow-y: scroll !important;
}
/* Custom Switch Styling */
.switch-wrapper {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
}
.switch-wrapper input {
opacity: 0;
width: 100%;
height: 100%;
position: absolute;
z-index: 1;
cursor: pointer;
}
.switch-slider {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #4b5563;
border: 1px solid;
transition: background-color 0.4s;
border-radius: 20px;
}
.switch-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 2px;
bottom: 2px;
background-color: #9ca3af;
transition: transform 0.4s;
border-radius: 50%;
}
.switch-wrapper input:checked + .switch-slider {
background-color: #047857;
}
.switch-wrapper input:checked + .switch-slider:before {
transform: translateX(20px);
background-color: #10b981;
}
/* Focus state for switch */
.switch-wrapper input:focus + .switch-slider {
--tw-border-opacity: 1;
border-color: rgb(22 163 74 / var(--tw-border-opacity, 1)); /* green-600, matches focus:border-green-600 */
}
/*!* Glow effect on focus for all form elements *!*/
/*input:focus,*/
/*select:focus,*/
/*button:focus,*/
/*.switch-wrapper input:focus + .switch-slider {*/
/* box-shadow: 0 0 5px rgba(0, 255, 0, 0.5);*/
/* outline: none;*/
/*}*/
/* Selected Option Styling */
select option:checked {
background-color: #064e3b;
color: #BBF7D0FF;
}
select:focus option:checked {
background: #065f46 linear-gradient(0deg, #065f46 0%, #065f46 100%);
}
select:focus option:hover,
select option:hover {
background-color: #11a36f !important;
color: #fff;
}

View File

@@ -0,0 +1,59 @@
document.addEventListener('DOMContentLoaded', () => {
const toggle = document.getElementById('menu-toggle');
const menu = document.getElementById('dropdown-menu');
toggle.addEventListener('click', () => {
menu.classList.toggle('hidden');
});
document.addEventListener('click', (e) => {
if (!toggle.contains(e.target) && !menu.contains(e.target)) {
menu.classList.add('hidden');
}
});
});
function makeLocalTime(elements) {
elements.forEach(element => {
const utcTime = element.getAttribute('data-timestamp');
if (utcTime) {
element.textContent = new Intl.DateTimeFormat(
'sv-SE',
{
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZoneName: 'shortOffset'
}
).format(new Date(utcTime));
}
});
}
function passwordMatchCheck(passwordInput, confirmPasswordInput, matchMessage) {
if (passwordInput && confirmPasswordInput && matchMessage) {
function checkPasswordMatch() {
const password = passwordInput.value;
const confirmPassword = confirmPasswordInput.value;
if (password === '' && confirmPassword === '') {
matchMessage.textContent = '';
matchMessage.classList.remove('text-green-600', 'text-red-600');
} else if (password === confirmPassword) {
matchMessage.textContent = 'Passwords match';
matchMessage.classList.remove('text-red-600');
matchMessage.classList.add('text-green-600');
} else {
matchMessage.textContent = 'Passwords do not match';
matchMessage.classList.remove('text-green-600');
matchMessage.classList.add('text-red-600');
}
}
passwordInput.addEventListener('input', checkPasswordMatch);
confirmPasswordInput.addEventListener('input', checkPasswordMatch);
}
}

View File

@@ -1,90 +1,62 @@
<!DOCTYPE HTML>
<!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>
<!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
<main class="container mx-auto p-4 flex-grow flex items-center justify-center">
<div class="w-full max-w-md">
<h2 class="text-lg sm:text-xl mb-4 terminal-glow">New Account Registration</h2>
<hr class="border-green-900 mb-4">
<th:block th:replace="~{messages :: messageDisplay(messageList=${validationErrors}, error=true, styleClass='text-red-600')}"/>
<form th:action="@{/account/create}" th:method="post">
<div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<div class="mb-4">
<label for="username" class="block text-sm mb-2">Username</label>
<input th:field="*{accountForm.username}" id="username" type="text" placeholder="Enter username..." class="w-full bg-gray-900 border border-green-900 rounded px-3 py-2 text-green-400 focus:outline-none focus:border-green-600">
</div>
<div class="mb-4">
<label for="roles" class="block text-sm mb-2">Roles</label>
<select th:field="*{accountForm.roles}" id="roles" multiple="multiple" size="6" class="w-full bg-gray-900 border border-green-900 rounded scrollable-no-radius-right px-3 py-2 text-green-400 focus:outline-none focus:border-green-600 overflow-y-auto">
<optgroup th:each="group : ${roleGroups}" th:label="${group.key}" class="text-green-600">
<option th:each="role : ${group.value}" th:value="${role}" th:text="${role}" class="text-green-400"/>
</optgroup>
</select>
<p class="text-xs text-green-600 mt-1">[Hold Ctrl/Cmd to select multiple]</p>
</div>
<div class="mb-4">
<label for="enabled" class="block text-sm mb-2">Enabled</label>
<div class="switch-wrapper">
<input th:checked="${accountForm.enabled}" value="true" id="enabled" name="enabled" type="checkbox">
<span class="switch-slider border-green-900"></span>
</div>
</div>
<div class="mb-4">
<label for="password" class="block text-sm mb-2">Password</label>
<input th:field="*{accountForm.password}" id="password" type="password" placeholder="Enter password..." class="w-full bg-gray-900 border border-green-900 rounded px-3 py-2 text-green-400 focus:outline-none focus:border-green-600">
</div>
<div class="mb-4">
<label class="block text-sm mb-2" for="passwordConfirm">Confirm password</label>
<input th:field="*{accountForm.passwordConfirm}" id="passwordConfirm" type="password" placeholder="Confirm password..." class="w-full bg-gray-900 border border-green-900 rounded px-3 py-2 text-green-400 focus:outline-none focus:border-green-600">
<span id="passwordMatchMessage" class="text-xs mt-1"></span>
</div>
</div>
<div class="mt-4 flex justify-end space-x-4">
<a href="/account" class="bg-gray-800 hover:bg-gray-700 text-green-400 px-4 py-2 rounded border border-green-900 transition-colors inline-block">Cancel</a>
<button type="submit" class="bg-green-900 hover:bg-green-800 text-green-400 px-4 py-2 rounded border border-green-600 transition-colors">Create</button>
</div>
</form>
</div>
</main>
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<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();
}
document.addEventListener('DOMContentLoaded', () => {
passwordMatchCheck(
document.getElementById('password'),
document.getElementById('passwordConfirm'),
document.getElementById('passwordMatchMessage')
);
});
</script>
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
</body>
</html>

View File

@@ -1,96 +1,71 @@
<!DOCTYPE HTML>
<!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>
<div th:if="${successMessage != null}" style="color: blue; margin-bottom: 10px;">
<span th:text="${successMessage}">success 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>
<!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
<main class="container mx-auto p-4 flex-grow flex items-center justify-center">
<div class="w-full max-w-4xl">
<form th:action="@{/account/edit-{account}(account = ${account})}" th:method="post">
<div class="mb-4">
<h1 class="text-lg sm:text-xl mb-4 terminal-glow">Edit User <strong th:text="${accountForm.username}"/></h1>
<hr class="border-green-900 mb-4">
<th:block th:replace="~{messages :: messageDisplay(messageList=${validationErrors}, error=true, styleClass='text-red-600')}"/>
<th:block th:replace="~{messages :: messageDisplay(messageList=${successMessage}, error=false, styleClass='text-green-600')}"/>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<h2 class="text-lg sm:text-xl mb-4 terminal-glow">Account Details</h2>
<div class="mb-4">
<label class="block text-sm mb-2" for="username">Username:</label>
<input th:field="*{accountForm.username}" id="username" name="username" type="text" class="w-full bg-gray-900 border border-green-900 rounded px-3 py-2 text-green-400 focus:outline-none focus:border-green-600">
</div>
<div class="mb-4">
<label class="block text-sm mb-2" for="roles">Role:</label>
<select th:field="*{accountForm.roles}" id="roles" multiple="multiple" size="6" class="w-full bg-gray-900 border border-green-900 rounded scrollable-no-radius-right px-3 py-2 text-green-400 focus:outline-none focus:border-green-600 overflow-y-auto">
<optgroup th:each="group : ${roleGroups}" th:label="${group.key}" class="text-green-600">
<option th:each="role : ${group.value}" th:value="${role}" th:text="${role}" class="text-green-400"/>
</optgroup>
</select>
<p class="text-xs text-green-600 mt-1">[Hold Ctrl/Cmd to select multiple]</p>
</div>
<div class="mb-4">
<label class="block text-sm mb-2" for="enabled">Enabled:</label>
<div class="switch-wrapper">
<input th:checked="${accountForm.enabled}" value="true" id="enabled" name="enabled" type="checkbox">
<span class="switch-slider border-green-900"></span>
</div>
</div>
</div>
<div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<h2 class="text-lg sm:text-xl mb-4 terminal-glow">Password Update</h2>
<div class="mb-4">
<label class="block text-sm mb-2" for="password">Password:</label>
<input th:field="*{accountForm.password}" id="password" name="password" type="password" placeholder="Leave blank to keep current" class="w-full bg-gray-900 border border-green-900 rounded px-3 py-2 text-green-400 focus:outline-none focus:border-green-600">
</div>
<div class="mb-4">
<label class="block text-sm mb-2" for="passwordConfirm">Re-enter Password:</label>
<input th:field="*{accountForm.passwordConfirm}" id="passwordConfirm" name="passwordConfirm" type="password" placeholder="Confirm new password" class="w-full bg-gray-900 border border-green-900 rounded px-3 py-2 text-green-400 focus:outline-none focus:border-green-600">
<span id="passwordMatchMessage" class="text-sm text-green-600"></span>
</div>
</div>
</div>
<div class="mt-4 flex justify-end space-x-4">
<a href="/account" class="bg-gray-800 hover:bg-gray-700 text-green-400 px-4 py-2 rounded border border-green-900 transition-colors inline-block">Cancel</a>
<button type="submit" class="bg-green-900 hover:bg-green-800 text-green-400 px-4 py-2 rounded border border-green-600 transition-colors">Update User</button>
</div>
</div>
</form>
</div>
</main>
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<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();
}
document.addEventListener('DOMContentLoaded', () => {
passwordMatchCheck(
document.getElementById('password'),
document.getElementById('passwordConfirm'),
document.getElementById('passwordMatchMessage')
);
});
</script>
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
</body>
</html>

View File

@@ -1,44 +1,67 @@
<!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</h1>
<hr>
<div>Show page <span th:text="${pagination.page}"/> items <span th:text="${pagination.start}"/> - <span th:text="${pagination.end}"/></div>
<table>
<tr>
<th>Id</th>
<th>Name</th>
<th>Description</th>
<th>Actions</th>
</tr>
<tr th:each="item : ${items}">
<td th:text="${item.id}">ID</td>
<td th:text="${item.timestamp}">timestamp</td>
<td th:text="${item.username}">username</td>
<td><a th:href="@{/account/edit-{id}(id = ${item.id})}">Edit</a></td>
</tr>
</table>
<div th:if="${pagination.showSize}">
<span th:if="${pagination.first}">Previous</span>
<a th:unless="${pagination.first}" th:href="@{'/account/page-' + ${pagination.previous} + '/show-' + ${size}}">Previous</a>
<a th:if="${pagination.hasMore}" th:href="@{'/account/page-' + ${pagination.next} + '/show-' + ${size}}">Next</a>
<span th:unless="${pagination.hasMore}">Next</span>
<!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
<main class="container mx-auto p-4 flex-grow">
<div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<h1 class="text-lg sm:text-xl mb-4 terminal-glow">Account Registry</h1>
<hr class="border-green-900 mb-4">
<div class="flex justify-between items-center mb-4">
<div th:if="${pagination.start > pagination.size}" class="text-sm">
Show page <span th:text="${pagination.page}"/> items 0 - 0
</div>
<div th:unless="${pagination.start > pagination.size}" class="text-sm">
Show page <span th:text="${pagination.page}"/>
items <span th:text="${pagination.start}"/> -
<span th:text="${pagination.size}"/>
</div>
<div class="mt-[-2px]">
<a th:href="@{/account/create}" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded border border-green-900 transition-colors text-sm">$ Create New Account</a>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<tr class="border-b border-green-900">
<th class="py-2 px-4 text-left">Username</th>
<th class="py-2 px-4 text-left">Timestamp</th>
<th class="py-2 px-4 text-left">Id</th>
<th class="py-2 px-4 text-left">Actions</th>
</tr>
<tr th:if="${items.isEmpty()}">
<td colspan="4" class="py-2 px-4 text-center text-green-600">No accounts found</td>
</tr>
<tr th:each="item : ${items}" class="border-b border-gray-700 hover:bg-gray-700">
<td class="py-2 px-4" th:text="${item.username}">username</td>
<td class="py-2 px-4 utcTimestamp" th:data-timestamp="${item.timestamp}">Loading...</td>
<td class="py-2 px-4" th:text="${item.id}">ID</td>
<td class="py-2 px-4"><a th:href="@{/account/edit-{id}(id = ${item.id})}">Edit</a></td>
</tr>
</table>
</div>
<div class="mt-6 text-sm" th:classappend="${pagination.showSize} ? 'space-y-2' : ''">
<div th:if="${pagination.showSize}" class="mt-6 flex justify-between items-center text-sm">
<span th:if="${pagination.first}" class="px-3 py-1 bg-gray-800 text-gray-500 rounded border border-green-900">Previous</span>
<a th:unless="${pagination.first}" th:href="@{'/account/page-' + ${pagination.previous} + '/show-' + ${pagination.show}}" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded border border-green-900 transition-colors">Previous</a>
<a th:if="${pagination.hasMore}" th:href="@{'/account/page-' + ${pagination.next} + '/show-' + ${pagination.show}}" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded border border-green-900 transition-colors">Next</a>
<span th:unless="${pagination.hasMore}" class="px-3 py-1 bg-gray-800 text-gray-500 rounded border border-green-900">Next</span>
</div>
<div th:unless="${pagination.showSize}" class="mt-6 flex justify-between items-center text-sm">
<span th:if="${pagination.first}" class="px-3 py-1 bg-gray-800 text-gray-500 rounded border border-green-900">Previous</span>
<a th:unless="${pagination.first}" th:href="@{'/account/page-' + ${pagination.previous}}" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded border border-green-900 transition-colors">Previous</a>
<a th:if="${pagination.hasMore}" th:href="@{'/account/page-' + ${pagination.next}}" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded border border-green-900 transition-colors">Next</a>
<span th:unless="${pagination.hasMore}" class="px-3 py-1 bg-gray-800 text-gray-500 rounded border border-green-900">Next</span>
</div>
</div>
</div>
<div th:unless="${pagination.showSize}">
<span th:if="${pagination.first}">Previous</span>
<a th:unless="${pagination.first}" th:href="@{'/account/page-' + ${pagination.previous}}">Previous</a>
<a th:if="${pagination.hasMore}" th:href="@{'/account/page-' + ${pagination.next}}">Next</a>
<span th:unless="${pagination.hasMore}">Next</span>
</div>
<br>
<a href="/account/create">Create Account</a><br>
<a href="/logout">Logout</a><br>
</main>
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
<script>
// Assuming makeLocalTime is defined elsewhere or needs to be added
document.addEventListener('DOMContentLoaded', () => {
makeLocalTime(document.querySelectorAll('.utcTimestamp'));
});
</script>
</body>
</html>

View File

@@ -1,16 +1,28 @@
<!DOCTYPE HTML>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Goodbye</title>
<!--/*/<th:block th:insert="~{layout.html :: documentHead}"/>/*/-->
</head>
<body>
<main>
<h1>You are logged out</h1>
<hr>
<p>We hope to see you again soon!</p>
<a th:href="@{/login}">Login Again</a>
<!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
<main class="container mx-auto p-4 flex-grow flex items-center justify-center">
<div class="w-full max-w-md">
<div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<h2 class="text-lg sm:text-xl mb-4 terminal-glow">Session Terminated</h2>
<pre class="text-sm mb-6 text-green-600">
> logout initiated...
> Saving session data... [OK]
> Closing connections... [OK]
> System cleanup complete... [OK]
Thank you for using Hlaeja Systems.
Goodbye.
</pre>
<div class="flex justify-center">
<a href="/login" class="bg-green-900 hover:bg-green-800 text-green-400 px-6 py-2 rounded border border-green-600 transition-colors">Reconnect</a>
</div>
</div>
</div>
</main>
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
</body>
</html>

View File

@@ -1,23 +1,42 @@
<!DOCTYPE HTML>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
<!--/*/<th:block th:insert="~{layout.html :: documentHead}"/>/*/-->
</head>
<body>
<main>
<h1>Login</h1>
<hr>
<form id="loginForm" th:action="@{/login}" th:method="post">
<label for="username" >Username</label>
<input type="text" id="username" name="username" placeholder="Enter your username" required>
<br>
<label for="password">Password</label>
<input type="password" id="password" name="password" placeholder="Enter your password" required>
<br>
<button type="submit">Login</button>
</form>
<!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
<!-- Main Content -->
<main class="container mx-auto p-4 flex-grow flex items-center justify-center">
<div class="w-full max-w-md">
<div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<h2 class="text-lg sm:text-xl mb-4 terminal-glow">Authentication Required</h2>
<div id="login-error" class="mb-4 text-red-600 text-sm hidden">
<span>Error: </span>Bad username or password!
</div>
<form th:action="@{/login}" th:method="post">
<div class="mb-4">
<label class="block text-sm mb-2" for="username">username</label>
<input type="text" id="username" name="username" class="w-full bg-gray-900 border border-green-900 rounded px-3 py-2 text-green-400 focus:outline-none focus:border-green-600" placeholder="Enter user ID...">
</div>
<div class="mb-6">
<label class="block text-sm mb-2" for="password">password</label>
<input type="password" id="password" name="password" class="w-full bg-gray-900 border border-green-900 rounded px-3 py-2 text-green-400 focus:outline-none focus:border-green-600" placeholder="Enter access code...">
</div>
<div class="flex justify-end">
<button type="submit" class="bg-green-900 hover:bg-green-800 text-green-400 px-4 py-2 rounded border border-green-600 transition-colors">Login</button>
</div>
</form>
</div>
</div>
</main>
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
<script>
document.addEventListener('DOMContentLoaded', () => {
const errorDiv = document.getElementById('login-error');
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('error')) {
errorDiv.classList.remove('hidden');
}
});
</script>
</body>
</html>

View File

@@ -1,17 +1,24 @@
<!DOCTYPE HTML>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Logout</title>
<!--/*/<th:block th:insert="~{layout.html :: documentHead}"/>/*/-->
</head>
<body>
<main>
<h1>Logout</h1>
<hr>
<p>Are you sure you want to logout?</p>
<form id="logoutForm" th:action="@{/logout}" th:method="post"></form>
<button type="submit" onclick="document.getElementById('logoutForm').submit(); return false;">Logout</button>
<!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
<main class="container mx-auto p-4 flex-grow flex items-center justify-center">
<div class="w-full max-w-md">
<div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<h2 class="text-lg sm:text-xl mb-4 terminal-glow">Confirm System Logout</h2>
<p class="text-sm mb-6 text-green-600">Are you sure you want to terminate your session?</p>
<div class="flex justify-end space-x-4">
<a href="/account/page-1/show-10" class="bg-gray-800 hover:bg-gray-700 text-green-400 px-4 py-2 rounded border border-green-900 transition-colors inline-block">Cancel</a>
<!-- <a href="/account" class=" text-center bg-gray-700 hover:bg-gray-600 text-green-400 px-4 py-2 rounded border border-green-900 transition-colors">Cansel</a>-->
<form th:action="@{/logout}" th:method="post" class="flex-1">
<button type="submit" class="w-full bg-red-900 hover:bg-red-800 text-green-400 px-4 py-2 rounded border border-red-600 transition-colors">Yes, Logout</button>
</form>
</div>
</div>
</div>
</main>
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
</body>
</html>

View File

@@ -1,16 +1,72 @@
<!DOCTYPE HTML>
<!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</h1>
<hr>
<p>This is a index page!</p>
<a href="/login">login</a>
<!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
<!-- Main Content -->
<main class="container mx-auto p-4 flex-grow">
<!-- Main Grid -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-4">
<!-- Sidebar -->
<div class="md:col-span-3">
<div class="bg-gray-800 p-4 rounded-lg border border-green-900">
<h2 class="text-base sm:text-lg mb-2 terminal-glow">System Commands</h2>
<ul class="space-y-2 text-sm sm:text-base">
<li><span class="text-green-600">$</span> status_check</li>
<li><span class="text-green-600">$</span> sys_reboot</li>
<li><span class="text-green-600">$</span> mem_info</li>
<li><span class="text-green-600">$</span> net_stat</li>
</ul>
</div>
</div>
<!-- Main Terminal -->
<div class="md:col-span-6">
<div class="bg-gray-800 p-4 rounded-lg border border-green-900 h-64 sm:h-96 overflow-y-auto">
<h2 class="text-base sm:text-lg mb-2 terminal-glow">Terminal Output</h2>
<pre class="text-xs sm:text-sm">
SYSTEM BOOT [OK]
Initializing kernel... [OK]
Mounting drives... [OK]
Network interfaces up... [OK]
Loading console interface... [OK]
> Running diagnostics...
CPU: 2.4GHz [85%]
Memory: 16GB [72% used]
Storage: 1TB [45% free]
Network: 1Gbps [Stable]
</pre>
</div>
</div>
<!-- Status Panel -->
<div class="md:col-span-3">
<div class="bg-gray-800 p-4 rounded-lg border border-green-900">
<h2 class="text-base sm:text-lg mb-2 terminal-glow">Status</h2>
<div class="space-y-2 text-sm sm:text-base">
<p>CPU: <span class="text-green-600">Online</span></p>
<p>Memory: <span class="text-green-600">Stable</span></p>
<p>Network: <span class="text-green-600">Connected</span></p>
<p>Uptime: 23h 45m</p>
</div>
</div>
</div>
</div>
<!-- Terminal Input -->
<div class="mt-4">
<div class="bg-gray-800 p-4 rounded-lg border border-green-900">
<div class="flex items-center">
<span class="text-green-600 mr-2">$</span>
<input type="text" class="bg-transparent w-full focus:outline-none text-green-400 text-sm sm:text-base" placeholder="Enter command...">
</div>
</div>
</div>
</main>
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
</body>
</html>

View File

@@ -1,24 +1,72 @@
<!DOCTYPE HTML>
<!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>Welcome</h1>
<hr>
<!--/*@thymesVar id="remoteUser" type="ltd.hlaeja.security.RemoteAuthentication"*/-->
<div th:if="${remoteUser.hasRole('admin')}">
You are an admin!
<a href="/account">Account</a>
<!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
<!-- Main Content -->
<main class="container mx-auto p-4 flex-grow">
<!-- Main Grid -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-4">
<!-- Sidebar -->
<div class="md:col-span-3">
<div class="bg-gray-800 p-4 rounded-lg border border-green-900">
<h2 class="text-base sm:text-lg mb-2 terminal-glow">System Commands</h2>
<ul class="space-y-2 text-sm sm:text-base">
<li><span class="text-green-600">$</span> status_check</li>
<li><span class="text-green-600">$</span> sys_reboot</li>
<li><span class="text-green-600">$</span> mem_info</li>
<li><span class="text-green-600">$</span> net_stat</li>
</ul>
</div>
</div>
<!-- Main Terminal -->
<div class="md:col-span-6">
<div class="bg-gray-800 p-4 rounded-lg border border-green-900 h-64 sm:h-96 overflow-y-auto">
<h2 class="text-base sm:text-lg mb-2 terminal-glow">Terminal Output</h2>
<pre class="text-xs sm:text-sm">
SYSTEM BOOT [OK]
Initializing kernel... [OK]
Mounting drives... [OK]
Network interfaces up... [OK]
Loading console interface... [OK]
> Running diagnostics...
CPU: 2.4GHz [85%]
Memory: 16GB [72% used]
Storage: 1TB [45% free]
Network: 1Gbps [Stable]
</pre>
</div>
</div>
<!-- Status Panel -->
<div class="md:col-span-3">
<div class="bg-gray-800 p-4 rounded-lg border border-green-900">
<h2 class="text-base sm:text-lg mb-2 terminal-glow">Status</h2>
<div class="space-y-2 text-sm sm:text-base">
<p>CPU: <span class="text-green-600">Online</span></p>
<p>Memory: <span class="text-green-600">Stable</span></p>
<p>Network: <span class="text-green-600">Connected</span></p>
<p>Uptime: 23h 45m</p>
</div>
</div>
</div>
</div>
<div th:if="${remoteUser.hasRole('user')}">
You are a user!
<!-- Terminal Input -->
<div class="mt-4">
<div class="bg-gray-800 p-4 rounded-lg border border-green-900">
<div class="flex items-center">
<span class="text-green-600 mr-2">$</span>
<input type="text" class="bg-transparent w-full focus:outline-none text-green-400 text-sm sm:text-base" placeholder="Enter command...">
</div>
</div>
</div>
<p>This is welcome pages and you're a user!</p>
<a href="/logout">Logout</a>
</main>
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
</body>
</html>

View File

@@ -1,10 +1,53 @@
<!DOCTYPE HTML>
<html lang="" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="documentHead">
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="documentHead (title)">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title th:text="${title}"/>
<link th:href="@{/css/management.css}" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<header th:fragment="header" class="bg-gray-800 border-b border-green-900">
<div class="container mx-auto px-4 py-2 flex items-center justify-between">
<div>
<h1 class="text-xl sm:text-2xl terminal-glow">Hlaeja Management</h1>
</div>
<div class="relative">
<button id="menu-toggle" class="focus:outline-none">
<svg class="w-6 h-6 text-green-400 hover:text-green-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
<div id="dropdown-menu" class="hidden absolute right-0 mt-2 w-48 bg-gray-800 border border-green-900 shadow-lg z-10">
<th:block th:if="${remoteUser.hasRole('admin')}">
<a href="/account" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Account</a>
<hr class="dropdown-divider">
</th:block>
<th:block th:if="${remoteUser.authenticated}">
<a href="#" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">$ diagnostics</a>
<a href="#" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">$ logs</a>
<a href="#" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">$ shutdown</a>
</th:block>
<th:block th:if="${remoteUser.authenticated}">
<hr class="dropdown-divider">
<a href="/logout" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Logout</a>
</th:block>
<th:block th:unless="${remoteUser.authenticated}">
<a href="/login" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Login</a>
</th:block>
</div>
</div>
</div>
</header>
<footer th:fragment="footer" class="bg-gray-800 border-t border-green-900 mt-4">
<div class="container mx-auto p-4">
<div class="flex flex-col sm:flex-row justify-between items-center text-sm">
<div class="text-green-600 mb-2 sm:mb-0">Hlaeja © 2025 Lulz Ltd</div>
<div class="flex space-x-4"></div>
</div>
</div>
</footer>
<div th:fragment="script">
<script th:src="@{/js/management.js}"></script>
</div>

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<th:block th:fragment="messageDisplay(messageList, error, styleClass)">
<div th:if="${messageList != null and #lists.size(messageList) > 0}" th:class="'mb-4 text-sm ' + ${styleClass}">
<th:block th:switch="${#lists.size(messageList)}">
<!-- Single Message Case -->
<th:block th:case="1">
<span th:if="${error}">Validation Error:</span>
<span th:unless="${error}">Validation:</span>
<span th:text="${messageList[0]}"></span>
</th:block>
<!-- Multiple Messages Case -->
<th:block th:case="*">
<span th:if="${error}">Validation Errors:</span>
<span th:unless="${error}">Validations:</span>
<ul class="list-none error-list-dot">
<li th:each="message : ${messageList}" th:text="'→ ' + ${message}"/>
</ul>
</th:block>
</th:block>
</div>
</th:block>
</body>
</html>