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:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package ltd.hlaeja.controller.validation
|
||||||
|
|
||||||
|
interface CreateGroup
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package ltd.hlaeja.controller.validation
|
||||||
|
|
||||||
|
interface EditGroup
|
||||||
@@ -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>> = [],
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
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
|
||||||
|
|
||||||
|
@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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
25
src/main/resources/templates/messages.html
Normal file
25
src/main/resources/templates/messages.html
Normal 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>
|
||||||
Reference in New Issue
Block a user