add device type
add postEditType to TypeController update form.html and messages.html for saving types add updateType in DeviceRegistryService add WebClient deviceRegistryTypesUpdate in DeviceRegisterWebClientCalls.kt add getEditType to TypeController update type form.html for edit device type add Type Response toTypeForm in Mapping.kt add getType to DeviceRegistryService add WebClient deviceRegistryType in DeviceRegisterWebClientCalls.kt update edit link in device type list.html add getCreateType and postCreateType to TypeController add device type form.html add TypeFormadd TypeForm toTypeRequest in Mapping.kt add TypeForm add createType to DeviceRegistryService add deviceRegistryTypesCreate in DeviceRegisterWebClientCalls.kt add TypeNameDuplicateException add DeviceRegistryException extract validationErrors from postCreateAccount in AccountController to util Controller update AccountController getAccounts and TypeController getTypes with max show value add redis for spring boot session - make RemoteUserDetail Serializable - add application properties - add dependencies add TypeController add list.html for type extract fragment pagination.html from users.html add DeviceRegistryService with getTypes add DeviceRegisterWebClientCalls.kt with deviceRegistryTypes set up hlaeja device registry add type to layout menu add type to admin SecurityConfiguration
This commit is contained in:
@@ -9,6 +9,7 @@ In realms of connectedness, where devices roam free, A nexus of management, harm
|
|||||||
| spring.profiles.active | ✓ | Spring Boot environment |
|
| spring.profiles.active | ✓ | Spring Boot environment |
|
||||||
| jwt.public-key | ✓ | JWT public key file |
|
| jwt.public-key | ✓ | JWT public key file |
|
||||||
| account-registry.url | ✓ | Account Register URL |
|
| account-registry.url | ✓ | Account Register URL |
|
||||||
|
| device-registry.url | ✓ | Device Register URL |
|
||||||
|
|
||||||
*Required: ✓ can be stored as text, and ✗ need to be stored as secret.*
|
*Required: ✓ can be stored as text, and ✗ need to be stored as secret.*
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ dependencies {
|
|||||||
implementation(hlaeja.library.common.messages)
|
implementation(hlaeja.library.common.messages)
|
||||||
implementation(hlaeja.library.jwt)
|
implementation(hlaeja.library.jwt)
|
||||||
implementation(hlaeja.projectreactor.kotlin.reactor.extensions)
|
implementation(hlaeja.projectreactor.kotlin.reactor.extensions)
|
||||||
|
implementation(hlaeja.springboot.redis.session)
|
||||||
implementation(hlaeja.springboot.starter.actuator)
|
implementation(hlaeja.springboot.starter.actuator)
|
||||||
|
implementation(hlaeja.springboot.starter.redis)
|
||||||
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.validation)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
version=0.2.0-SNAPSHOT
|
version=0.2.0-SNAPSHOT
|
||||||
catalog=0.9.0
|
catalog=0.10.0-SNAPSHOT
|
||||||
container.port.host=9060
|
container.port.host=9060
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package ltd.hlaeja
|
package ltd.hlaeja
|
||||||
|
|
||||||
import ltd.hlaeja.property.AccountRegistryProperty
|
import ltd.hlaeja.property.AccountRegistryProperty
|
||||||
|
import ltd.hlaeja.property.DeviceRegistryProperty
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
import org.springframework.boot.runApplication
|
import org.springframework.boot.runApplication
|
||||||
|
|
||||||
@EnableConfigurationProperties(
|
@EnableConfigurationProperties(
|
||||||
AccountRegistryProperty::class,
|
AccountRegistryProperty::class,
|
||||||
|
DeviceRegistryProperty::class,
|
||||||
)
|
)
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
class Application
|
class Application
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class SecurityConfiguration {
|
|||||||
|
|
||||||
private fun AuthorizeExchangeSpec.adminPaths(): AuthorizeExchangeSpec.Access = pathMatchers(
|
private fun AuthorizeExchangeSpec.adminPaths(): AuthorizeExchangeSpec.Access = pathMatchers(
|
||||||
"/account/**",
|
"/account/**",
|
||||||
|
"/type/**",
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun AuthorizeExchangeSpec.publicPaths(): AuthorizeExchangeSpec.Access = pathMatchers(
|
private fun AuthorizeExchangeSpec.publicPaths(): AuthorizeExchangeSpec.Access = pathMatchers(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package ltd.hlaeja.controller
|
package ltd.hlaeja.controller
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Max
|
||||||
|
import jakarta.validation.constraints.Min
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import ltd.hlaeja.controller.validation.CreateGroup
|
import ltd.hlaeja.controller.validation.CreateGroup
|
||||||
import ltd.hlaeja.controller.validation.EditGroup
|
import ltd.hlaeja.controller.validation.EditGroup
|
||||||
@@ -11,6 +13,7 @@ import ltd.hlaeja.form.AccountForm
|
|||||||
import ltd.hlaeja.service.AccountRegistryService
|
import ltd.hlaeja.service.AccountRegistryService
|
||||||
import ltd.hlaeja.util.toAccountForm
|
import ltd.hlaeja.util.toAccountForm
|
||||||
import ltd.hlaeja.util.toAccountRequest
|
import ltd.hlaeja.util.toAccountRequest
|
||||||
|
import ltd.hlaeja.util.validationErrors
|
||||||
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.BindingResult
|
||||||
@@ -30,6 +33,8 @@ class AccountController(
|
|||||||
companion object {
|
companion object {
|
||||||
const val DEFAULT_PAGE: Int = 1
|
const val DEFAULT_PAGE: Int = 1
|
||||||
const val DEFAULT_SIZE: Int = 25
|
const val DEFAULT_SIZE: Int = 25
|
||||||
|
const val MIN: Long = 1
|
||||||
|
const val MAX: Long = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/edit-{account}")
|
@GetMapping("/edit-{account}")
|
||||||
@@ -50,22 +55,13 @@ class AccountController(
|
|||||||
@Validated(EditGroup::class) @ModelAttribute("accountForm") accountForm: AccountForm,
|
@Validated(EditGroup::class) @ModelAttribute("accountForm") accountForm: AccountForm,
|
||||||
bindingResult: BindingResult,
|
bindingResult: BindingResult,
|
||||||
model: Model,
|
model: Model,
|
||||||
): Mono<String> {
|
): Mono<String> = if (bindingResult.hasErrors()) {
|
||||||
val validationErrors = if (bindingResult.hasErrors()) {
|
model.addAttribute("accountForm", accountForm)
|
||||||
bindingResult.allErrors.map { error ->
|
model.addAttribute("validationErrors", validationErrors(bindingResult))
|
||||||
error.defaultMessage ?: "Unknown validation error"
|
model.addAttribute("roleGroups", accountRegistryService.getRoles())
|
||||||
}
|
Mono.just("account/edit")
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
Mono.just(accountForm)
|
||||||
}
|
|
||||||
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()) }
|
.flatMap { accountRegistryService.updateAccount(account, it.toAccountRequest()) }
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
model.addAttribute("successMessage", listOf("Saved changes!!!"))
|
model.addAttribute("successMessage", listOf("Saved changes!!!"))
|
||||||
@@ -82,6 +78,7 @@ class AccountController(
|
|||||||
"validationErrors",
|
"validationErrors",
|
||||||
"Username already exists. Please choose another.",
|
"Username already exists. Please choose another.",
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> Pair("validationErrors", "An unexpected error occurred. Please try again later.")
|
else -> Pair("validationErrors", "An unexpected error occurred. Please try again later.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,21 +104,13 @@ class AccountController(
|
|||||||
@Validated(CreateGroup::class) @ModelAttribute("accountForm") accountForm: AccountForm,
|
@Validated(CreateGroup::class) @ModelAttribute("accountForm") accountForm: AccountForm,
|
||||||
bindingResult: BindingResult,
|
bindingResult: BindingResult,
|
||||||
model: Model,
|
model: Model,
|
||||||
): Mono<String> {
|
): Mono<String> = if (bindingResult.hasErrors()) {
|
||||||
val validationErrors = if (bindingResult.hasErrors()) {
|
model.addAttribute("accountForm", accountForm)
|
||||||
bindingResult.allErrors.map { error ->
|
model.addAttribute("roleGroups", accountRegistryService.getRoles())
|
||||||
error.defaultMessage ?: "Unknown validation error"
|
model.addAttribute("validationErrors", validationErrors(bindingResult))
|
||||||
}
|
Mono.just("account/create")
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
Mono.just(accountForm)
|
||||||
}
|
|
||||||
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()) }
|
.flatMap { accountRegistryService.addAccount(it.toAccountRequest()) }
|
||||||
.map { "redirect:/account" }
|
.map { "redirect:/account" }
|
||||||
.onErrorResume { error ->
|
.onErrorResume { error ->
|
||||||
@@ -135,33 +124,20 @@ class AccountController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping(
|
||||||
fun getDefaultAccounts(
|
"",
|
||||||
|
"/page-{page}",
|
||||||
|
"/page-{page}/show-{show}",
|
||||||
|
)
|
||||||
|
fun getAccounts(
|
||||||
|
@PathVariable(required = false) @Min(MIN) page: Int = DEFAULT_PAGE,
|
||||||
|
@PathVariable(required = false) @Min(MIN) @Max(MAX) show: Int = DEFAULT_SIZE,
|
||||||
model: Model,
|
model: Model,
|
||||||
): Mono<String> = getAccounts(DEFAULT_PAGE, DEFAULT_SIZE, model)
|
): Mono<String> = accountRegistryService.getAccounts(page, show)
|
||||||
|
|
||||||
@GetMapping("/page-{page}")
|
|
||||||
fun getAccountsPage(
|
|
||||||
@PathVariable page: Int,
|
|
||||||
model: Model,
|
|
||||||
): Mono<String> = getAccounts(page, DEFAULT_SIZE, model)
|
|
||||||
|
|
||||||
@GetMapping("/page-{page}/show-{size}")
|
|
||||||
fun getAccountsPageSize(
|
|
||||||
@PathVariable page: Int,
|
|
||||||
@PathVariable size: Int,
|
|
||||||
model: Model,
|
|
||||||
): Mono<String> = getAccounts(page, size, model)
|
|
||||||
|
|
||||||
private fun getAccounts(
|
|
||||||
page: Int,
|
|
||||||
size: Int,
|
|
||||||
model: Model,
|
|
||||||
) = accountRegistryService.getAccounts(page, size)
|
|
||||||
.collectList()
|
.collectList()
|
||||||
.doOnNext { items ->
|
.doOnNext { items ->
|
||||||
model.addAttribute("items", items)
|
model.addAttribute("items", items)
|
||||||
model.addAttribute("pagination", Pagination(page, size, items.size, DEFAULT_SIZE))
|
model.addAttribute("pagination", Pagination(page, show, items.size, DEFAULT_SIZE))
|
||||||
}
|
}
|
||||||
.then(Mono.just("account/users"))
|
.then(Mono.just("account/users"))
|
||||||
}
|
}
|
||||||
|
|||||||
142
src/main/kotlin/ltd/hlaeja/controller/TypeController.kt
Normal file
142
src/main/kotlin/ltd/hlaeja/controller/TypeController.kt
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package ltd.hlaeja.controller
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Max
|
||||||
|
import jakarta.validation.constraints.Min
|
||||||
|
import java.util.UUID
|
||||||
|
import ltd.hlaeja.dto.Pagination
|
||||||
|
import ltd.hlaeja.exception.NoChangeException
|
||||||
|
import ltd.hlaeja.exception.NotFoundException
|
||||||
|
import ltd.hlaeja.exception.TypeNameDuplicateException
|
||||||
|
import ltd.hlaeja.form.TypeForm
|
||||||
|
import ltd.hlaeja.service.DeviceRegistryService
|
||||||
|
import ltd.hlaeja.util.toTypeForm
|
||||||
|
import ltd.hlaeja.util.toTypeRequest
|
||||||
|
import ltd.hlaeja.util.validationErrors
|
||||||
|
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
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
class TypeController(
|
||||||
|
private val deviceRegistryService: DeviceRegistryService,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_PAGE: Int = 1
|
||||||
|
const val DEFAULT_SIZE: Int = 25
|
||||||
|
const val MIN: Long = 1
|
||||||
|
const val MAX: Long = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(
|
||||||
|
"/type",
|
||||||
|
"/type/page-{page}",
|
||||||
|
"/type/page-{page}/show-{show}",
|
||||||
|
)
|
||||||
|
fun getTypes(
|
||||||
|
@PathVariable(required = false) @Min(MIN) page: Int = DEFAULT_PAGE,
|
||||||
|
@PathVariable(required = false) @Min(MIN) @Max(MAX) show: Int = DEFAULT_SIZE,
|
||||||
|
model: Model,
|
||||||
|
) = deviceRegistryService.getTypes(page, show)
|
||||||
|
.collectList()
|
||||||
|
.doOnNext { items ->
|
||||||
|
model.addAttribute("items", items)
|
||||||
|
model.addAttribute("pagination", Pagination(page, show, items.size, DEFAULT_SIZE))
|
||||||
|
}
|
||||||
|
.then(Mono.just("device/type/list"))
|
||||||
|
|
||||||
|
@GetMapping("/type/create")
|
||||||
|
fun getCreateType(
|
||||||
|
model: Model,
|
||||||
|
): Mono<String> = Mono.just("device/type/form")
|
||||||
|
.doOnNext {
|
||||||
|
model.addAttribute("typeForm", TypeForm())
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/type/create")
|
||||||
|
fun postCreateType(
|
||||||
|
@Validated @ModelAttribute("typeForm") typeForm: TypeForm,
|
||||||
|
bindingResult: BindingResult,
|
||||||
|
model: Model,
|
||||||
|
): Mono<String> = if (bindingResult.hasErrors()) {
|
||||||
|
model.addAttribute("typeForm", typeForm)
|
||||||
|
model.addAttribute("validationErrors", validationErrors(bindingResult))
|
||||||
|
Mono.just("device/type/form")
|
||||||
|
} else {
|
||||||
|
Mono.just(typeForm)
|
||||||
|
.flatMap { deviceRegistryService.createType(it.toTypeRequest()) }
|
||||||
|
.map { "redirect:/type" }
|
||||||
|
.onErrorResume { error ->
|
||||||
|
val errorMessage = when (error) {
|
||||||
|
is TypeNameDuplicateException -> "Type name already exists. Please choose another."
|
||||||
|
else -> "An unexpected error occurred. Please try again later."
|
||||||
|
}
|
||||||
|
model.addAttribute("validationErrors", listOf(errorMessage))
|
||||||
|
Mono.just("device/type/form")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/type-{type}")
|
||||||
|
fun getEditType(
|
||||||
|
@PathVariable type: UUID,
|
||||||
|
model: Model,
|
||||||
|
): Mono<String> = deviceRegistryService.getType(type)
|
||||||
|
.doOnNext {
|
||||||
|
model.addAttribute("type", it)
|
||||||
|
model.addAttribute("typeForm", it.toTypeForm())
|
||||||
|
}
|
||||||
|
.then(Mono.just("device/type/form"))
|
||||||
|
|
||||||
|
@PostMapping("/type-{type}")
|
||||||
|
fun postEditType(
|
||||||
|
@PathVariable type: UUID,
|
||||||
|
@Validated @ModelAttribute("typeForm") typeForm: TypeForm,
|
||||||
|
bindingResult: BindingResult,
|
||||||
|
model: Model,
|
||||||
|
): Mono<String> = if (bindingResult.hasErrors()) {
|
||||||
|
deviceRegistryService.getType(type)
|
||||||
|
.doOnNext {
|
||||||
|
model.addAttribute("type", it)
|
||||||
|
model.addAttribute("typeForm", typeForm)
|
||||||
|
model.addAttribute("validationErrors", validationErrors(bindingResult))
|
||||||
|
}
|
||||||
|
.then(
|
||||||
|
Mono.just("device/type/form"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Mono.just(typeForm)
|
||||||
|
.flatMap { deviceRegistryService.updateType(type, it.toTypeRequest()) }
|
||||||
|
.doOnNext {
|
||||||
|
model.addAttribute("successMessage", listOf("Saved changes!!!"))
|
||||||
|
model.addAttribute("type", it)
|
||||||
|
model.addAttribute("typeForm", it.toTypeForm())
|
||||||
|
}
|
||||||
|
.then(Mono.just("device/type/form"))
|
||||||
|
.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 TypeNameDuplicateException -> Pair(
|
||||||
|
"validationErrors",
|
||||||
|
"Type name already exists. Please choose another.",
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> Pair("validationErrors", "An unexpected error occurred. Please try again later.")
|
||||||
|
}
|
||||||
|
deviceRegistryService.getType(type)
|
||||||
|
.doOnNext {
|
||||||
|
model.addAttribute(errorMessage.first, listOf(errorMessage.second))
|
||||||
|
model.addAttribute("type", it)
|
||||||
|
model.addAttribute("typeForm", typeForm)
|
||||||
|
}
|
||||||
|
.then(
|
||||||
|
Mono.just("device/type/form"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package ltd.hlaeja.exception
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
open class DeviceRegistryException : HlaejaException {
|
||||||
|
|
||||||
|
constructor() : super()
|
||||||
|
|
||||||
|
constructor(message: String) : super(message)
|
||||||
|
|
||||||
|
constructor(cause: Throwable) : super(cause)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: String,
|
||||||
|
cause: Throwable,
|
||||||
|
) : super(message, cause)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: String,
|
||||||
|
cause: Throwable,
|
||||||
|
enableSuppression: Boolean,
|
||||||
|
writableStackTrace: Boolean,
|
||||||
|
) : super(message, cause, enableSuppression, writableStackTrace)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package ltd.hlaeja.exception
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
open class TypeNameDuplicateException : DeviceRegistryException {
|
||||||
|
|
||||||
|
constructor() : super()
|
||||||
|
|
||||||
|
constructor(message: String) : super(message)
|
||||||
|
|
||||||
|
constructor(cause: Throwable) : super(cause)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: String,
|
||||||
|
cause: Throwable,
|
||||||
|
) : super(message, cause)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: String,
|
||||||
|
cause: Throwable,
|
||||||
|
enableSuppression: Boolean,
|
||||||
|
writableStackTrace: Boolean,
|
||||||
|
) : super(message, cause, enableSuppression, writableStackTrace)
|
||||||
|
}
|
||||||
10
src/main/kotlin/ltd/hlaeja/form/TypeForm.kt
Normal file
10
src/main/kotlin/ltd/hlaeja/form/TypeForm.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package ltd.hlaeja.form
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Size
|
||||||
|
|
||||||
|
data class TypeForm(
|
||||||
|
@field:Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
|
||||||
|
val name: String = "",
|
||||||
|
@field:Size(min = 2, max = 1000, message = "Description must be between 2 and 1000 characters")
|
||||||
|
val description: String = "",
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package ltd.hlaeja.property
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
|
||||||
|
@ConfigurationProperties(prefix = "device-registry")
|
||||||
|
data class DeviceRegistryProperty(
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
package ltd.hlaeja.security
|
package ltd.hlaeja.security
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
data class RemoteUserDetail(
|
data class RemoteUserDetail(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val username: String,
|
val username: String,
|
||||||
)
|
) : Serializable {
|
||||||
|
companion object {
|
||||||
|
@Suppress("ConstPropertyName")
|
||||||
|
private const val serialVersionUID = 1L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
63
src/main/kotlin/ltd/hlaeja/service/DeviceRegistryService.kt
Normal file
63
src/main/kotlin/ltd/hlaeja/service/DeviceRegistryService.kt
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package ltd.hlaeja.service
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
import ltd.hlaeja.exception.DeviceRegistryException
|
||||||
|
import ltd.hlaeja.exception.HlaejaException
|
||||||
|
import ltd.hlaeja.library.deviceRegistry.Type
|
||||||
|
import ltd.hlaeja.library.deviceRegistry.Types
|
||||||
|
import ltd.hlaeja.property.DeviceRegistryProperty
|
||||||
|
import ltd.hlaeja.util.deviceRegistryType
|
||||||
|
import ltd.hlaeja.util.deviceRegistryTypes
|
||||||
|
import ltd.hlaeja.util.deviceRegistryTypesCreate
|
||||||
|
import ltd.hlaeja.util.deviceRegistryTypesUpdate
|
||||||
|
import org.springframework.http.HttpStatus.BAD_REQUEST
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient
|
||||||
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class DeviceRegistryService(
|
||||||
|
private val webClient: WebClient,
|
||||||
|
private val property: DeviceRegistryProperty,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun getTypes(
|
||||||
|
page: Int,
|
||||||
|
show: Int,
|
||||||
|
): Flux<Types.Response> = webClient.deviceRegistryTypes(page, show, property)
|
||||||
|
|
||||||
|
fun createType(
|
||||||
|
request: Type.Request,
|
||||||
|
): Mono<Type.Response> = webClient.deviceRegistryTypesCreate(request, property)
|
||||||
|
.onErrorResume { error ->
|
||||||
|
when (error) {
|
||||||
|
is DeviceRegistryException -> Mono.error(error)
|
||||||
|
else -> Mono.error(ResponseStatusException(BAD_REQUEST, error.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getType(
|
||||||
|
type: UUID,
|
||||||
|
): Mono<Type.Response> = webClient.deviceRegistryType(type, property)
|
||||||
|
.onErrorResume { error ->
|
||||||
|
when (error) {
|
||||||
|
is ResponseStatusException -> Mono.error(error)
|
||||||
|
else -> Mono.error(ResponseStatusException(BAD_REQUEST, error.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateType(
|
||||||
|
type: UUID,
|
||||||
|
request: Type.Request,
|
||||||
|
): Mono<Type.Response> = webClient.deviceRegistryTypesUpdate(type, request, property)
|
||||||
|
.onErrorResume(::errorHandler)
|
||||||
|
|
||||||
|
private fun errorHandler(
|
||||||
|
error: Throwable,
|
||||||
|
): Mono<out Type.Response> = when (error) {
|
||||||
|
is HlaejaException -> Mono.error(error)
|
||||||
|
else -> Mono.error(ResponseStatusException(BAD_REQUEST, error.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/main/kotlin/ltd/hlaeja/util/Controller.kt
Normal file
11
src/main/kotlin/ltd/hlaeja/util/Controller.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package ltd.hlaeja.util
|
||||||
|
|
||||||
|
import org.springframework.validation.BindingResult
|
||||||
|
|
||||||
|
fun validationErrors(
|
||||||
|
bindingResult: BindingResult,
|
||||||
|
): List<String> = if (bindingResult.hasErrors()) {
|
||||||
|
bindingResult.allErrors.map { error -> error.defaultMessage ?: "Unknown validation error" }
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package ltd.hlaeja.util
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
import ltd.hlaeja.exception.DeviceRegistryException
|
||||||
|
import ltd.hlaeja.exception.NoChangeException
|
||||||
|
import ltd.hlaeja.exception.NotFoundException
|
||||||
|
import ltd.hlaeja.exception.TypeNameDuplicateException
|
||||||
|
import ltd.hlaeja.library.deviceRegistry.Type
|
||||||
|
import ltd.hlaeja.library.deviceRegistry.Types
|
||||||
|
import ltd.hlaeja.property.DeviceRegistryProperty
|
||||||
|
import org.springframework.http.HttpStatus.ACCEPTED
|
||||||
|
import org.springframework.http.HttpStatus.BAD_REQUEST
|
||||||
|
import org.springframework.http.HttpStatus.CONFLICT
|
||||||
|
import org.springframework.http.HttpStatus.NOT_FOUND
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient
|
||||||
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
fun WebClient.deviceRegistryTypes(
|
||||||
|
page: Int,
|
||||||
|
size: Int,
|
||||||
|
property: DeviceRegistryProperty,
|
||||||
|
): Flux<Types.Response> = get()
|
||||||
|
.uri("${property.url}/types/page-$page/show-$size".also(::logCall))
|
||||||
|
.retrieve()
|
||||||
|
.bodyToFlux(Types.Response::class.java)
|
||||||
|
|
||||||
|
fun WebClient.deviceRegistryTypesCreate(
|
||||||
|
request: Type.Request,
|
||||||
|
property: DeviceRegistryProperty,
|
||||||
|
): Mono<Type.Response> = post()
|
||||||
|
.uri("${property.url}/type".also(::logCall))
|
||||||
|
.bodyValue(request)
|
||||||
|
.retrieve()
|
||||||
|
.onStatus(CONFLICT::equals) { throw TypeNameDuplicateException("Remote service returned 409") }
|
||||||
|
.onStatus(BAD_REQUEST::equals) { throw DeviceRegistryException("Remote service returned 400") }
|
||||||
|
.bodyToMono(Type.Response::class.java)
|
||||||
|
|
||||||
|
fun WebClient.deviceRegistryType(
|
||||||
|
type: UUID,
|
||||||
|
property: DeviceRegistryProperty,
|
||||||
|
): Mono<Type.Response> = get()
|
||||||
|
.uri("${property.url}/type-$type".also(::logCall))
|
||||||
|
.retrieve()
|
||||||
|
.onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NOT_FOUND) }
|
||||||
|
.bodyToMono(Type.Response::class.java)
|
||||||
|
|
||||||
|
fun WebClient.deviceRegistryTypesUpdate(
|
||||||
|
type: UUID,
|
||||||
|
request: Type.Request,
|
||||||
|
property: DeviceRegistryProperty,
|
||||||
|
): Mono<Type.Response> = put()
|
||||||
|
.uri("${property.url}/type-$type".also(::logCall))
|
||||||
|
.bodyValue(request)
|
||||||
|
.retrieve()
|
||||||
|
.onStatus(ACCEPTED::equals) { throw NoChangeException("Remote service returned 202") }
|
||||||
|
.onStatus(BAD_REQUEST::equals) { throw DeviceRegistryException("Remote service returned 400") }
|
||||||
|
.onStatus(NOT_FOUND::equals) { throw NotFoundException("Remote service returned 404") }
|
||||||
|
.onStatus(CONFLICT::equals) { throw TypeNameDuplicateException("Remote service returned 409") }
|
||||||
|
.bodyToMono(Type.Response::class.java)
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package ltd.hlaeja.util
|
package ltd.hlaeja.util
|
||||||
|
|
||||||
import ltd.hlaeja.form.AccountForm
|
import ltd.hlaeja.form.AccountForm
|
||||||
|
import ltd.hlaeja.form.TypeForm
|
||||||
import ltd.hlaeja.library.accountRegistry.Account
|
import ltd.hlaeja.library.accountRegistry.Account
|
||||||
import ltd.hlaeja.library.accountRegistry.Authentication
|
import ltd.hlaeja.library.accountRegistry.Authentication
|
||||||
|
import ltd.hlaeja.library.deviceRegistry.Type
|
||||||
import org.springframework.security.core.Authentication as SpringAuthentication
|
import org.springframework.security.core.Authentication as SpringAuthentication
|
||||||
|
|
||||||
fun SpringAuthentication.toAuthenticationRequest(): Authentication.Request = Authentication.Request(
|
fun SpringAuthentication.toAuthenticationRequest(): Authentication.Request = Authentication.Request(
|
||||||
@@ -26,3 +28,13 @@ fun Account.Response.toAccountForm(): AccountForm = AccountForm(
|
|||||||
.replaceFirstChar { char -> char.uppercase() }
|
.replaceFirstChar { char -> char.uppercase() }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun TypeForm.toTypeRequest(): Type.Request = Type.Request(
|
||||||
|
name = name,
|
||||||
|
description = description,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Type.Response.toTypeForm(): Type.Request = Type.Request(
|
||||||
|
name = name,
|
||||||
|
description = description,
|
||||||
|
)
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ spring:
|
|||||||
os:
|
os:
|
||||||
name: "%APP_BUILD_OS_NAME%"
|
name: "%APP_BUILD_OS_NAME%"
|
||||||
version: "%APP_BUILD_OS_VERSION%"
|
version: "%APP_BUILD_OS_VERSION%"
|
||||||
|
session:
|
||||||
|
timeout: 60m
|
||||||
|
|
||||||
|
redis:
|
||||||
|
namespace: "spring:session:management"
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
port: 6379
|
||||||
|
|
||||||
management:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
@@ -42,10 +50,17 @@ spring:
|
|||||||
web:
|
web:
|
||||||
resources:
|
resources:
|
||||||
static-locations: file:src/main/resources/static/
|
static-locations: file:src/main/resources/static/
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
database: 2
|
||||||
|
|
||||||
account-registry:
|
account-registry:
|
||||||
url: http://localhost:9050
|
url: http://localhost:9050
|
||||||
|
|
||||||
|
device-registry:
|
||||||
|
url: http://localhost:9010
|
||||||
|
|
||||||
---
|
---
|
||||||
##########################
|
##########################
|
||||||
### Docker environment ###
|
### Docker environment ###
|
||||||
@@ -54,10 +69,17 @@ spring:
|
|||||||
config:
|
config:
|
||||||
activate:
|
activate:
|
||||||
on-profile: docker
|
on-profile: docker
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: Redis
|
||||||
|
database: 2
|
||||||
|
|
||||||
account-registry:
|
account-registry:
|
||||||
url: http://AccountRegistry:8080
|
url: http://AccountRegistry:8080
|
||||||
|
|
||||||
|
device-registry:
|
||||||
|
url: http://DeviceRegistry:8080
|
||||||
|
|
||||||
---
|
---
|
||||||
##############################
|
##############################
|
||||||
### Production environment ###
|
### Production environment ###
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<span th:text="${pagination.size}"/>
|
<span th:text="${pagination.size}"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-[-2px]">
|
<div class="mt-[-2px]">
|
||||||
<a th:href="@{/account/create}" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded border border-green-900 transition-colors text-sm">$ Create New Account</a>
|
<a th:href="@{/account/create}" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded border border-green-900 transition-colors text-sm">Create New Account</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
@@ -39,29 +39,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 text-sm" th:classappend="${pagination.showSize} ? 'space-y-2' : ''">
|
<!--/*/<th:block th:replace="~{pagination :: pagination('/account', ${pagination})}"/>/*/-->
|
||||||
<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>
|
||||||
</main>
|
</main>
|
||||||
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
|
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
|
||||||
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
|
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
|
||||||
<script>
|
<script>
|
||||||
// Assuming makeLocalTime is defined elsewhere or needs to be added
|
document.addEventListener('DOMContentLoaded', () => makeLocalTime(document.querySelectorAll('.utcTimestamp')));
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
makeLocalTime(document.querySelectorAll('.utcTimestamp'));
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
44
src/main/resources/templates/device/type/form.html
Normal file
44
src/main/resources/templates/device/type/form.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>
|
||||||
|
<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
|
||||||
|
<th:block th:replace="~{layout.html :: header}"/>
|
||||||
|
<main class="container mx-auto p-4 flex-grow flex items-center justify-center">
|
||||||
|
<div class="w-full max-w-3xl">
|
||||||
|
<div class="flex justify-between items-end mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg sm:text-xl terminal-glow" th:if="${type} == null" th:text="'Create Device Type'"></h2>
|
||||||
|
<h2 class="text-lg sm:text-xl terminal-glow" th:unless="${type} == null" th:text="'Edit Device Type'"></h2>
|
||||||
|
</div>
|
||||||
|
<div th:if="${type} != null" class="text-xs text-green-600">
|
||||||
|
Created: <span class="utcTimestamp" th:data-timestamp="${type.timestamp}"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="border-green-900 mb-4">
|
||||||
|
<th:block th:replace="~{messages :: messageDisplay(messageList=${validationErrors}, error=true, styleClass='text-red-600')}"/>
|
||||||
|
<th:block th:replace="~{messages :: messageDisplay(messageList=${successMessage}, error=false, styleClass='text-green-600')}"/>
|
||||||
|
<form th:action="${type} == null ? @{/type/create} : @{/type-{id}(id=${type.id})}" th:method="post">
|
||||||
|
<div class="bg-gray-800 p-6 rounded-lg border border-green-900">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="username" class="block text-sm mb-2">Name</label>
|
||||||
|
<input th:field="*{typeForm.name}" id="username" type="text" placeholder="Enter name..." class="w-full bg-gray-900 border border-green-900 rounded px-3 py-2 text-green-400 focus:outline-none focus:border-green-600">
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm mb-2" for="textarea">Description</label>
|
||||||
|
<textarea id="textarea" th:field="*{typeForm.description}" rows="12" 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 description..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end space-x-4">
|
||||||
|
<a href="/type" class="bg-gray-800 hover:bg-gray-700 text-green-400 px-4 py-2 rounded border border-green-900 transition-colors inline-block" th:text="${type} == null ? 'Cancel' : 'Go Back'"/>
|
||||||
|
<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" th:text="${type} == null ? 'Create' : 'Save'"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<th:block th:replace="~{layout.html :: footer}"/>
|
||||||
|
<th:block th:replace="~{layout.html :: script}"/>
|
||||||
|
<script th:if="${type} != null">
|
||||||
|
document.addEventListener('DOMContentLoaded', () => makeLocalTime(document.querySelectorAll('.utcTimestamp')));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
55
src/main/resources/templates/device/type/list.html
Normal file
55
src/main/resources/templates/device/type/list.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<!--/*/<th:block th:replace="~{layout.html :: documentHead ('Hlaeja Management')}"/>/*/-->
|
||||||
|
<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
|
||||||
|
<!--/*/<th:block th:replace="~{layout.html :: header}"/>/*/-->
|
||||||
|
<main class="container mx-auto p-4 flex-grow">
|
||||||
|
<div class="bg-gray-800 p-6 rounded-lg border border-green-900">
|
||||||
|
<h1 class="text-lg sm:text-xl mb-4 terminal-glow">Device Types</h1>
|
||||||
|
<hr class="border-green-900 mb-4">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<div th:if="${pagination.start > pagination.size}" class="text-sm">
|
||||||
|
Show page <span th:text="${pagination.page}"/> items 0 - 0
|
||||||
|
</div>
|
||||||
|
<div th:unless="${pagination.start > pagination.size}" class="text-sm">
|
||||||
|
Show page <span th:text="${pagination.page}"/>
|
||||||
|
items <span th:text="${pagination.start}"/> -
|
||||||
|
<span th:text="${pagination.size}"/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-[-2px]">
|
||||||
|
<a th:href="@{/type/create}" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded border border-green-900 transition-colors text-sm">Create New Type</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-green-900">
|
||||||
|
<th class="py-2 px-4 text-left">Name</th>
|
||||||
|
<th class="py-2 px-4 text-left">Time</th>
|
||||||
|
<th class="py-2 px-4 text-left">ID</th>
|
||||||
|
<th class="py-2 px-4 text-left">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<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 th:text="${item.name}" class="py-2 px-4"></td>
|
||||||
|
<td th:data-timestamp="${item.timestamp}" class="py-2 px-4 utcTimestamp"></td>
|
||||||
|
<td th:text="${item.id}" class="py-2 px-4"></td>
|
||||||
|
<td class="py-2 px-4"><a th:href="@{/type-{id}(id=${item.id})}">Edit</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--/*/<th:block th:replace="~{pagination :: pagination('/type', ${pagination})}"/>/*/-->
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!--/*/<th:block th:replace="~{layout.html :: footer}"/>/*/-->
|
||||||
|
<!--/*/<th:block th:replace="~{layout.html :: script}"/>/*/-->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => makeLocalTime(document.querySelectorAll('.utcTimestamp')));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
<div id="dropdown-menu" class="hidden absolute right-0 mt-2 w-48 bg-gray-800 border border-green-900 shadow-lg z-10">
|
<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')}">
|
<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>
|
<a href="/account" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Account</a>
|
||||||
|
<a href="/type" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Device Type</a>
|
||||||
<hr class="dropdown-divider">
|
<hr class="dropdown-divider">
|
||||||
</th:block>
|
</th:block>
|
||||||
<th:block th:if="${remoteUser.authenticated}">
|
<th:block th:if="${remoteUser.authenticated}">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<!-- Single Message Case -->
|
<!-- Single Message Case -->
|
||||||
<th:block th:case="1">
|
<th:block th:case="1">
|
||||||
<span th:if="${error}">Validation Error:</span>
|
<span th:if="${error}">Validation Error:</span>
|
||||||
<span th:unless="${error}">Validation:</span>
|
<span th:unless="${error}">Information:</span>
|
||||||
<span th:text="${messageList[0]}"></span>
|
<span th:text="${messageList[0]}"></span>
|
||||||
</th:block>
|
</th:block>
|
||||||
<!-- Multiple Messages Case -->
|
<!-- Multiple Messages Case -->
|
||||||
|
|||||||
19
src/main/resources/templates/pagination.html
Normal file
19
src/main/resources/templates/pagination.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<th:block th:fragment="pagination(baseUrl, pagination)">
|
||||||
|
<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="@{${baseUrl} + '/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="@{${baseUrl} + '/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="@{${baseUrl} + '/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="@{${baseUrl} + '/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>
|
||||||
|
</th:block>
|
||||||
|
</html>
|
||||||
@@ -2,3 +2,5 @@ jwt:
|
|||||||
public-key: cert/valid-public-key.pem
|
public-key: cert/valid-public-key.pem
|
||||||
account-registry:
|
account-registry:
|
||||||
url: http://localhost
|
url: http://localhost
|
||||||
|
device-registry:
|
||||||
|
url: http://localhost
|
||||||
|
|||||||
Reference in New Issue
Block a user