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 4013bd6905
22 changed files with 809 additions and 344 deletions

View File

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

View File

@@ -1,10 +1,11 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import java.util.UUID import java.util.UUID
import ltd.hlaeja.controller.validation.CreateGroup
import ltd.hlaeja.controller.validation.EditGroup
import ltd.hlaeja.dto.Pagination import ltd.hlaeja.dto.Pagination
import ltd.hlaeja.exception.NoChangeException import ltd.hlaeja.exception.NoChangeException
import ltd.hlaeja.exception.NotFoundException import ltd.hlaeja.exception.NotFoundException
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
@@ -12,6 +13,8 @@ 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
import org.springframework.validation.BindingResult
import org.springframework.validation.annotation.Validated
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.ModelAttribute
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
@@ -36,6 +39,7 @@ class AccountController(
): Mono<String> = accountRegistryService.getAccount(account) ): Mono<String> = accountRegistryService.getAccount(account)
.doOnNext { .doOnNext {
model.addAttribute("account", account) model.addAttribute("account", account)
model.addAttribute("roleGroups", accountRegistryService.getRoles())
model.addAttribute("accountForm", it.toAccountForm()) model.addAttribute("accountForm", it.toAccountForm())
} }
.then(Mono.just("account/edit")) .then(Mono.just("account/edit"))
@@ -43,65 +47,93 @@ class AccountController(
@PostMapping("/edit-{account}") @PostMapping("/edit-{account}")
fun postEditAccount( fun postEditAccount(
@PathVariable account: UUID, @PathVariable account: UUID,
@ModelAttribute("accountForm") accountForm: AccountForm, @Validated(EditGroup::class) @ModelAttribute("accountForm") accountForm: AccountForm,
bindingResult: BindingResult,
model: Model, model: Model,
): Mono<String> = Mono.just(accountForm) ): Mono<String> {
.flatMap { val validationErrors = if (bindingResult.hasErrors()) {
accountRegistryService.updateAccount( bindingResult.allErrors.map { error ->
account, error.defaultMessage ?: "Unknown validation error"
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.")
} }
model.addAttribute(errorMessage.first, errorMessage.second) } else {
model.addAttribute("accountForm", accountForm) emptyList()
model.addAttribute("account", account)
Mono.just("account/edit")
} }
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") @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("", "")) } .doOnNext {
model.addAttribute("accountForm", AccountForm("", emptyList()))
model.addAttribute("roleGroups", accountRegistryService.getRoles())
}
@PostMapping("/create") @PostMapping("/create")
fun postCreateAccount( fun postCreateAccount(
@ModelAttribute("accountForm") accountForm: AccountForm, @Validated(CreateGroup::class) @ModelAttribute("accountForm") accountForm: AccountForm,
bindingResult: BindingResult,
model: Model, model: Model,
): Mono<String> = Mono.just(accountForm) ): Mono<String> {
.flatMap { val validationErrors = if (bindingResult.hasErrors()) {
accountRegistryService.addAccount( bindingResult.allErrors.map { error ->
it.toAccountRequest { password -> error.defaultMessage ?: "Unknown validation error"
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."
} }
model.addAttribute("errorMessage", errorMessage) } else {
Mono.just("account/create") 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 @GetMapping
fun getDefaultAccounts( 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") @Suppress("unused")
data class Pagination( data class Pagination(
val page: Int, val page: Int,
val size: Int, val show: Int,
val items: Int, val items: Int,
val defaultSize: Int, val defaultSize: Int,
) { ) {
val hasMore: Boolean = size == items val hasMore: Boolean = show == items
val showSize: Boolean = size != defaultSize val showSize: Boolean = show != defaultSize
val first: Boolean = page <= 1 val first: Boolean = page <= 1
val previous: Int = page - 1 val previous: Int = page - 1
val next: Int = page + 1 val next: Int = page + 1
val start: Int = (page - 1) * size + 1 val start: Int = previous * show + 1
val end: Int = page * size val end: Int = page * show
val size: Int = previous * show + items
} }

View File

@@ -1,9 +1,21 @@
package ltd.hlaeja.form 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
import org.springframework.validation.annotation.Validated
@Validated
@PasswordMatch(groups = [CreateGroup::class, EditGroup::class])
data class AccountForm( data class AccountForm(
val username: String, @field:NotEmpty(message = "Username cannot be empty", groups = [CreateGroup::class, EditGroup::class])
val role: String, var username: String,
val enabled: Boolean = false, @field:NotEmpty(message = "At least one role must be selected", groups = [CreateGroup::class, EditGroup::class])
val password: CharSequence? = null, var roles: List<String> = emptyList(),
val passwordConfirm: CharSequence? = null, 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)) 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, credentials as String,
) )
fun AccountForm.toAccountRequest(operation: (CharSequence?) -> CharSequence?): Account.Request = Account.Request( fun AccountForm.toAccountRequest(): Account.Request = Account.Request(
username = username, username = username,
password = operation(password), password = if (password.isNullOrEmpty()) null else password,
enabled = enabled, enabled = enabled,
roles = listOf("ROLE_${role.uppercase()}"), roles = roles.map { "ROLE_${it.uppercase()}" },
) )
fun Account.Response.toAccountForm(): AccountForm = AccountForm( fun Account.Response.toAccountForm(): AccountForm = AccountForm(
username = username, username = username,
enabled = enabled, 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 { body {
background-color: #000000; font-family: 'JetBrains Mono', monospace;
color: #04931b; }
::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"> <html lang="en" xmlns:th="http://www.thymeleaf.org">
<head> <!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<title>Home Pages</title> <body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:insert="~{layout.html :: documentHead}"/>/*/--> <!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
</head> <main class="container mx-auto p-4 flex-grow flex items-center justify-center">
<body> <div class="w-full max-w-md">
<main> <h2 class="text-lg sm:text-xl mb-4 terminal-glow">New Account Registration</h2>
<h1>Test create user</h1> <hr class="border-green-900 mb-4">
<hr> <th:block th:replace="~{messages :: messageDisplay(messageList=${validationErrors}, error=true, styleClass='text-red-600')}"/>
<form th:action="@{/account/create}" th:method="post"> <form th:action="@{/account/create}" th:method="post">
<!-- Display error message if present --> <div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<div th:if="${errorMessage != null}" style="color: red; margin-bottom: 10px;"> <div class="mb-4">
<span th:text="${errorMessage}">Error Message</span> <label for="username" class="block text-sm mb-2">Username</label>
</div> <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>
<!-- Username --> <div class="mb-4">
<div> <label for="roles" class="block text-sm mb-2">Roles</label>
<label for="username">Username:</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">
<input type="text" id="username" name="username" th:field="*{accountForm.username}" required/> <optgroup th:each="group : ${roleGroups}" th:label="${group.key}" class="text-green-600">
</div> <option th:each="role : ${group.value}" th:value="${role}" th:text="${role}" class="text-green-400"/>
</optgroup>
<!-- Password --> </select>
<div> <p class="text-xs text-green-600 mt-1">[Hold Ctrl/Cmd to select multiple]</p>
<label for="password">Password:</label> </div>
<input type="password" id="password" name="password" required/> <div class="mb-4">
</div> <label for="enabled" class="block text-sm mb-2">Enabled</label>
<div class="switch-wrapper">
<!-- Re-enter Password --> <input th:checked="${accountForm.enabled}" value="true" id="enabled" name="enabled" type="checkbox">
<div> <span class="switch-slider border-green-900"></span>
<label for="passwordConfirm">Re-enter Password:</label> </div>
<input type="password" id="passwordConfirm" name="passwordConfirm" required/> </div>
<span id="passwordMatchMessage"></span> <div class="mb-4">
</div> <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">
<!-- Role --> </div>
<div> <div class="mb-4">
<label for="role">Role:</label> <label class="block text-sm mb-2" for="passwordConfirm">Confirm password</label>
<select id="role" name="role" th:field="*{accountForm.role}" required> <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">
<option value="user">User</option> <span id="passwordMatchMessage" class="text-xs mt-1"></span>
<option value="registered">Registered</option> </div>
<option value="admin">Admin</option> </div>
</select> <div class="mt-4 flex justify-end space-x-4">
</div> <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>
<!-- Enabled --> </div>
<div> </form>
<label for="enabled">Enabled:</label> </div>
<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> </main>
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/--> <!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<script> <script>
// Get password fields document.addEventListener('DOMContentLoaded', () => {
const password = document.getElementById('password'); passwordMatchCheck(
const passwordConfirm = document.getElementById('passwordConfirm'); document.getElementById('password'),
const passwordMatchMessage = document.getElementById('passwordMatchMessage'); document.getElementById('passwordConfirm'),
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> </script>
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
</body> </body>
</html> </html>

View File

@@ -1,96 +1,71 @@
<!DOCTYPE HTML> <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <html lang="en" xmlns:th="http://www.thymeleaf.org">
<head> <!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<title>Home Pages</title> <body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:insert="~{layout.html :: documentHead}"/>/*/--> <!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
</head> <main class="container mx-auto p-4 flex-grow flex items-center justify-center">
<body> <div class="w-full max-w-4xl">
<main> <form th:action="@{/account/edit-{account}(account = ${account})}" th:method="post">
<h1>Test edit user <strong th:text="${accountForm.username}">username</strong></h1> <div class="mb-4">
<hr> <h1 class="text-lg sm:text-xl mb-4 terminal-glow">Edit User <strong th:text="${accountForm.username}"/></h1>
<form th:action="@{/account/edit-{account}(account = ${account})}" th:method="post"> <hr class="border-green-900 mb-4">
<!-- Display error message if present --> <th:block th:replace="~{messages :: messageDisplay(messageList=${validationErrors}, error=true, styleClass='text-red-600')}"/>
<div th:if="${errorMessage != null}" style="color: red; margin-bottom: 10px;"> <th:block th:replace="~{messages :: messageDisplay(messageList=${successMessage}, error=false, styleClass='text-green-600')}"/>
<span th:text="${errorMessage}">Error Message</span> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
</div> <div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<div th:if="${successMessage != null}" style="color: blue; margin-bottom: 10px;"> <h2 class="text-lg sm:text-xl mb-4 terminal-glow">Account Details</h2>
<span th:text="${successMessage}">success Message</span> <div class="mb-4">
</div> <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">
<!-- Username --> </div>
<div> <div class="mb-4">
<label for="username">Username:</label> <label class="block text-sm mb-2" for="roles">Role:</label>
<input type="text" id="username" name="username" th:field="*{accountForm.username}" required/> <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">
</div> <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"/>
<!-- Role --> </optgroup>
<div> </select>
<label for="role">Role:</label> <p class="text-xs text-green-600 mt-1">[Hold Ctrl/Cmd to select multiple]</p>
<select id="role" name="role" th:field="*{accountForm.role}" required> </div>
<option value="user">User</option> <div class="mb-4">
<option value="registered">Registered</option> <label class="block text-sm mb-2" for="enabled">Enabled:</label>
<option value="admin">Admin</option> <div class="switch-wrapper">
</select> <input th:checked="${accountForm.enabled}" value="true" id="enabled" name="enabled" type="checkbox">
</div> <span class="switch-slider border-green-900"></span>
</div>
<!-- Enabled --> </div>
<div> </div>
<label for="enabled">Enabled:</label> <div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<input type="checkbox" id="enabled" name="enabled" th:field="*{accountForm.enabled}"/> <h2 class="text-lg sm:text-xl mb-4 terminal-glow">Password Update</h2>
</div> <div class="mb-4">
<label class="block text-sm mb-2" for="password">Password:</label>
<hr> <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>
<!-- Password --> <div class="mb-4">
<div> <label class="block text-sm mb-2" for="passwordConfirm">Re-enter Password:</label>
<label for="password">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">
<input type="password" id="password" name="password"/> <span id="passwordMatchMessage" class="text-sm text-green-600"></span>
</div> </div>
</div>
<!-- Re-enter Password --> </div>
<div> <div class="mt-4 flex justify-end space-x-4">
<label for="passwordConfirm">Re-enter Password:</label> <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>
<input type="password" id="passwordConfirm" name="passwordConfirm"/> <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>
<span id="passwordMatchMessage"></span> </div>
</div> </div>
<hr> </form>
</div>
<!-- Submit Button -->
<button type="submit">Update User</button>
</form>
<br>
<a href="/account">Account</a>
<a href="/logout">Logout</a><br>
</main> </main>
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/--> <!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<script> <script>
// Get password fields document.addEventListener('DOMContentLoaded', () => {
const password = document.getElementById('password'); passwordMatchCheck(
const passwordConfirm = document.getElementById('passwordConfirm'); document.getElementById('password'),
const passwordMatchMessage = document.getElementById('passwordMatchMessage'); document.getElementById('passwordConfirm'),
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> </script>
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
</body> </body>
</html> </html>

View File

@@ -1,44 +1,67 @@
<!DOCTYPE HTML> <!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <html lang="en" xmlns:th="http://www.thymeleaf.org">
<head> <!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<title>Home Pages</title> <body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:insert="~{layout.html :: documentHead}"/>/*/--> <!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
</head> <main class="container mx-auto p-4 flex-grow">
<body> <div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<main> <h1 class="text-lg sm:text-xl mb-4 terminal-glow">Account Registry</h1>
<h1>Test</h1> <hr class="border-green-900 mb-4">
<hr> <div class="flex justify-between items-center mb-4">
<div>Show page <span th:text="${pagination.page}"/> items <span th:text="${pagination.start}"/> - <span th:text="${pagination.end}"/></div> <div th:if="${pagination.start > pagination.size}" class="text-sm">
<table> Show page <span th:text="${pagination.page}"/> items 0 - 0
<tr> </div>
<th>Id</th> <div th:unless="${pagination.start > pagination.size}" class="text-sm">
<th>Name</th> Show page <span th:text="${pagination.page}"/>
<th>Description</th> items <span th:text="${pagination.start}"/> -
<th>Actions</th> <span th:text="${pagination.size}"/>
</tr> </div>
<tr th:each="item : ${items}"> <div class="mt-[-2px]">
<td th:text="${item.id}">ID</td> <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>
<td th:text="${item.timestamp}">timestamp</td> </div>
<td th:text="${item.username}">username</td> </div>
<td><a th:href="@{/account/edit-{id}(id = ${item.id})}">Edit</a></td> <div class="overflow-x-auto">
</tr> <table class="w-full text-sm">
</table> <tr class="border-b border-green-900">
<div th:if="${pagination.showSize}"> <th class="py-2 px-4 text-left">Username</th>
<span th:if="${pagination.first}">Previous</span> <th class="py-2 px-4 text-left">Timestamp</th>
<a th:unless="${pagination.first}" th:href="@{'/account/page-' + ${pagination.previous} + '/show-' + ${size}}">Previous</a> <th class="py-2 px-4 text-left">Id</th>
<a th:if="${pagination.hasMore}" th:href="@{'/account/page-' + ${pagination.next} + '/show-' + ${size}}">Next</a> <th class="py-2 px-4 text-left">Actions</th>
<span th:unless="${pagination.hasMore}">Next</span> </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>
<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> </main>
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/--> <!--/*/<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> </body>
</html> </html>

View File

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

View File

@@ -1,23 +1,42 @@
<!DOCTYPE HTML> <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <html lang="en" xmlns:th="http://www.thymeleaf.org">
<head> <!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<title>Login</title> <body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:insert="~{layout.html :: documentHead}"/>/*/--> <!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
</head> <!-- Main Content -->
<body> <main class="container mx-auto p-4 flex-grow flex items-center justify-center">
<main> <div class="w-full max-w-md">
<h1>Login</h1> <div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<hr> <h2 class="text-lg sm:text-xl mb-4 terminal-glow">Authentication Required</h2>
<form id="loginForm" th:action="@{/login}" th:method="post"> <div id="login-error" class="mb-4 text-red-600 text-sm hidden">
<label for="username" >Username</label> <span>Error: </span>Bad username or password!
<input type="text" id="username" name="username" placeholder="Enter your username" required> </div>
<br> <form th:action="@{/login}" th:method="post">
<label for="password">Password</label> <div class="mb-4">
<input type="password" id="password" name="password" placeholder="Enter your password" required> <label class="block text-sm mb-2" for="username">username</label>
<br> <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...">
<button type="submit">Login</button> </div>
</form> <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> </main>
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/--> <!--/*/<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> </body>
</html> </html>

View File

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

View File

@@ -1,16 +1,72 @@
<!DOCTYPE HTML> <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <html lang="en" xmlns:th="http://www.thymeleaf.org">
<head> <!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<title>Home Pages</title> <body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:insert="~{layout.html :: documentHead}"/>/*/--> <!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
</head>
<body> <!-- Main Content -->
<main> <main class="container mx-auto p-4 flex-grow">
<h1>Test</h1> <!-- Main Grid -->
<hr> <div class="grid grid-cols-1 md:grid-cols-12 gap-4">
<p>This is a index page!</p> <!-- Sidebar -->
<a href="/login">login</a> <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> </main>
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/--> <!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
</body> </body>
</html> </html>

View File

@@ -1,24 +1,72 @@
<!DOCTYPE HTML> <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <html lang="en" xmlns:th="http://www.thymeleaf.org">
<head> <!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
<title>Home Pages</title> <body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<!--/*/<th:block th:insert="~{layout.html :: documentHead}"/>/*/--> <!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
</head>
<body> <!-- Main Content -->
<main> <main class="container mx-auto p-4 flex-grow">
<h1>Welcome</h1> <!-- Main Grid -->
<hr> <div class="grid grid-cols-1 md:grid-cols-12 gap-4">
<!--/*@thymesVar id="remoteUser" type="ltd.hlaeja.security.RemoteAuthentication"*/--> <!-- Sidebar -->
<div th:if="${remoteUser.hasRole('admin')}"> <div class="md:col-span-3">
You are an admin! <div class="bg-gray-800 p-4 rounded-lg border border-green-900">
<a href="/account">Account</a> <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>
<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> </div>
<p>This is welcome pages and you're a user!</p>
<a href="/logout">Logout</a>
</main> </main>
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/--> <!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
</body> </body>
</html> </html>

View File

@@ -1,10 +1,53 @@
<!DOCTYPE HTML> <!DOCTYPE html>
<html lang="" xmlns:th="http://www.thymeleaf.org"> <html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="documentHead"> <head th:fragment="documentHead (title)">
<meta charset="UTF-8"> <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"> <link th:href="@{/css/management.css}" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
</head> </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"> <div th:fragment="script">
<script th:src="@{/js/management.js}"></script> <script th:src="@{/js/management.js}"></script>
</div> </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>