mirror of
https://github.com/pcvolkmer/etl-processor.git
synced 2025-04-19 17:26:51 +00:00
feat #29: add initial support for mtbfile api tokens
This commit is contained in:
parent
531a8589db
commit
30cf0fd22e
@ -86,6 +86,7 @@ data class KafkaTargetProperties(
|
|||||||
data class SecurityConfigProperties(
|
data class SecurityConfigProperties(
|
||||||
val adminUser: String?,
|
val adminUser: String?,
|
||||||
val adminPassword: String?,
|
val adminPassword: String?,
|
||||||
|
val enableTokens: Boolean = false
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val NAME = "app.security"
|
const val NAME = "app.security"
|
||||||
|
@ -25,6 +25,8 @@ import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
|
|||||||
import dev.dnpm.etl.processor.pseudonym.Generator
|
import dev.dnpm.etl.processor.pseudonym.Generator
|
||||||
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
||||||
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
||||||
|
import dev.dnpm.etl.processor.services.TokenRepository
|
||||||
|
import dev.dnpm.etl.processor.services.TokenService
|
||||||
import dev.dnpm.etl.processor.services.Transformation
|
import dev.dnpm.etl.processor.services.Transformation
|
||||||
import dev.dnpm.etl.processor.services.TransformationService
|
import dev.dnpm.etl.processor.services.TransformationService
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@ -37,6 +39,9 @@ import org.springframework.retry.policy.SimpleRetryPolicy
|
|||||||
import org.springframework.retry.support.RetryTemplate
|
import org.springframework.retry.support.RetryTemplate
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
import org.springframework.retry.support.RetryTemplateBuilder
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling
|
import org.springframework.scheduling.annotation.EnableScheduling
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||||
|
import org.springframework.security.provisioning.UserDetailsManager
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.toJavaDuration
|
import kotlin.time.toJavaDuration
|
||||||
@ -114,6 +119,12 @@ class AppConfiguration {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ConditionalOnProperty(value = ["app.security.enable-tokens"], havingValue = "true")
|
||||||
|
@Bean
|
||||||
|
fun tokenService(userDetailsManager: InMemoryUserDetailsManager, passwordEncoder: PasswordEncoder, tokenRepository: TokenRepository): TokenService {
|
||||||
|
return TokenService(userDetailsManager, passwordEncoder, tokenRepository)
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun statisticsUpdateProducer(): Sinks.Many<Any> {
|
fun statisticsUpdateProducer(): Sinks.Many<Any> {
|
||||||
return Sinks.many().multicast().directBestEffort()
|
return Sinks.many().multicast().directBestEffort()
|
||||||
|
@ -24,15 +24,21 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
|||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.core.Ordered
|
||||||
|
import org.springframework.core.annotation.Order
|
||||||
|
import org.springframework.http.HttpMethod
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
import org.springframework.security.config.annotation.web.invoke
|
import org.springframework.security.config.annotation.web.invoke
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy
|
||||||
import org.springframework.security.core.userdetails.User
|
import org.springframework.security.core.userdetails.User
|
||||||
import org.springframework.security.core.userdetails.UserDetails
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories
|
import org.springframework.security.crypto.factory.PasswordEncoderFactories
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
@ -76,12 +82,16 @@ class AppSecurityConfiguration(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain {
|
||||||
http {
|
http {
|
||||||
authorizeRequests {
|
authorizeRequests {
|
||||||
authorize("/configs/**", hasRole("ADMIN"))
|
authorize("/configs/**", hasRole("ADMIN"))
|
||||||
|
authorize("/mtbfile/**", hasAnyRole("MTBFILE"))
|
||||||
authorize(anyRequest, permitAll)
|
authorize(anyRequest, permitAll)
|
||||||
}
|
}
|
||||||
|
httpBasic {
|
||||||
|
realmName = "ETL-Processor"
|
||||||
|
}
|
||||||
formLogin {
|
formLogin {
|
||||||
loginPage = "/login"
|
loginPage = "/login"
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of ETL-Processor
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct
|
||||||
|
import org.springframework.data.annotation.Id
|
||||||
|
import org.springframework.data.relational.core.mapping.Table
|
||||||
|
import org.springframework.data.repository.CrudRepository
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.security.core.userdetails.User
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class TokenService(
|
||||||
|
private val userDetailsManager: InMemoryUserDetailsManager,
|
||||||
|
private val passwordEncoder: PasswordEncoder,
|
||||||
|
private val tokenRepository: TokenRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
fun setup() {
|
||||||
|
tokenRepository.findAll().forEach {
|
||||||
|
userDetailsManager.createUser(
|
||||||
|
User.withUsername(it.username)
|
||||||
|
.password(it.password)
|
||||||
|
.roles("MTBFILE")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addToken(name: String): Result<String> {
|
||||||
|
val username = name.lowercase().replace("""[^a-z0-9]""".toRegex(), "")
|
||||||
|
if (userDetailsManager.userExists(username)) {
|
||||||
|
return Result.failure(RuntimeException("Cannot use token name"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val password = Base64.getEncoder().encodeToString(UUID.randomUUID().toString().encodeToByteArray())
|
||||||
|
val encodedPassword = passwordEncoder.encode(password).toString()
|
||||||
|
|
||||||
|
userDetailsManager.createUser(
|
||||||
|
User.withUsername(username)
|
||||||
|
.password(encodedPassword)
|
||||||
|
.roles("MTBFILE")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
tokenRepository.save(Token(name = name, username = username, password = encodedPassword))
|
||||||
|
|
||||||
|
return Result.success("$username:$password")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteToken(id: Long) {
|
||||||
|
val token = tokenRepository.findByIdOrNull(id) ?: return
|
||||||
|
userDetailsManager.deleteUser(token.username)
|
||||||
|
tokenRepository.delete(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findAll(): List<Token> {
|
||||||
|
return tokenRepository.findAll().toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table("token")
|
||||||
|
data class Token(
|
||||||
|
@Id val id: Long? = null,
|
||||||
|
val name: String,
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
val createdAt: Instant = Instant.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
interface TokenRepository : CrudRepository<Token, Long>
|
@ -22,14 +22,15 @@ package dev.dnpm.etl.processor.web
|
|||||||
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.Generator
|
import dev.dnpm.etl.processor.pseudonym.Generator
|
||||||
|
import dev.dnpm.etl.processor.services.Token
|
||||||
|
import dev.dnpm.etl.processor.services.TokenService
|
||||||
import dev.dnpm.etl.processor.services.TransformationService
|
import dev.dnpm.etl.processor.services.TransformationService
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.codec.ServerSentEvent
|
import org.springframework.http.codec.ServerSentEvent
|
||||||
import org.springframework.stereotype.Controller
|
import org.springframework.stereotype.Controller
|
||||||
import org.springframework.ui.Model
|
import org.springframework.ui.Model
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.*
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
|
|
||||||
@ -41,8 +42,8 @@ class ConfigController(
|
|||||||
private val transformationService: TransformationService,
|
private val transformationService: TransformationService,
|
||||||
private val pseudonymGenerator: Generator,
|
private val pseudonymGenerator: Generator,
|
||||||
private val mtbFileSender: MtbFileSender,
|
private val mtbFileSender: MtbFileSender,
|
||||||
private val connectionCheckService: ConnectionCheckService
|
private val connectionCheckService: ConnectionCheckService,
|
||||||
|
private val tokenService: TokenService?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@ -51,6 +52,12 @@ class ConfigController(
|
|||||||
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
|
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
|
||||||
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
|
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
|
||||||
model.addAttribute("connectionAvailable", connectionCheckService.connectionAvailable())
|
model.addAttribute("connectionAvailable", connectionCheckService.connectionAvailable())
|
||||||
|
model.addAttribute("tokensEnabled", tokenService != null)
|
||||||
|
if (tokenService != null) {
|
||||||
|
model.addAttribute("tokens", tokenService.findAll())
|
||||||
|
} else {
|
||||||
|
model.addAttribute("tokens", listOf<Token>())
|
||||||
|
}
|
||||||
model.addAttribute("transformations", transformationService.getTransformations())
|
model.addAttribute("transformations", transformationService.getTransformations())
|
||||||
|
|
||||||
return "configs"
|
return "configs"
|
||||||
@ -61,10 +68,50 @@ class ConfigController(
|
|||||||
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
|
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
|
||||||
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
|
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
|
||||||
model.addAttribute("connectionAvailable", connectionCheckService.connectionAvailable())
|
model.addAttribute("connectionAvailable", connectionCheckService.connectionAvailable())
|
||||||
|
if (tokenService != null) {
|
||||||
|
model.addAttribute("tokensEnabled", true)
|
||||||
|
model.addAttribute("tokens", tokenService.findAll())
|
||||||
|
} else {
|
||||||
|
model.addAttribute("tokens", listOf<Token>())
|
||||||
|
}
|
||||||
|
|
||||||
return "configs/connectionAvailable"
|
return "configs/connectionAvailable"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = ["tokens"])
|
||||||
|
fun addToken(@ModelAttribute("name") name: String, model: Model): String {
|
||||||
|
if (tokenService == null) {
|
||||||
|
model.addAttribute("tokensEnabled", false)
|
||||||
|
model.addAttribute("success", false)
|
||||||
|
} else {
|
||||||
|
model.addAttribute("tokensEnabled", true)
|
||||||
|
val result = tokenService.addToken(name)
|
||||||
|
if (result.isSuccess) {
|
||||||
|
model.addAttribute("newTokenValue", result.getOrDefault(""))
|
||||||
|
model.addAttribute("success", true)
|
||||||
|
} else {
|
||||||
|
model.addAttribute("success", false)
|
||||||
|
}
|
||||||
|
model.addAttribute("tokens", tokenService.findAll())
|
||||||
|
}
|
||||||
|
|
||||||
|
return "configs/tokens"
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping(path = ["tokens/{id}"])
|
||||||
|
fun deleteToken(@PathVariable id: Long, model: Model): String {
|
||||||
|
if (tokenService != null) {
|
||||||
|
tokenService.deleteToken(id)
|
||||||
|
|
||||||
|
model.addAttribute("tokensEnabled", true)
|
||||||
|
model.addAttribute("tokens", tokenService.findAll())
|
||||||
|
} else {
|
||||||
|
model.addAttribute("tokensEnabled", false)
|
||||||
|
model.addAttribute("tokens", listOf<Token>())
|
||||||
|
}
|
||||||
|
return "configs/tokens"
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
|
@GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
|
||||||
fun events(): Flux<ServerSentEvent<Any>> {
|
fun events(): Flux<ServerSentEvent<Any>> {
|
||||||
return configsUpdateProducer.asFlux().map {
|
return configsUpdateProducer.asFlux().map {
|
||||||
|
@ -27,13 +27,19 @@ import org.springframework.http.ResponseEntity
|
|||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping(path = ["mtbfile"])
|
||||||
class MtbFileRestController(
|
class MtbFileRestController(
|
||||||
private val requestProcessor: RequestProcessor,
|
private val requestProcessor: RequestProcessor,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java)
|
private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java)
|
||||||
|
|
||||||
@PostMapping(path = ["/mtbfile"])
|
@GetMapping
|
||||||
|
fun info(): ResponseEntity<String> {
|
||||||
|
return ResponseEntity.ok("Test")
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Void> {
|
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Void> {
|
||||||
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
||||||
logger.debug("Accepted MTB File for processing")
|
logger.debug("Accepted MTB File for processing")
|
||||||
@ -45,7 +51,7 @@ class MtbFileRestController(
|
|||||||
return ResponseEntity.accepted().build()
|
return ResponseEntity.accepted().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping(path = ["/mtbfile/{patientId}"])
|
@DeleteMapping(path = ["{patientId}"])
|
||||||
fun deleteData(@PathVariable patientId: String): ResponseEntity<Void> {
|
fun deleteData(@PathVariable patientId: String): ResponseEntity<Void> {
|
||||||
logger.debug("Accepted patient ID to process deletion")
|
logger.debug("Accepted patient ID to process deletion")
|
||||||
requestProcessor.processDeletion(patientId)
|
requestProcessor.processDeletion(patientId)
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS token
|
||||||
|
(
|
||||||
|
id int auto_increment primary key,
|
||||||
|
name varchar(255) not null,
|
||||||
|
username varchar(255) not null unique,
|
||||||
|
password varchar(255) not null,
|
||||||
|
created_at datetime default utc_timestamp() not null
|
||||||
|
);
|
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS token
|
||||||
|
(
|
||||||
|
id serial,
|
||||||
|
name varchar(255) not null,
|
||||||
|
username varchar(255) not null unique,
|
||||||
|
password varchar(255) not null,
|
||||||
|
created_at timestamp with time zone default now() not null,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
@ -4,14 +4,17 @@ const dateFormat = new Intl.DateTimeFormat('de-DE', dateFormatOptions);
|
|||||||
const dateTimeFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: 'numeric', second: 'numeric' };
|
const dateTimeFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: 'numeric', second: 'numeric' };
|
||||||
const dateTimeFormat = new Intl.DateTimeFormat('de-DE', dateTimeFormatOptions);
|
const dateTimeFormat = new Intl.DateTimeFormat('de-DE', dateTimeFormatOptions);
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
const formatTimeElements = () => {
|
||||||
Array.from(document.getElementsByTagName('time')).forEach((timeTag) => {
|
Array.from(document.getElementsByTagName('time')).forEach((timeTag) => {
|
||||||
let date = Date.parse(timeTag.getAttribute('datetime'));
|
let date = Date.parse(timeTag.getAttribute('datetime'));
|
||||||
if (! isNaN(date)) {
|
if (! isNaN(date)) {
|
||||||
timeTag.innerText = dateTimeFormat.format(date);
|
timeTag.innerText = dateTimeFormat.format(date);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
window.addEventListener('load', formatTimeElements);
|
||||||
|
window.addEventListener('htmx:afterRequest', formatTimeElements);
|
||||||
|
|
||||||
function drawPieChart(url, elemId, title, data) {
|
function drawPieChart(url, elemId, title, data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
@ -202,13 +202,15 @@ form.samplecode-input input:focus-visible {
|
|||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form form * {
|
.login-form form *,
|
||||||
|
.token-form form * {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border: 1px solid var(--table-border);
|
border: 1px solid var(--table-border);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form button {
|
.login-form button,
|
||||||
|
.token-form button {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
background: var(--bg-blue);
|
background: var(--bg-blue);
|
||||||
color: white;
|
color: white;
|
||||||
@ -536,3 +538,21 @@ a.reload {
|
|||||||
align-content: center;
|
align-content: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-token {
|
||||||
|
padding: 1em;
|
||||||
|
background: var(--bg-green-op);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-token > pre {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid var(--bg-green);
|
||||||
|
padding: .5em;
|
||||||
|
width: max-content;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-token {
|
||||||
|
padding: 1em;
|
||||||
|
background: var(--bg-red-op);
|
||||||
|
}
|
@ -37,6 +37,9 @@
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section th:insert="~{configs/tokens.html}">
|
||||||
|
</section>
|
||||||
|
|
||||||
<section hx-ext="sse" th:sse-connect="@{/configs/events}">
|
<section hx-ext="sse" th:sse-connect="@{/configs/events}">
|
||||||
<div th:insert="~{configs/connectionAvailable.html}" th:hx-get="@{/configs?connectionAvailable}" hx-trigger="sse:connection-available">
|
<div th:insert="~{configs/connectionAvailable.html}" th:hx-get="@{/configs?connectionAvailable}" hx-trigger="sse:connection-available">
|
||||||
</div>
|
</div>
|
||||||
@ -86,6 +89,7 @@
|
|||||||
</th:block>
|
</th:block>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
<script th:src="@{/scripts.js}"></script>
|
||||||
<script th:src="@{/webjars/htmx.org/dist/htmx.min.js}"></script>
|
<script th:src="@{/webjars/htmx.org/dist/htmx.min.js}"></script>
|
||||||
<script th:src="@{/webjars/htmx.org/dist/ext/sse.js}"></script>
|
<script th:src="@{/webjars/htmx.org/dist/ext/sse.js}"></script>
|
||||||
</body>
|
</body>
|
||||||
|
39
src/main/resources/templates/configs/tokens.html
Normal file
39
src/main/resources/templates/configs/tokens.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<div th:if="${not tokensEnabled}">
|
||||||
|
<h2><span>⛔</span> Tokens</h2>
|
||||||
|
<p>Die Verwendung von Tokens ist nicht aktiviert.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tokens" th:if="${tokensEnabled}">
|
||||||
|
<h2><span>✅</span> Tokens</h2>
|
||||||
|
<div class="border">
|
||||||
|
<div th:if="${tokens.isEmpty()}">Noch keine Tokens vorhanden.</div>
|
||||||
|
<table th:if="${not tokens.isEmpty()}">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Erstellt</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="token : ${tokens}">
|
||||||
|
<td>[[ ${token.name} ]]</td>
|
||||||
|
<td><time th:datetime="${token.createdAt}">[[ ${token.createdAt} ]]</time></td>
|
||||||
|
<td><button class="btn btn-red" th:hx-delete="@{/configs/tokens/{id}(id=${token.id})}" hx-target="#tokens">Löschen</button></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div th:if="${newTokenValue != null and success}" class="new-token">
|
||||||
|
Verwendung über HTTP-Basic. Bitte notieren, wird nicht erneut angezeigt: <pre>[[ ${newTokenValue} ]]</pre>
|
||||||
|
</div>
|
||||||
|
<div th:if="${success != null and not success}" class="no-token">
|
||||||
|
Das Token konnte nicht erzeugt werden. Versuchen Sie einen anderen Namen.
|
||||||
|
</div>
|
||||||
|
<div class="token-form">
|
||||||
|
<form th:hx-post="@{/configs/tokens}" hx-target="#tokens">
|
||||||
|
<input placeholder="Token-Name" name="name" required />
|
||||||
|
<button>Token Erstellen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -15,7 +15,7 @@
|
|||||||
<form method="post" th:action="@{/login}">
|
<form method="post" th:action="@{/login}">
|
||||||
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required="" autofocus="">
|
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required="" autofocus="">
|
||||||
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required="">
|
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required="">
|
||||||
<button class="" type="submit">Anmelden</button>
|
<button type="submit">Anmelden</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user