feat(fullstack): Read user out of JWT and persist with created and last modified

This commit is contained in:
2025-05-01 09:20:32 +02:00
parent aaea7d3b28
commit aee88ad261
14 changed files with 129 additions and 147 deletions

View File

@@ -240,48 +240,6 @@ paths:
"503": "503":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable" $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable"
/users:
get:
summary: Get all users
operationId: getAllUsers
tags:
- user
responses:
"200":
description: Paged list of users
content:
application/json:
schema:
$ref: "#/components/schemas/PagedUserDto"
"500":
description: Internal server error
post:
summary: Create a new user
operationId: createUser
tags:
- user
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserDTO"
responses:
"201":
description: Successfully created user
content:
application/json:
schema:
$ref: "#/components/schemas/UserDto"
"400":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/BadRequest"
"401":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized"
"500":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
"503":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable"
/users/{id}: /users/{id}:
parameters: parameters:
- name: id - name: id
@@ -607,77 +565,12 @@ components:
type: object type: object
required: required:
- id - id
- username - name
- firstname
- lastname
- email
- password
- roleId
- createdAt
- modifiedAt
properties: properties:
id: id:
type: string type: string
format: uuid name:
username:
type: string type: string
firstName:
type: string
lastName:
type: string
email:
type: string
format: email
password:
type: string
roleId:
type: string
format: uuid
createdAt:
type: string
format: date-time
modifiedAt:
type: string
format: date-time
CreateUserDTO:
type: object
required:
- username
- firstname
- lastname
- email
- password
- roleId
- createdAt
- modifiedAt
properties:
username:
type: string
firstName:
type: string
lastName:
type: string
email:
type: string
format: email
password:
type: string
roleId:
type: string
format: uuid
PagedUserDto:
type: object
allOf:
- $ref: "#/components/schemas/Page"
required:
- content
properties:
content:
type: array
items:
$ref: "#/components/schemas/UserDto"
####### ApplicationFormDto ####### ####### ApplicationFormDto #######
ApplicationFormDto: ApplicationFormDto:
@@ -704,9 +597,9 @@ components:
isTemplate: isTemplate:
type: boolean type: boolean
createdBy: createdBy:
type: string $ref: "#/components/schemas/UserDto"
lastModifiedBy: lastModifiedBy:
type: string $ref: "#/components/schemas/UserDto"
createdAt: createdAt:
type: string type: string
format: date-time format: date-time
@@ -732,10 +625,6 @@ components:
isTemplate: isTemplate:
type: boolean type: boolean
default: false default: false
createdBy:
type: string
lastModifiedBy:
type: string
PagedApplicationFormDto: PagedApplicationFormDto:
type: object type: object

View File

@@ -1,6 +1,9 @@
package com.betriebsratkanzlei.legalconsenthub.application_form package com.betriebsratkanzlei.legalconsenthub.application_form
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElement import com.betriebsratkanzlei.legalconsenthub.form_element.FormElement
import com.betriebsratkanzlei.legalconsenthub.user.User
import jakarta.persistence.AttributeOverride
import jakarta.persistence.AttributeOverrides
import jakarta.persistence.CascadeType import jakarta.persistence.CascadeType
import jakarta.persistence.Column import jakarta.persistence.Column
import jakarta.persistence.Entity import jakarta.persistence.Entity
@@ -8,6 +11,7 @@ import jakarta.persistence.EntityListeners
import jakarta.persistence.GeneratedValue import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id import jakarta.persistence.Id
import jakarta.persistence.OneToMany import jakarta.persistence.OneToMany
import jakarta.persistence.Embedded
import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.springframework.data.jpa.domain.support.AuditingEntityListener
@@ -30,11 +34,19 @@ class ApplicationForm(
@Column(nullable = false) @Column(nullable = false)
var isTemplate: Boolean, var isTemplate: Boolean,
@Column(nullable = false) @Embedded
var createdBy: String = "", @AttributeOverrides(
AttributeOverride(name = "id", column = Column(name = "created_by_id", nullable = false)),
AttributeOverride(name = "name", column = Column(name = "created_by_name", nullable = false))
)
var createdBy: User,
@Column(nullable = false) @Embedded
var lastModifiedBy: String = "", @AttributeOverrides(
AttributeOverride(name = "id", column = Column(name = "last_modified_by_id", nullable = false)),
AttributeOverride(name = "name", column = Column(name = "last_modified_by_name", nullable = false))
)
var lastModifiedBy: User,
@CreatedDate @CreatedDate
@Column(nullable = false) @Column(nullable = false)

View File

@@ -1,20 +1,24 @@
package com.betriebsratkanzlei.legalconsenthub.application_form package com.betriebsratkanzlei.legalconsenthub.application_form
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
import com.betriebsratkanzlei.legalconsenthub.user.User
import com.betriebsratkanzlei.legalconsenthub.user.UserMapper
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.time.LocalDateTime import java.time.LocalDateTime
@Component @Component
class ApplicationFormMapper(private val formElementMapper: FormElementMapper) { class ApplicationFormMapper(private val formElementMapper: FormElementMapper, private val userMapper: UserMapper) {
fun toApplicationFormDto(applicationForm: ApplicationForm): ApplicationFormDto { fun toApplicationFormDto(applicationForm: ApplicationForm): ApplicationFormDto {
return ApplicationFormDto( return ApplicationFormDto(
id = applicationForm.id ?: throw IllegalStateException("ApplicationForm ID must not be null!"), id = applicationForm.id ?: throw IllegalStateException("ApplicationForm ID must not be null!"),
name = applicationForm.name, name = applicationForm.name,
formElements = applicationForm.formElements.map { formElementMapper.toFormElementDto(it) }, formElements = applicationForm.formElements.map { formElementMapper.toFormElementDto(it) },
isTemplate = applicationForm.isTemplate, isTemplate = applicationForm.isTemplate,
createdBy = applicationForm.createdBy, createdBy = userMapper.toUserDto(applicationForm.createdBy),
lastModifiedBy = applicationForm.lastModifiedBy, lastModifiedBy = userMapper.toUserDto(applicationForm.lastModifiedBy),
createdAt = applicationForm.createdAt ?: LocalDateTime.now(), createdAt = applicationForm.createdAt ?: LocalDateTime.now(),
modifiedAt = applicationForm.modifiedAt ?: LocalDateTime.now() modifiedAt = applicationForm.modifiedAt ?: LocalDateTime.now()
) )
@@ -26,19 +30,23 @@ class ApplicationFormMapper(private val formElementMapper: FormElementMapper) {
name = applicationForm.name, name = applicationForm.name,
formElements = applicationForm.formElements.map { formElementMapper.toFormElement(it) }.toMutableList(), formElements = applicationForm.formElements.map { formElementMapper.toFormElement(it) }.toMutableList(),
isTemplate = applicationForm.isTemplate, isTemplate = applicationForm.isTemplate,
createdBy = applicationForm.createdBy, createdBy = userMapper.toUser(applicationForm.createdBy),
lastModifiedBy = applicationForm.lastModifiedBy, lastModifiedBy = userMapper.toUser(applicationForm.lastModifiedBy),
createdAt = applicationForm.createdAt, createdAt = applicationForm.createdAt,
modifiedAt = applicationForm.modifiedAt modifiedAt = applicationForm.modifiedAt
) )
} }
fun toApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm { fun toApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
val createdBy = User(principal.name ?: "UNKNOWN USER", principal.id ?: "")
val lastModifiedBy = User(principal.name ?: "UNKNOWN USER", principal.id ?: "")
val applicationForm = ApplicationForm( val applicationForm = ApplicationForm(
name = createApplicationFormDto.name, name = createApplicationFormDto.name,
isTemplate = createApplicationFormDto.isTemplate, isTemplate = createApplicationFormDto.isTemplate,
createdBy = createApplicationFormDto.createdBy, createdBy = createdBy,
lastModifiedBy = createApplicationFormDto.lastModifiedBy lastModifiedBy = lastModifiedBy,
) )
applicationForm.formElements = createApplicationFormDto.formElements applicationForm.formElements = createApplicationFormDto.formElements
.map { formElementMapper.toFormElement(it, applicationForm) } .map { formElementMapper.toFormElement(it, applicationForm) }

View File

@@ -0,0 +1,22 @@
package com.betriebsratkanzlei.legalconsenthub.config
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtAuthentication
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
import org.springframework.core.convert.converter.Converter
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.stereotype.Component
@Component
class CustomJwtAuthenticationConverter : Converter<Jwt, AbstractAuthenticationToken> {
override fun convert(jwt: Jwt): AbstractAuthenticationToken {
val authorities: Collection<GrantedAuthority> = emptyList()
val userId = jwt.getClaimAsString("id")
val username = jwt.getClaimAsString("name")
val principal = CustomJwtTokenPrincipal(userId, username)
return CustomJwtAuthentication(jwt, principal, authorities)
}
}

View File

@@ -15,7 +15,10 @@ import org.springframework.security.web.SecurityFilterChain
class SecurityConfig { class SecurityConfig {
@Bean @Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { fun securityFilterChain(
http: HttpSecurity,
customJwtAuthenticationConverter: CustomJwtAuthenticationConverter
): SecurityFilterChain {
http { http {
csrf { disable() } csrf { disable() }
authorizeHttpRequests { authorizeHttpRequests {
@@ -24,7 +27,7 @@ class SecurityConfig {
authorize(anyRequest, authenticated) authorize(anyRequest, authenticated)
} }
oauth2ResourceServer { oauth2ResourceServer {
jwt { } jwt { jwtAuthenticationConverter = customJwtAuthenticationConverter }
} }
} }

View File

@@ -0,0 +1,17 @@
package com.betriebsratkanzlei.legalconsenthub.security
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.security.oauth2.jwt.Jwt
class CustomJwtAuthentication(
jwt: Jwt,
private val principal: CustomJwtTokenPrincipal,
authorities: Collection<GrantedAuthority>
) : JwtAuthenticationToken(
jwt, authorities, principal.id
) {
override fun getPrincipal(): CustomJwtTokenPrincipal {
return principal
}
}

View File

@@ -0,0 +1,6 @@
package com.betriebsratkanzlei.legalconsenthub.security
data class CustomJwtTokenPrincipal(
val id: String? = null,
val name: String? = null
)

View File

@@ -0,0 +1,9 @@
package com.betriebsratkanzlei.legalconsenthub.user
import jakarta.persistence.Embeddable
@Embeddable
class User(
var name: String,
var id: String
)

View File

@@ -0,0 +1,21 @@
package com.betriebsratkanzlei.legalconsenthub.user
import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto
import org.springframework.stereotype.Component
@Component
class UserMapper() {
fun toUserDto(user: User): UserDto {
return UserDto(
id = user.id,
name = user.name,
)
}
fun toUser(userDto: UserDto): User {
return User(
id = userDto.id,
name = userDto.name,
)
}
}

View File

@@ -22,14 +22,6 @@ spring:
order_inserts: true order_inserts: true
enable_lazy_load_no_trans: true enable_lazy_load_no_trans: true
# security:
# oauth2:
# resourceserver:
# jwt:
# issuer-uri: http://192.168.178.105:3001
# jwk-set-uri: http://192.168.178.105:3001/api/auth/jwks
# jws-algorithms: ES512
liquibase: liquibase:
enabled: true enabled: true
drop-first: false drop-first: false

View File

@@ -4,8 +4,10 @@ create table application_form
created_at timestamp(6) not null, created_at timestamp(6) not null,
modified_at timestamp(6) not null, modified_at timestamp(6) not null,
id uuid not null, id uuid not null,
created_by varchar(255) not null, created_by_id varchar(255) not null,
last_modified_by varchar(255) not null, created_by_name varchar(255) not null,
last_modified_by_id varchar(255) not null,
last_modified_by_name varchar(255) not null,
name varchar(255) not null, name varchar(255) not null,
primary key (id) primary key (id)
); );

View File

@@ -62,7 +62,7 @@ const applicationForm = computed({
}) })
const isReadOnly = computed(() => { const isReadOnly = computed(() => {
return applicationForm.value?.createdBy !== user.value?.name return applicationForm.value?.createdBy.id !== user.value?.id
}) })
async function onSubmit() { async function onSubmit() {

View File

@@ -31,11 +31,11 @@
#{{ index }} {{ applicationFormElem.name }} #{{ index }} {{ applicationFormElem.name }}
</p> </p>
<p class="text-(--ui-text-muted) text-sm"> <p class="text-(--ui-text-muted) text-sm">
Zuletzt bearbeitet von {{ applicationFormElem.lastModifiedBy }} am Zuletzt bearbeitet von {{ applicationFormElem.lastModifiedBy.name }} am
{{ formatDate(applicationFormElem.modifiedAt) }} {{ formatDate(applicationFormElem.modifiedAt) }}
</p> </p>
<p class="text-(--ui-text-muted) text-sm"> <p class="text-(--ui-text-muted) text-sm">
Erstellt von {{ applicationFormElem.createdBy }} am {{ formatDate(applicationFormElem.createdAt) }} Erstellt von {{ applicationFormElem.createdBy.name }} am {{ formatDate(applicationFormElem.createdAt) }}
</p> </p>
</div> </div>
<div> <div>

View File

@@ -9,7 +9,8 @@ export const auth = betterAuth({
plugins: [ plugins: [
jwt({ jwt({
jwt: { jwt: {
issuer: 'http://192.168.178.105:3001' issuer: 'http://192.168.178.105:3001',
expirationTime: '48h'
}, },
jwks: { jwks: {
keyPairConfig: { keyPairConfig: {