diff --git a/build.gradle.kts b/build.gradle.kts index 60c2513..f402985 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.security) implementation(hlaeja.springboot.starter.thymeleaf) + implementation(hlaeja.springboot.starter.validation) implementation(hlaeja.springboot.starter.webflux) implementation(hlaeja.thymeleaf.spring.security) diff --git a/src/main/kotlin/ltd/hlaeja/controller/AccountController.kt b/src/main/kotlin/ltd/hlaeja/controller/AccountController.kt index 6d4e20a..7209a48 100644 --- a/src/main/kotlin/ltd/hlaeja/controller/AccountController.kt +++ b/src/main/kotlin/ltd/hlaeja/controller/AccountController.kt @@ -1,10 +1,11 @@ package ltd.hlaeja.controller import java.util.UUID +import ltd.hlaeja.controller.validation.CreateGroup +import ltd.hlaeja.controller.validation.EditGroup import ltd.hlaeja.dto.Pagination import ltd.hlaeja.exception.NoChangeException import ltd.hlaeja.exception.NotFoundException -import ltd.hlaeja.exception.PasswordException import ltd.hlaeja.exception.UsernameDuplicateException import ltd.hlaeja.form.AccountForm import ltd.hlaeja.service.AccountRegistryService @@ -12,6 +13,8 @@ import ltd.hlaeja.util.toAccountForm import ltd.hlaeja.util.toAccountRequest import org.springframework.stereotype.Controller import org.springframework.ui.Model +import org.springframework.validation.BindingResult +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.PathVariable @@ -36,6 +39,7 @@ class AccountController( ): Mono = accountRegistryService.getAccount(account) .doOnNext { model.addAttribute("account", account) + model.addAttribute("roleGroups", accountRegistryService.getRoles()) model.addAttribute("accountForm", it.toAccountForm()) } .then(Mono.just("account/edit")) @@ -43,65 +47,93 @@ class AccountController( @PostMapping("/edit-{account}") fun postEditAccount( @PathVariable account: UUID, - @ModelAttribute("accountForm") accountForm: AccountForm, + @Validated(EditGroup::class) @ModelAttribute("accountForm") accountForm: AccountForm, + bindingResult: BindingResult, model: Model, - ): Mono = Mono.just(accountForm) - .flatMap { - accountRegistryService.updateAccount( - account, - it.toAccountRequest { password -> if (password.isNullOrEmpty()) null else password }, - ) - } - .doOnNext { - model.addAttribute("successMessage", "Saved changes!!!") - model.addAttribute("account", account) - model.addAttribute("accountForm", it.toAccountForm()) - } - .then(Mono.just("account/edit")) - .onErrorResume { error -> - val errorMessage = when (error) { - is NoChangeException -> Pair("successMessage", "No change to save") - is NotFoundException -> Pair("errorMessage", "User dont exists. how did this happen?") - is UsernameDuplicateException -> Pair("errorMessage", "Username already exists. Please choose another.") - else -> Pair("errorMessage", "An unexpected error occurred. Please try again later.") + ): Mono { + val validationErrors = if (bindingResult.hasErrors()) { + bindingResult.allErrors.map { error -> + error.defaultMessage ?: "Unknown validation error" } - model.addAttribute(errorMessage.first, errorMessage.second) - model.addAttribute("accountForm", accountForm) - model.addAttribute("account", account) - Mono.just("account/edit") + } else { + emptyList() } + if (bindingResult.hasErrors()) { + model.addAttribute("accountForm", accountForm) + model.addAttribute("validationErrors", validationErrors) + model.addAttribute("roleGroups", accountRegistryService.getRoles()) + return Mono.just("account/edit") + } + + return Mono.just(accountForm) + .flatMap { accountRegistryService.updateAccount(account, it.toAccountRequest()) } + .doOnNext { + model.addAttribute("successMessage", listOf("Saved changes!!!")) + model.addAttribute("account", account) + model.addAttribute("accountForm", it.toAccountForm()) + model.addAttribute("roleGroups", accountRegistryService.getRoles()) + } + .then(Mono.just("account/edit")) + .onErrorResume { error -> + val errorMessage = when (error) { + is NoChangeException -> Pair("successMessage", "No change to save.") + is NotFoundException -> Pair("validationErrors", "User dont exists. how did this happen?") + is UsernameDuplicateException -> Pair( + "validationErrors", + "Username already exists. Please choose another.", + ) + else -> Pair("validationErrors", "An unexpected error occurred. Please try again later.") + } + + model.addAttribute(errorMessage.first, listOf(errorMessage.second)) + model.addAttribute("account", account) + model.addAttribute("accountForm", accountForm) + model.addAttribute("roleGroups", accountRegistryService.getRoles()) + Mono.just("account/edit") + } + } @GetMapping("/create") fun getCreateAccount( model: Model, ): Mono = Mono.just("account/create") - .doOnNext { model.addAttribute("accountForm", AccountForm("", "")) } + .doOnNext { + model.addAttribute("accountForm", AccountForm("", emptyList())) + model.addAttribute("roleGroups", accountRegistryService.getRoles()) + } @PostMapping("/create") fun postCreateAccount( - @ModelAttribute("accountForm") accountForm: AccountForm, + @Validated(CreateGroup::class) @ModelAttribute("accountForm") accountForm: AccountForm, + bindingResult: BindingResult, model: Model, - ): Mono = Mono.just(accountForm) - .flatMap { - accountRegistryService.addAccount( - it.toAccountRequest { password -> - when { - password.isNullOrEmpty() -> throw PasswordException("Password requirements failed") - else -> password - } - }, - ) - } - .map { "redirect:/account" } - .onErrorResume { error -> - val errorMessage = when (error) { - is UsernameDuplicateException -> "Username already exists. Please choose another." - is PasswordException -> error.message - else -> "An unexpected error occurred. Please try again later." + ): Mono { + val validationErrors = if (bindingResult.hasErrors()) { + bindingResult.allErrors.map { error -> + error.defaultMessage ?: "Unknown validation error" } - model.addAttribute("errorMessage", errorMessage) - Mono.just("account/create") + } else { + emptyList() } + if (bindingResult.hasErrors()) { + model.addAttribute("accountForm", accountForm) + model.addAttribute("validationErrors", validationErrors) + model.addAttribute("roleGroups", accountRegistryService.getRoles()) + return Mono.just("account/create") + } + return Mono.just(accountForm) + .flatMap { accountRegistryService.addAccount(it.toAccountRequest()) } + .map { "redirect:/account" } + .onErrorResume { error -> + val errorMessage = when (error) { + is UsernameDuplicateException -> "Username already exists. Please choose another." + else -> "An unexpected error occurred. Please try again later." + } + model.addAttribute("validationErrors", listOf(errorMessage)) + model.addAttribute("roleGroups", accountRegistryService.getRoles()) + Mono.just("account/create") + } + } @GetMapping fun getDefaultAccounts( diff --git a/src/main/kotlin/ltd/hlaeja/controller/validation/CreateGroup.kt b/src/main/kotlin/ltd/hlaeja/controller/validation/CreateGroup.kt new file mode 100644 index 0000000..6b0ed6a --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/controller/validation/CreateGroup.kt @@ -0,0 +1,3 @@ +package ltd.hlaeja.controller.validation + +interface CreateGroup diff --git a/src/main/kotlin/ltd/hlaeja/controller/validation/EditGroup.kt b/src/main/kotlin/ltd/hlaeja/controller/validation/EditGroup.kt new file mode 100644 index 0000000..54e6b8b --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/controller/validation/EditGroup.kt @@ -0,0 +1,3 @@ +package ltd.hlaeja.controller.validation + +interface EditGroup diff --git a/src/main/kotlin/ltd/hlaeja/controller/validation/PasswordMatch.kt b/src/main/kotlin/ltd/hlaeja/controller/validation/PasswordMatch.kt new file mode 100644 index 0000000..dd59bf8 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/controller/validation/PasswordMatch.kt @@ -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> = [], + val payload: Array> = [], +) diff --git a/src/main/kotlin/ltd/hlaeja/controller/validation/PasswordMatchValidator.kt b/src/main/kotlin/ltd/hlaeja/controller/validation/PasswordMatchValidator.kt new file mode 100644 index 0000000..8b19695 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/controller/validation/PasswordMatchValidator.kt @@ -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 { + 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 + } + } +} diff --git a/src/main/kotlin/ltd/hlaeja/dto/Pagination.kt b/src/main/kotlin/ltd/hlaeja/dto/Pagination.kt index 2e625a1..39656f1 100644 --- a/src/main/kotlin/ltd/hlaeja/dto/Pagination.kt +++ b/src/main/kotlin/ltd/hlaeja/dto/Pagination.kt @@ -3,15 +3,16 @@ package ltd.hlaeja.dto @Suppress("unused") data class Pagination( val page: Int, - val size: Int, + val show: Int, val items: Int, val defaultSize: Int, ) { - val hasMore: Boolean = size == items - val showSize: Boolean = size != defaultSize + val hasMore: Boolean = show == items + val showSize: Boolean = show != defaultSize val first: Boolean = page <= 1 val previous: Int = page - 1 val next: Int = page + 1 - val start: Int = (page - 1) * size + 1 - val end: Int = page * size + val start: Int = previous * show + 1 + val end: Int = page * show + val size: Int = previous * show + items } diff --git a/src/main/kotlin/ltd/hlaeja/form/AccountForm.kt b/src/main/kotlin/ltd/hlaeja/form/AccountForm.kt index d2139d0..1bae8dd 100644 --- a/src/main/kotlin/ltd/hlaeja/form/AccountForm.kt +++ b/src/main/kotlin/ltd/hlaeja/form/AccountForm.kt @@ -1,9 +1,21 @@ 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( - val username: String, - val role: String, - val enabled: Boolean = false, - val password: CharSequence? = null, - val passwordConfirm: CharSequence? = null, + @field:NotEmpty(message = "Username cannot be empty", groups = [CreateGroup::class, EditGroup::class]) + var username: String, + @field:NotEmpty(message = "At least one role must be selected", groups = [CreateGroup::class, EditGroup::class]) + var roles: List = emptyList(), + var enabled: Boolean = false, + @field:NotEmpty(message = "Password cannot be empty", groups = [CreateGroup::class]) + var password: CharSequence? = null, + @field:NotEmpty(message = "Password confirmation cannot be empty", groups = [CreateGroup::class]) + var passwordConfirm: CharSequence? = null, ) diff --git a/src/main/kotlin/ltd/hlaeja/service/AccountRegistryService.kt b/src/main/kotlin/ltd/hlaeja/service/AccountRegistryService.kt index 1b9721f..68bd518 100644 --- a/src/main/kotlin/ltd/hlaeja/service/AccountRegistryService.kt +++ b/src/main/kotlin/ltd/hlaeja/service/AccountRegistryService.kt @@ -90,4 +90,11 @@ class AccountRegistryService( else -> Mono.error(ResponseStatusException(BAD_REQUEST, error.message)) } } + + // TODO implement user gropes and access + fun getRoles(): Map> = mapOf( + "Admin Group" to listOf("Admin"), + "Operations Group" to listOf("Registry"), + "User Group" to listOf("User"), + ) } diff --git a/src/main/kotlin/ltd/hlaeja/util/Mapping.kt b/src/main/kotlin/ltd/hlaeja/util/Mapping.kt index 44c558e..c68b40e 100644 --- a/src/main/kotlin/ltd/hlaeja/util/Mapping.kt +++ b/src/main/kotlin/ltd/hlaeja/util/Mapping.kt @@ -10,15 +10,19 @@ fun SpringAuthentication.toAuthenticationRequest(): Authentication.Request = Aut credentials as String, ) -fun AccountForm.toAccountRequest(operation: (CharSequence?) -> CharSequence?): Account.Request = Account.Request( +fun AccountForm.toAccountRequest(): Account.Request = Account.Request( username = username, - password = operation(password), + password = if (password.isNullOrEmpty()) null else password, enabled = enabled, - roles = listOf("ROLE_${role.uppercase()}"), + roles = roles.map { "ROLE_${it.uppercase()}" }, ) fun Account.Response.toAccountForm(): AccountForm = AccountForm( username = username, enabled = enabled, - role = roles.first().removePrefix("ROLE_").lowercase(), + roles = roles.map { + it.removePrefix("ROLE_") + .lowercase() + .replaceFirstChar { char -> char.uppercase() } + }, ) diff --git a/src/main/resources/static/css/management.css b/src/main/resources/static/css/management.css index ed5831d..9b15c1e 100644 --- a/src/main/resources/static/css/management.css +++ b/src/main/resources/static/css/management.css @@ -1,4 +1,135 @@ + +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap'); + body { - background-color: #000000; - color: #04931b; + font-family: 'JetBrains Mono', monospace; +} + +::selection { + background-color: #065f46; + color: #ffffff; +} + +.terminal-glow { + text-shadow: 0 0 5px rgba(0, 255, 0, 0.5); +} + +/* Dropdown Divider Styling */ +.dropdown-divider { + border: 0; + border-top: 1px solid #065f46; + margin: 0.5rem 0; +} + +/* Custom Scrollbar Styling */ +::-webkit-scrollbar { + width: 16px; +} + +::-webkit-scrollbar-track { + background: #1f2937; + border-left: 1px solid #065f46; + border-radius: 0; +} + +::-webkit-scrollbar-thumb { + background: #047857; + border-radius: 0; +} + +::-webkit-scrollbar-thumb:hover { + background: #10b981; +} + +* { + scrollbar-width: medium; + scrollbar-color: #047857 #1f2937; +} + +.scrollable-no-radius-right { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + overflow-y: scroll !important; +} + +/* Custom Switch Styling */ +.switch-wrapper { + position: relative; + display: inline-block; + width: 40px; + height: 20px; +} + +.switch-wrapper input { + opacity: 0; + width: 100%; + height: 100%; + position: absolute; + z-index: 1; + cursor: pointer; +} + +.switch-slider { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #4b5563; + border: 1px solid; + transition: background-color 0.4s; + border-radius: 20px; +} + +.switch-slider:before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 2px; + bottom: 2px; + background-color: #9ca3af; + transition: transform 0.4s; + border-radius: 50%; +} + +.switch-wrapper input:checked + .switch-slider { + background-color: #047857; +} + +.switch-wrapper input:checked + .switch-slider:before { + transform: translateX(20px); + background-color: #10b981; +} + +/* Focus state for switch */ +.switch-wrapper input:focus + .switch-slider { + --tw-border-opacity: 1; + border-color: rgb(22 163 74 / var(--tw-border-opacity, 1)); /* green-600, matches focus:border-green-600 */ +} + +/*!* Glow effect on focus for all form elements *!*/ +/*input:focus,*/ +/*select:focus,*/ +/*button:focus,*/ +/*.switch-wrapper input:focus + .switch-slider {*/ +/* box-shadow: 0 0 5px rgba(0, 255, 0, 0.5);*/ +/* outline: none;*/ +/*}*/ + +/* Selected Option Styling */ + +select option:checked { + background-color: #064e3b; + color: #BBF7D0FF; +} + +select:focus option:checked { + background: #065f46 linear-gradient(0deg, #065f46 0%, #065f46 100%); +} + +select:focus option:hover, +select option:hover { + background-color: #11a36f !important; + color: #fff; } diff --git a/src/main/resources/static/js/management.js b/src/main/resources/static/js/management.js index e69de29..0d9c5ac 100644 --- a/src/main/resources/static/js/management.js +++ b/src/main/resources/static/js/management.js @@ -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); + } +} diff --git a/src/main/resources/templates/account/create.html b/src/main/resources/templates/account/create.html index fdbfc3f..70ea18f 100644 --- a/src/main/resources/templates/account/create.html +++ b/src/main/resources/templates/account/create.html @@ -1,90 +1,62 @@ - + - - Home Pages - - - -
-

Test create user

-
-
- -
- Error Message -
- - -
- - -
- - -
- - -
- - -
- - - -
- - -
- - -
- - -
- - -
- - - -
-
- Account - Logout
+ + + +
+
+

New Account Registration

+
+ +
+
+
+ + +
+
+ + +

[Hold Ctrl/Cmd to select multiple]

+
+
+ +
+ + +
+
+
+ + +
+
+ + + +
+
+
+ Cancel + +
+
+
- + + diff --git a/src/main/resources/templates/account/edit.html b/src/main/resources/templates/account/edit.html index fc303c2..d5ead27 100644 --- a/src/main/resources/templates/account/edit.html +++ b/src/main/resources/templates/account/edit.html @@ -1,96 +1,71 @@ - + - - Home Pages - - - -
-

Test edit user username

-
-
- -
- Error Message -
-
- success Message -
- - -
- - -
- - -
- - -
- - -
- - -
- -
- - -
- - -
- - -
- - - -
-
- - - -
-
- Account - Logout
+ + + +
+
+
+
+

Edit User

+
+ + +
+
+

Account Details

+
+ + +
+
+ + +

[Hold Ctrl/Cmd to select multiple]

+
+
+ +
+ + +
+
+
+
+

Password Update

+
+ + +
+
+ + + +
+
+
+
+ Cancel + +
+
+ +
- + + diff --git a/src/main/resources/templates/account/users.html b/src/main/resources/templates/account/users.html index dabde49..b867e9d 100644 --- a/src/main/resources/templates/account/users.html +++ b/src/main/resources/templates/account/users.html @@ -1,44 +1,67 @@ - - Home Pages - - - -
-

Test

-
-
Show page items -
- - - - - - - - - - - - - -
IdNameDescriptionActions
IDtimestampusernameEdit
-
- Previous - Previous - Next - Next + + + +
+
+

Account Registry

+
+
+
+ Show page items 0 - 0 +
+
+ Show page + items - + +
+ +
+
+ + + + + + + + + + + + + + + + +
UsernameTimestampIdActions
No accounts found
usernameLoading...IDEdit
+
+
+
+ Previous + Previous + Next + Next +
+
+ Previous + Previous + Next + Next +
+
-
- Previous - Previous - Next - Next -
-
- Create Account
- Logout
+ + diff --git a/src/main/resources/templates/authentication/goodbye.html b/src/main/resources/templates/authentication/goodbye.html index fda425c..be72b2b 100644 --- a/src/main/resources/templates/authentication/goodbye.html +++ b/src/main/resources/templates/authentication/goodbye.html @@ -1,16 +1,28 @@ - + - - Goodbye - - - -
-

You are logged out

-
-

We hope to see you again soon!

- Login Again + + + +
+
+
+

Session Terminated

+
+> logout initiated...
+> Saving session data... [OK]
+> Closing connections... [OK]
+> System cleanup complete... [OK]
+
+Thank you for using Hlaeja Systems.
+Goodbye.
+      
+
+ Reconnect +
+
+
+ diff --git a/src/main/resources/templates/authentication/login.html b/src/main/resources/templates/authentication/login.html index a310c14..94d41d3 100644 --- a/src/main/resources/templates/authentication/login.html +++ b/src/main/resources/templates/authentication/login.html @@ -1,23 +1,42 @@ - + - - Login - - - -
-

Login

-
-
- - -
- - -
- -
+ + + + +
+
+
+

Authentication Required

+ +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + diff --git a/src/main/resources/templates/authentication/logout.html b/src/main/resources/templates/authentication/logout.html index 20c9bf1..2105f0b 100644 --- a/src/main/resources/templates/authentication/logout.html +++ b/src/main/resources/templates/authentication/logout.html @@ -1,17 +1,24 @@ - + - - Logout - - - -
-

Logout

-
-

Are you sure you want to logout?

-
- + + + +
+
+
+

Confirm System Logout

+

Are you sure you want to terminate your session?

+
+ Cancel + +
+ +
+
+
+
+ diff --git a/src/main/resources/templates/home/index.html b/src/main/resources/templates/home/index.html index 8df7f1d..d8352d1 100644 --- a/src/main/resources/templates/home/index.html +++ b/src/main/resources/templates/home/index.html @@ -1,16 +1,72 @@ - + - - Home Pages - - - -
-

Test

-
-

This is a index page!

- login + + + + + +
+ +
+ +
+
+

System Commands

+
    +
  • $ status_check
  • +
  • $ sys_reboot
  • +
  • $ mem_info
  • +
  • $ net_stat
  • +
+
+
+ + +
+
+

Terminal Output

+
+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]
+        
+
+
+ + +
+
+

Status

+
+

CPU: Online

+

Memory: Stable

+

Network: Connected

+

Uptime: 23h 45m

+
+
+
+
+ + +
+
+
+ $ + +
+
+
+ + diff --git a/src/main/resources/templates/home/welcome.html b/src/main/resources/templates/home/welcome.html index af271bf..d8352d1 100644 --- a/src/main/resources/templates/home/welcome.html +++ b/src/main/resources/templates/home/welcome.html @@ -1,24 +1,72 @@ - + - - Home Pages - - - -
-

Welcome

-
- -
- You are an admin! - Account + + + + + +
+ +
+ +
+
+

System Commands

+
    +
  • $ status_check
  • +
  • $ sys_reboot
  • +
  • $ mem_info
  • +
  • $ net_stat
  • +
+
+
+ + +
+
+

Terminal Output

+
+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]
+        
+
+
+ + +
+
+

Status

+
+

CPU: Online

+

Memory: Stable

+

Network: Connected

+

Uptime: 23h 45m

+
+
+
-
- You are a user! + + +
+
+
+ $ + +
+
-

This is welcome pages and you're a user!

- Logout
+ + diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html index b4d4734..2b7402d 100644 --- a/src/main/resources/templates/layout.html +++ b/src/main/resources/templates/layout.html @@ -1,10 +1,53 @@ - - - + + + + + <link th:href="@{/css/management.css}" rel="stylesheet"> + <script src="https://cdn.tailwindcss.com"></script> </head> -<body> +<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col"> +<header th:fragment="header" class="bg-gray-800 border-b border-green-900"> + <div class="container mx-auto px-4 py-2 flex items-center justify-between"> + <div> + <h1 class="text-xl sm:text-2xl terminal-glow">Hlaeja Management</h1> + </div> + <div class="relative"> + <button id="menu-toggle" class="focus:outline-none"> + <svg class="w-6 h-6 text-green-400 hover:text-green-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/> + </svg> + </button> + <div id="dropdown-menu" class="hidden absolute right-0 mt-2 w-48 bg-gray-800 border border-green-900 shadow-lg z-10"> + <th:block th:if="${remoteUser.hasRole('admin')}"> + <a href="/account" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Account</a> + <hr class="dropdown-divider"> + </th:block> + <th:block th:if="${remoteUser.authenticated}"> + <a href="#" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">$ diagnostics</a> + <a href="#" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">$ logs</a> + <a href="#" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">$ shutdown</a> + </th:block> + <th:block th:if="${remoteUser.authenticated}"> + <hr class="dropdown-divider"> + <a href="/logout" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Logout</a> + </th:block> + <th:block th:unless="${remoteUser.authenticated}"> + <a href="/login" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Login</a> + </th:block> + </div> + </div> + </div> +</header> +<footer th:fragment="footer" class="bg-gray-800 border-t border-green-900 mt-4"> + <div class="container mx-auto p-4"> + <div class="flex flex-col sm:flex-row justify-between items-center text-sm"> + <div class="text-green-600 mb-2 sm:mb-0">Hlaeja © 2025 Lulz Ltd</div> + <div class="flex space-x-4"></div> + </div> + </div> +</footer> <div th:fragment="script"> <script th:src="@{/js/management.js}"></script> </div> diff --git a/src/main/resources/templates/messages.html b/src/main/resources/templates/messages.html new file mode 100644 index 0000000..ac32dd9 --- /dev/null +++ b/src/main/resources/templates/messages.html @@ -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>